Rewrote connection handling.

This has been in the works for over a month and might break things because it's a very radical change.
If it causes you to be unable to join chat, report it on the forum or try joining using the legacy chat on https://sockchat.flashii.net.
This commit is contained in:
flash 2024-04-17 15:42:50 +00:00
parent d01a1d454c
commit cf71bab92d
44 changed files with 2703 additions and 1775 deletions

View file

@ -21,9 +21,9 @@ const isDebugBuild = fs.existsSync(path.join(rootDir, '.debug'));
const buildTasks = {
js: [
{ source: 'websock.js', target: '/assets', name: 'mami-ws.{hash}.js', buildVar: 'MAMI_WS', buildVarsTarget: 'self' },
{ source: 'mami.js', target: '/assets', name: 'mami.{hash}.js', buildVar: 'MAMI_JS' },
{ source: 'mami-init.js', target: '/assets', name: 'mami-init.{hash}.js', es: 'es5' },
{ source: 'proto.js', target: '/assets', name: 'proto.{hash}.js', buildVar: 'MAMI_PROTO_JS', buildVarsTarget: 'self' },
{ source: 'mami.js', target: '/assets', name: 'mami.{hash}.js', buildVar: 'MAMI_MAIN_JS' },
{ source: 'init.js', target: '/assets', name: 'init.{hash}.js', es: 'es5' },
],
css: [
{ source: 'mami.css', target: '/assets', name: 'mami.{hash}.css' },

466
package-lock.json generated
View file

@ -5,21 +5,21 @@
"packages": {
"": {
"dependencies": {
"@swc/core": "^1.4.2",
"autoprefixer": "^10.4.17",
"cssnano": "^6.0.3",
"@swc/core": "^1.4.14",
"autoprefixer": "^10.4.19",
"cssnano": "^6.1.2",
"html-minifier-terser": "^7.2.0",
"postcss": "^8.4.35"
"postcss": "^8.4.38"
}
},
"node_modules/@jridgewell/gen-mapping": {
"version": "0.3.3",
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz",
"integrity": "sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==",
"version": "0.3.5",
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz",
"integrity": "sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==",
"dependencies": {
"@jridgewell/set-array": "^1.0.1",
"@jridgewell/set-array": "^1.2.1",
"@jridgewell/sourcemap-codec": "^1.4.10",
"@jridgewell/trace-mapping": "^0.3.9"
"@jridgewell/trace-mapping": "^0.3.24"
},
"engines": {
"node": ">=6.0.0"
@ -34,20 +34,20 @@
}
},
"node_modules/@jridgewell/set-array": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz",
"integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==",
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz",
"integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==",
"engines": {
"node": ">=6.0.0"
}
},
"node_modules/@jridgewell/source-map": {
"version": "0.3.5",
"resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.5.tgz",
"integrity": "sha512-UTYAUj/wviwdsMfzoSJspJxbkH5o1snzwX0//0ENX1u/55kkZZkcTZP6u9bwKGkv+dkk9at4m1Cpt0uY80kcpQ==",
"version": "0.3.6",
"resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.6.tgz",
"integrity": "sha512-1ZJTZebgqllO79ue2bm3rIGud/bOe0pP5BjSRCRxxYkEZS8STV7zN84UBbiYu7jy+eCKSnVIUgoWWE/tt+shMQ==",
"dependencies": {
"@jridgewell/gen-mapping": "^0.3.0",
"@jridgewell/trace-mapping": "^0.3.9"
"@jridgewell/gen-mapping": "^0.3.5",
"@jridgewell/trace-mapping": "^0.3.25"
}
},
"node_modules/@jridgewell/sourcemap-codec": {
@ -56,18 +56,18 @@
"integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg=="
},
"node_modules/@jridgewell/trace-mapping": {
"version": "0.3.22",
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.22.tgz",
"integrity": "sha512-Wf963MzWtA2sjrNt+g18IAln9lKnlRp+K2eH4jjIoF1wYeq3aMREpG09xhlhdzS0EjwU7qmUJYangWa+151vZw==",
"version": "0.3.25",
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz",
"integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==",
"dependencies": {
"@jridgewell/resolve-uri": "^3.1.0",
"@jridgewell/sourcemap-codec": "^1.4.14"
}
},
"node_modules/@swc/core": {
"version": "1.4.2",
"resolved": "https://registry.npmjs.org/@swc/core/-/core-1.4.2.tgz",
"integrity": "sha512-vWgY07R/eqj1/a0vsRKLI9o9klGZfpLNOVEnrv4nrccxBgYPjcf22IWwAoaBJ+wpA7Q4fVjCUM8lP0m01dpxcg==",
"version": "1.4.14",
"resolved": "https://registry.npmjs.org/@swc/core/-/core-1.4.14.tgz",
"integrity": "sha512-tHXg6OxboUsqa/L7DpsCcFnxhLkqN/ht5pCwav1HnvfthbiNIJypr86rNx4cUnQDJepETviSqBTIjxa7pSpGDQ==",
"hasInstallScript": true,
"dependencies": {
"@swc/counter": "^0.1.2",
@ -81,16 +81,16 @@
"url": "https://opencollective.com/swc"
},
"optionalDependencies": {
"@swc/core-darwin-arm64": "1.4.2",
"@swc/core-darwin-x64": "1.4.2",
"@swc/core-linux-arm-gnueabihf": "1.4.2",
"@swc/core-linux-arm64-gnu": "1.4.2",
"@swc/core-linux-arm64-musl": "1.4.2",
"@swc/core-linux-x64-gnu": "1.4.2",
"@swc/core-linux-x64-musl": "1.4.2",
"@swc/core-win32-arm64-msvc": "1.4.2",
"@swc/core-win32-ia32-msvc": "1.4.2",
"@swc/core-win32-x64-msvc": "1.4.2"
"@swc/core-darwin-arm64": "1.4.14",
"@swc/core-darwin-x64": "1.4.14",
"@swc/core-linux-arm-gnueabihf": "1.4.14",
"@swc/core-linux-arm64-gnu": "1.4.14",
"@swc/core-linux-arm64-musl": "1.4.14",
"@swc/core-linux-x64-gnu": "1.4.14",
"@swc/core-linux-x64-musl": "1.4.14",
"@swc/core-win32-arm64-msvc": "1.4.14",
"@swc/core-win32-ia32-msvc": "1.4.14",
"@swc/core-win32-x64-msvc": "1.4.14"
},
"peerDependencies": {
"@swc/helpers": "^0.5.0"
@ -102,9 +102,9 @@
}
},
"node_modules/@swc/core-darwin-arm64": {
"version": "1.4.2",
"resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.4.2.tgz",
"integrity": "sha512-1uSdAn1MRK5C1m/TvLZ2RDvr0zLvochgrZ2xL+lRzugLlCTlSA+Q4TWtrZaOz+vnnFVliCpw7c7qu0JouhgQIw==",
"version": "1.4.14",
"resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.4.14.tgz",
"integrity": "sha512-8iPfLhYNspBl836YYsfv6ErXwDUqJ7IMieddV3Ey/t/97JAEAdNDUdtTKDtbyP0j/Ebyqyn+fKcqwSq7rAof0g==",
"cpu": [
"arm64"
],
@ -117,9 +117,9 @@
}
},
"node_modules/@swc/core-darwin-x64": {
"version": "1.4.2",
"resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.4.2.tgz",
"integrity": "sha512-TYD28+dCQKeuxxcy7gLJUCFLqrwDZnHtC2z7cdeGfZpbI2mbfppfTf2wUPzqZk3gEC96zHd4Yr37V3Tvzar+lQ==",
"version": "1.4.14",
"resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.4.14.tgz",
"integrity": "sha512-9CqSj8uRZ92cnlgAlVaWMaJJBdxtNvCzJxaGj5KuIseeG6Q0l1g+qk8JcU7h9dAsH9saHTNwNFBVGKQo0W0ujg==",
"cpu": [
"x64"
],
@ -132,9 +132,9 @@
}
},
"node_modules/@swc/core-linux-arm-gnueabihf": {
"version": "1.4.2",
"resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.4.2.tgz",
"integrity": "sha512-Eyqipf7ZPGj0vplKHo8JUOoU1un2sg5PjJMpEesX0k+6HKE2T8pdyeyXODN0YTFqzndSa/J43EEPXm+rHAsLFQ==",
"version": "1.4.14",
"resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.4.14.tgz",
"integrity": "sha512-mfd5JArPITTzMjcezH4DwMw+BdjBV1y25Khp8itEIpdih9ei+fvxOOrDYTN08b466NuE2dF2XuhKtRLA7fXArQ==",
"cpu": [
"arm"
],
@ -147,9 +147,9 @@
}
},
"node_modules/@swc/core-linux-arm64-gnu": {
"version": "1.4.2",
"resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.4.2.tgz",
"integrity": "sha512-wZn02DH8VYPv3FC0ub4my52Rttsus/rFw+UUfzdb3tHMHXB66LqN+rR0ssIOZrH6K+VLN6qpTw9VizjyoH0BxA==",
"version": "1.4.14",
"resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.4.14.tgz",
"integrity": "sha512-3Lqlhlmy8MVRS9xTShMaPAp0oyUt0KFhDs4ixJsjdxKecE0NJSV/MInuDmrkij1C8/RQ2wySRlV9np5jK86oWw==",
"cpu": [
"arm64"
],
@ -162,9 +162,9 @@
}
},
"node_modules/@swc/core-linux-arm64-musl": {
"version": "1.4.2",
"resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.4.2.tgz",
"integrity": "sha512-3G0D5z9hUj9bXNcwmA1eGiFTwe5rWkuL3DsoviTj73TKLpk7u64ND0XjEfO0huVv4vVu9H1jodrKb7nvln/dlw==",
"version": "1.4.14",
"resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.4.14.tgz",
"integrity": "sha512-n0YoCa64TUcJrbcXIHIHDWQjdUPdaXeMHNEu7yyBtOpm01oMGTKP3frsUXIABLBmAVWtKvqit4/W1KVKn5gJzg==",
"cpu": [
"arm64"
],
@ -177,9 +177,9 @@
}
},
"node_modules/@swc/core-linux-x64-gnu": {
"version": "1.4.2",
"resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.4.2.tgz",
"integrity": "sha512-LFxn9U8cjmYHw3jrdPNqPAkBGglKE3tCZ8rA7hYyp0BFxuo7L2ZcEnPm4RFpmSCCsExFH+LEJWuMGgWERoktvg==",
"version": "1.4.14",
"resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.4.14.tgz",
"integrity": "sha512-CGmlwLWbfG1dB4jZBJnp2IWlK5xBMNLjN7AR5kKA3sEpionoccEnChOEvfux1UdVJQjLRKuHNV9yGyqGBTpxfQ==",
"cpu": [
"x64"
],
@ -192,9 +192,9 @@
}
},
"node_modules/@swc/core-linux-x64-musl": {
"version": "1.4.2",
"resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.4.2.tgz",
"integrity": "sha512-dp0fAmreeVVYTUcb4u9njTPrYzKnbIH0EhH2qvC9GOYNNREUu2GezSIDgonjOXkHiTCvopG4xU7y56XtXj4VrQ==",
"version": "1.4.14",
"resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.4.14.tgz",
"integrity": "sha512-xq4npk8YKYmNwmr8fbvF2KP3kUVdZYfXZMQnW425gP3/sn+yFQO8Nd0bGH40vOVQn41kEesSe0Z5O/JDor2TgQ==",
"cpu": [
"x64"
],
@ -207,9 +207,9 @@
}
},
"node_modules/@swc/core-win32-arm64-msvc": {
"version": "1.4.2",
"resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.4.2.tgz",
"integrity": "sha512-HlVIiLMQkzthAdqMslQhDkoXJ5+AOLUSTV6fm6shFKZKqc/9cJvr4S8UveNERL9zUficA36yM3bbfo36McwnvQ==",
"version": "1.4.14",
"resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.4.14.tgz",
"integrity": "sha512-imq0X+gU9uUe6FqzOQot5gpKoaC00aCUiN58NOzwp0QXEupn8CDuZpdBN93HiZswfLruu5jA1tsc15x6v9p0Yg==",
"cpu": [
"arm64"
],
@ -222,9 +222,9 @@
}
},
"node_modules/@swc/core-win32-ia32-msvc": {
"version": "1.4.2",
"resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.4.2.tgz",
"integrity": "sha512-WCF8faPGjCl4oIgugkp+kL9nl3nUATlzKXCEGFowMEmVVCFM0GsqlmGdPp1pjZoWc9tpYanoXQDnp5IvlDSLhA==",
"version": "1.4.14",
"resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.4.14.tgz",
"integrity": "sha512-cH6QpXMw5D3t+lpx6SkErHrxN0yFzmQ0lgNAJxoDRiaAdDbqA6Col8UqUJwUS++Ul6aCWgNhCdiEYehPaoyDPA==",
"cpu": [
"ia32"
],
@ -237,9 +237,9 @@
}
},
"node_modules/@swc/core-win32-x64-msvc": {
"version": "1.4.2",
"resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.4.2.tgz",
"integrity": "sha512-oV71rwiSpA5xre2C5570BhCsg1HF97SNLsZ/12xv7zayGzqr3yvFALFJN8tHKpqUdCB4FGPjoP3JFdV3i+1wUw==",
"version": "1.4.14",
"resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.4.14.tgz",
"integrity": "sha512-FmZ4Tby4wW65K/36BKzmuu7mlq7cW5XOxzvufaSNVvQ5PN4OodAlqPjToe029oma4Av+ykJiif64scMttyNAzg==",
"cpu": [
"x64"
],
@ -257,9 +257,12 @@
"integrity": "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ=="
},
"node_modules/@swc/types": {
"version": "0.1.5",
"resolved": "https://registry.npmjs.org/@swc/types/-/types-0.1.5.tgz",
"integrity": "sha512-myfUej5naTBWnqOCc/MdVOLVjXUXtIA+NpDrDBKJtLLg2shUjBu3cZmB/85RyitKc55+lUUyl7oRfLOvkr2hsw=="
"version": "0.1.6",
"resolved": "https://registry.npmjs.org/@swc/types/-/types-0.1.6.tgz",
"integrity": "sha512-/JLo/l2JsT/LRd80C3HfbmVpxOAJ11FO2RCEslFrgzLltoP9j8XIbsyDcfCt2WWyX+CM96rBoNM+IToAkFOugg==",
"dependencies": {
"@swc/counter": "^0.1.3"
}
},
"node_modules/@trysound/sax": {
"version": "0.2.0",
@ -281,9 +284,9 @@
}
},
"node_modules/autoprefixer": {
"version": "10.4.17",
"resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.17.tgz",
"integrity": "sha512-/cpVNRLSfhOtcGflT13P2794gVSgmPgTR+erw5ifnMLZb0UnSlkK4tquLmkd3BhA+nLo5tX8Cu0upUsGKvKbmg==",
"version": "10.4.19",
"resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.19.tgz",
"integrity": "sha512-BaENR2+zBZ8xXhM4pUaKUxlVdxZ0EZhjvbopwnXmxRUfqDmwSpC2lAi/QXvx7NRdPCo1WKEcEF6mV64si1z4Ew==",
"funding": [
{
"type": "opencollective",
@ -299,8 +302,8 @@
}
],
"dependencies": {
"browserslist": "^4.22.2",
"caniuse-lite": "^1.0.30001578",
"browserslist": "^4.23.0",
"caniuse-lite": "^1.0.30001599",
"fraction.js": "^4.3.7",
"normalize-range": "^0.1.2",
"picocolors": "^1.0.0",
@ -378,9 +381,9 @@
}
},
"node_modules/caniuse-lite": {
"version": "1.0.30001588",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001588.tgz",
"integrity": "sha512-+hVY9jE44uKLkH0SrUTqxjxqNTOWHsbnQDIKjwkZ3lNTzUUVdBLBGXtj/q5Mp5u98r3droaZAewQuEDzjQdZlQ==",
"version": "1.0.30001610",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001610.tgz",
"integrity": "sha512-QFutAY4NgaelojVMjY63o6XlZyORPaLfyMnsl3HgnWdJUcX6K0oaJymHjH8PT5Gk7sTm8rvC/c5COUQKXqmOMA==",
"funding": [
{
"type": "opencollective",
@ -421,9 +424,9 @@
}
},
"node_modules/css-declaration-sorter": {
"version": "7.1.1",
"resolved": "https://registry.npmjs.org/css-declaration-sorter/-/css-declaration-sorter-7.1.1.tgz",
"integrity": "sha512-dZ3bVTEEc1vxr3Bek9vGwfB5Z6ESPULhcRvO472mfjVnj8jRcTnKO8/JTczlvxM10Myb+wBM++1MtdO76eWcaQ==",
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/css-declaration-sorter/-/css-declaration-sorter-7.2.0.tgz",
"integrity": "sha512-h70rUM+3PNFuaBDTLe8wF/cdWu+dOZmb7pJt8Z2sedYbAcQVQV/tEchueg3GWxwqS0cxtbxmaHEdkNACqcvsow==",
"engines": {
"node": "^14 || ^16 || >=18"
},
@ -481,12 +484,12 @@
}
},
"node_modules/cssnano": {
"version": "6.0.3",
"resolved": "https://registry.npmjs.org/cssnano/-/cssnano-6.0.3.tgz",
"integrity": "sha512-MRq4CIj8pnyZpcI2qs6wswoYoDD1t0aL28n+41c1Ukcpm56m1h6mCexIHBGjfZfnTqtGSSCP4/fB1ovxgjBOiw==",
"version": "6.1.2",
"resolved": "https://registry.npmjs.org/cssnano/-/cssnano-6.1.2.tgz",
"integrity": "sha512-rYk5UeX7VAM/u0lNqewCdasdtPK81CgX8wJFLEIXHbV2oldWRgJAsZrdhRXkV1NJzA2g850KiFm9mMU2HxNxMA==",
"dependencies": {
"cssnano-preset-default": "^6.0.3",
"lilconfig": "^3.0.0"
"cssnano-preset-default": "^6.1.2",
"lilconfig": "^3.1.1"
},
"engines": {
"node": "^14 || ^16 || >=18.0"
@ -500,39 +503,40 @@
}
},
"node_modules/cssnano-preset-default": {
"version": "6.0.3",
"resolved": "https://registry.npmjs.org/cssnano-preset-default/-/cssnano-preset-default-6.0.3.tgz",
"integrity": "sha512-4y3H370aZCkT9Ev8P4SO4bZbt+AExeKhh8wTbms/X7OLDo5E7AYUUy6YPxa/uF5Grf+AJwNcCnxKhZynJ6luBA==",
"version": "6.1.2",
"resolved": "https://registry.npmjs.org/cssnano-preset-default/-/cssnano-preset-default-6.1.2.tgz",
"integrity": "sha512-1C0C+eNaeN8OcHQa193aRgYexyJtU8XwbdieEjClw+J9d94E41LwT6ivKH0WT+fYwYWB0Zp3I3IZ7tI/BbUbrg==",
"dependencies": {
"css-declaration-sorter": "^7.1.1",
"cssnano-utils": "^4.0.1",
"browserslist": "^4.23.0",
"css-declaration-sorter": "^7.2.0",
"cssnano-utils": "^4.0.2",
"postcss-calc": "^9.0.1",
"postcss-colormin": "^6.0.2",
"postcss-convert-values": "^6.0.2",
"postcss-discard-comments": "^6.0.1",
"postcss-discard-duplicates": "^6.0.1",
"postcss-discard-empty": "^6.0.1",
"postcss-discard-overridden": "^6.0.1",
"postcss-merge-longhand": "^6.0.2",
"postcss-merge-rules": "^6.0.3",
"postcss-minify-font-values": "^6.0.1",
"postcss-minify-gradients": "^6.0.1",
"postcss-minify-params": "^6.0.2",
"postcss-minify-selectors": "^6.0.2",
"postcss-normalize-charset": "^6.0.1",
"postcss-normalize-display-values": "^6.0.1",
"postcss-normalize-positions": "^6.0.1",
"postcss-normalize-repeat-style": "^6.0.1",
"postcss-normalize-string": "^6.0.1",
"postcss-normalize-timing-functions": "^6.0.1",
"postcss-normalize-unicode": "^6.0.2",
"postcss-normalize-url": "^6.0.1",
"postcss-normalize-whitespace": "^6.0.1",
"postcss-ordered-values": "^6.0.1",
"postcss-reduce-initial": "^6.0.2",
"postcss-reduce-transforms": "^6.0.1",
"postcss-svgo": "^6.0.2",
"postcss-unique-selectors": "^6.0.2"
"postcss-colormin": "^6.1.0",
"postcss-convert-values": "^6.1.0",
"postcss-discard-comments": "^6.0.2",
"postcss-discard-duplicates": "^6.0.3",
"postcss-discard-empty": "^6.0.3",
"postcss-discard-overridden": "^6.0.2",
"postcss-merge-longhand": "^6.0.5",
"postcss-merge-rules": "^6.1.1",
"postcss-minify-font-values": "^6.1.0",
"postcss-minify-gradients": "^6.0.3",
"postcss-minify-params": "^6.1.0",
"postcss-minify-selectors": "^6.0.4",
"postcss-normalize-charset": "^6.0.2",
"postcss-normalize-display-values": "^6.0.2",
"postcss-normalize-positions": "^6.0.2",
"postcss-normalize-repeat-style": "^6.0.2",
"postcss-normalize-string": "^6.0.2",
"postcss-normalize-timing-functions": "^6.0.2",
"postcss-normalize-unicode": "^6.1.0",
"postcss-normalize-url": "^6.0.2",
"postcss-normalize-whitespace": "^6.0.2",
"postcss-ordered-values": "^6.0.2",
"postcss-reduce-initial": "^6.1.0",
"postcss-reduce-transforms": "^6.0.2",
"postcss-svgo": "^6.0.3",
"postcss-unique-selectors": "^6.0.4"
},
"engines": {
"node": "^14 || ^16 || >=18.0"
@ -542,9 +546,9 @@
}
},
"node_modules/cssnano-utils": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/cssnano-utils/-/cssnano-utils-4.0.1.tgz",
"integrity": "sha512-6qQuYDqsGoiXssZ3zct6dcMxiqfT6epy7x4R0TQJadd4LWO3sPR6JH6ZByOvVLoZ6EdwPGgd7+DR1EmX3tiXQQ==",
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/cssnano-utils/-/cssnano-utils-4.0.2.tgz",
"integrity": "sha512-ZR1jHg+wZ8o4c3zqf1SIUSTIvm/9mU343FMR6Obe/unskbvpGhZOo1J6d/r8D1pzkRQYuwbcH3hToOuoA2G7oQ==",
"engines": {
"node": "^14 || ^16 || >=18.0"
},
@ -643,9 +647,9 @@
}
},
"node_modules/electron-to-chromium": {
"version": "1.4.677",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.677.tgz",
"integrity": "sha512-erDa3CaDzwJOpyvfKhOiJjBVNnMM0qxHq47RheVVwsSQrgBA9ZSGV9kdaOfZDPXcHzhG7lBxhj6A7KvfLJBd6Q=="
"version": "1.4.737",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.737.tgz",
"integrity": "sha512-QvLTxaLHKdy5YxvixAw/FfHq2eWLUL9KvsPjp0aHK1gI5d3EDuDgITkvj0nFO2c6zUY3ZqVAJQiBYyQP9tQpfw=="
},
"node_modules/entities": {
"version": "4.5.0",
@ -806,9 +810,9 @@
"integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ=="
},
"node_modules/postcss": {
"version": "8.4.35",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.35.tgz",
"integrity": "sha512-u5U8qYpBCpN13BsiEB0CbR1Hhh4Gc0zLFuedrHJKMctHCHAGrMdG0PRM/KErzAL3CU6/eckEtmHNB3x6e3c0vA==",
"version": "8.4.38",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.38.tgz",
"integrity": "sha512-Wglpdk03BSfXkHoQa3b/oulrotAkwrlLDRSOb9D0bN86FdRyE9lppSp33aHNPgBa0JKCoB+drFLZkQoRRYae5A==",
"funding": [
{
"type": "opencollective",
@ -826,7 +830,7 @@
"dependencies": {
"nanoid": "^3.3.7",
"picocolors": "^1.0.0",
"source-map-js": "^1.0.2"
"source-map-js": "^1.2.0"
},
"engines": {
"node": "^10 || ^12 || >=14"
@ -848,13 +852,13 @@
}
},
"node_modules/postcss-colormin": {
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/postcss-colormin/-/postcss-colormin-6.0.2.tgz",
"integrity": "sha512-TXKOxs9LWcdYo5cgmcSHPkyrLAh86hX1ijmyy6J8SbOhyv6ua053M3ZAM/0j44UsnQNIWdl8gb5L7xX2htKeLw==",
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/postcss-colormin/-/postcss-colormin-6.1.0.tgz",
"integrity": "sha512-x9yX7DOxeMAR+BgGVnNSAxmAj98NX/YxEMNFP+SDCEeNLb2r3i6Hh1ksMsnW8Ub5SLCpbescQqn9YEbE9554Sw==",
"dependencies": {
"browserslist": "^4.22.2",
"browserslist": "^4.23.0",
"caniuse-api": "^3.0.0",
"colord": "^2.9.1",
"colord": "^2.9.3",
"postcss-value-parser": "^4.2.0"
},
"engines": {
@ -865,11 +869,11 @@
}
},
"node_modules/postcss-convert-values": {
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/postcss-convert-values/-/postcss-convert-values-6.0.2.tgz",
"integrity": "sha512-aeBmaTnGQ+NUSVQT8aY0sKyAD/BaLJenEKZ03YK0JnDE1w1Rr8XShoxdal2V2H26xTJKr3v5haByOhJuyT4UYw==",
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/postcss-convert-values/-/postcss-convert-values-6.1.0.tgz",
"integrity": "sha512-zx8IwP/ts9WvUM6NkVSkiU902QZL1bwPhaVaLynPtCsOTqp+ZKbNi+s6XJg3rfqpKGA/oc7Oxk5t8pOQJcwl/w==",
"dependencies": {
"browserslist": "^4.22.2",
"browserslist": "^4.23.0",
"postcss-value-parser": "^4.2.0"
},
"engines": {
@ -880,9 +884,9 @@
}
},
"node_modules/postcss-discard-comments": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/postcss-discard-comments/-/postcss-discard-comments-6.0.1.tgz",
"integrity": "sha512-f1KYNPtqYLUeZGCHQPKzzFtsHaRuECe6jLakf/RjSRqvF5XHLZnM2+fXLhb8Qh/HBFHs3M4cSLb1k3B899RYIg==",
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/postcss-discard-comments/-/postcss-discard-comments-6.0.2.tgz",
"integrity": "sha512-65w/uIqhSBBfQmYnG92FO1mWZjJ4GL5b8atm5Yw2UgrwD7HiNiSSNwJor1eCFGzUgYnN/iIknhNRVqjrrpuglw==",
"engines": {
"node": "^14 || ^16 || >=18.0"
},
@ -891,9 +895,9 @@
}
},
"node_modules/postcss-discard-duplicates": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/postcss-discard-duplicates/-/postcss-discard-duplicates-6.0.1.tgz",
"integrity": "sha512-1hvUs76HLYR8zkScbwyJ8oJEugfPV+WchpnA+26fpJ7Smzs51CzGBHC32RS03psuX/2l0l0UKh2StzNxOrKCYg==",
"version": "6.0.3",
"resolved": "https://registry.npmjs.org/postcss-discard-duplicates/-/postcss-discard-duplicates-6.0.3.tgz",
"integrity": "sha512-+JA0DCvc5XvFAxwx6f/e68gQu/7Z9ud584VLmcgto28eB8FqSFZwtrLwB5Kcp70eIoWP/HXqz4wpo8rD8gpsTw==",
"engines": {
"node": "^14 || ^16 || >=18.0"
},
@ -902,9 +906,9 @@
}
},
"node_modules/postcss-discard-empty": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/postcss-discard-empty/-/postcss-discard-empty-6.0.1.tgz",
"integrity": "sha512-yitcmKwmVWtNsrrRqGJ7/C0YRy53i0mjexBDQ9zYxDwTWVBgbU4+C9jIZLmQlTDT9zhml+u0OMFJh8+31krmOg==",
"version": "6.0.3",
"resolved": "https://registry.npmjs.org/postcss-discard-empty/-/postcss-discard-empty-6.0.3.tgz",
"integrity": "sha512-znyno9cHKQsK6PtxL5D19Fj9uwSzC2mB74cpT66fhgOadEUPyXFkbgwm5tvc3bt3NAy8ltE5MrghxovZRVnOjQ==",
"engines": {
"node": "^14 || ^16 || >=18.0"
},
@ -913,9 +917,9 @@
}
},
"node_modules/postcss-discard-overridden": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/postcss-discard-overridden/-/postcss-discard-overridden-6.0.1.tgz",
"integrity": "sha512-qs0ehZMMZpSESbRkw1+inkf51kak6OOzNRaoLd/U7Fatp0aN2HQ1rxGOrJvYcRAN9VpX8kUF13R2ofn8OlvFVA==",
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/postcss-discard-overridden/-/postcss-discard-overridden-6.0.2.tgz",
"integrity": "sha512-j87xzI4LUggC5zND7KdjsI25APtyMuynXZSujByMaav2roV6OZX+8AaCUcZSWqckZpjAjRyFDdpqybgjFO0HJQ==",
"engines": {
"node": "^14 || ^16 || >=18.0"
},
@ -924,12 +928,12 @@
}
},
"node_modules/postcss-merge-longhand": {
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/postcss-merge-longhand/-/postcss-merge-longhand-6.0.2.tgz",
"integrity": "sha512-+yfVB7gEM8SrCo9w2lCApKIEzrTKl5yS1F4yGhV3kSim6JzbfLGJyhR1B6X+6vOT0U33Mgx7iv4X9MVWuaSAfw==",
"version": "6.0.5",
"resolved": "https://registry.npmjs.org/postcss-merge-longhand/-/postcss-merge-longhand-6.0.5.tgz",
"integrity": "sha512-5LOiordeTfi64QhICp07nzzuTDjNSO8g5Ksdibt44d+uvIIAE1oZdRn8y/W5ZtYgRH/lnLDlvi9F8btZcVzu3w==",
"dependencies": {
"postcss-value-parser": "^4.2.0",
"stylehacks": "^6.0.2"
"stylehacks": "^6.1.1"
},
"engines": {
"node": "^14 || ^16 || >=18.0"
@ -939,14 +943,14 @@
}
},
"node_modules/postcss-merge-rules": {
"version": "6.0.3",
"resolved": "https://registry.npmjs.org/postcss-merge-rules/-/postcss-merge-rules-6.0.3.tgz",
"integrity": "sha512-yfkDqSHGohy8sGYIJwBmIGDv4K4/WrJPX355XrxQb/CSsT4Kc/RxDi6akqn5s9bap85AWgv21ArcUWwWdGNSHA==",
"version": "6.1.1",
"resolved": "https://registry.npmjs.org/postcss-merge-rules/-/postcss-merge-rules-6.1.1.tgz",
"integrity": "sha512-KOdWF0gju31AQPZiD+2Ar9Qjowz1LTChSjFFbS+e2sFgc4uHOp3ZvVX4sNeTlk0w2O31ecFGgrFzhO0RSWbWwQ==",
"dependencies": {
"browserslist": "^4.22.2",
"browserslist": "^4.23.0",
"caniuse-api": "^3.0.0",
"cssnano-utils": "^4.0.1",
"postcss-selector-parser": "^6.0.15"
"cssnano-utils": "^4.0.2",
"postcss-selector-parser": "^6.0.16"
},
"engines": {
"node": "^14 || ^16 || >=18.0"
@ -956,9 +960,9 @@
}
},
"node_modules/postcss-minify-font-values": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/postcss-minify-font-values/-/postcss-minify-font-values-6.0.1.tgz",
"integrity": "sha512-tIwmF1zUPoN6xOtA/2FgVk1ZKrLcCvE0dpZLtzyyte0j9zUeB8RTbCqrHZGjJlxOvNWKMYtunLrrl7HPOiR46w==",
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/postcss-minify-font-values/-/postcss-minify-font-values-6.1.0.tgz",
"integrity": "sha512-gklfI/n+9rTh8nYaSJXlCo3nOKqMNkxuGpTn/Qm0gstL3ywTr9/WRKznE+oy6fvfolH6dF+QM4nCo8yPLdvGJg==",
"dependencies": {
"postcss-value-parser": "^4.2.0"
},
@ -970,12 +974,12 @@
}
},
"node_modules/postcss-minify-gradients": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/postcss-minify-gradients/-/postcss-minify-gradients-6.0.1.tgz",
"integrity": "sha512-M1RJWVjd6IOLPl1hYiOd5HQHgpp6cvJVLrieQYS9y07Yo8itAr6jaekzJphaJFR0tcg4kRewCk3kna9uHBxn/w==",
"version": "6.0.3",
"resolved": "https://registry.npmjs.org/postcss-minify-gradients/-/postcss-minify-gradients-6.0.3.tgz",
"integrity": "sha512-4KXAHrYlzF0Rr7uc4VrfwDJ2ajrtNEpNEuLxFgwkhFZ56/7gaE4Nr49nLsQDZyUe+ds+kEhf+YAUolJiYXF8+Q==",
"dependencies": {
"colord": "^2.9.1",
"cssnano-utils": "^4.0.1",
"colord": "^2.9.3",
"cssnano-utils": "^4.0.2",
"postcss-value-parser": "^4.2.0"
},
"engines": {
@ -986,12 +990,12 @@
}
},
"node_modules/postcss-minify-params": {
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/postcss-minify-params/-/postcss-minify-params-6.0.2.tgz",
"integrity": "sha512-zwQtbrPEBDj+ApELZ6QylLf2/c5zmASoOuA4DzolyVGdV38iR2I5QRMsZcHkcdkZzxpN8RS4cN7LPskOkTwTZw==",
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/postcss-minify-params/-/postcss-minify-params-6.1.0.tgz",
"integrity": "sha512-bmSKnDtyyE8ujHQK0RQJDIKhQ20Jq1LYiez54WiaOoBtcSuflfK3Nm596LvbtlFcpipMjgClQGyGr7GAs+H1uA==",
"dependencies": {
"browserslist": "^4.22.2",
"cssnano-utils": "^4.0.1",
"browserslist": "^4.23.0",
"cssnano-utils": "^4.0.2",
"postcss-value-parser": "^4.2.0"
},
"engines": {
@ -1002,11 +1006,11 @@
}
},
"node_modules/postcss-minify-selectors": {
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/postcss-minify-selectors/-/postcss-minify-selectors-6.0.2.tgz",
"integrity": "sha512-0b+m+w7OAvZejPQdN2GjsXLv5o0jqYHX3aoV0e7RBKPCsB7TYG5KKWBFhGnB/iP3213Ts8c5H4wLPLMm7z28Sg==",
"version": "6.0.4",
"resolved": "https://registry.npmjs.org/postcss-minify-selectors/-/postcss-minify-selectors-6.0.4.tgz",
"integrity": "sha512-L8dZSwNLgK7pjTto9PzWRoMbnLq5vsZSTu8+j1P/2GB8qdtGQfn+K1uSvFgYvgh83cbyxT5m43ZZhUMTJDSClQ==",
"dependencies": {
"postcss-selector-parser": "^6.0.15"
"postcss-selector-parser": "^6.0.16"
},
"engines": {
"node": "^14 || ^16 || >=18.0"
@ -1016,9 +1020,9 @@
}
},
"node_modules/postcss-normalize-charset": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/postcss-normalize-charset/-/postcss-normalize-charset-6.0.1.tgz",
"integrity": "sha512-aW5LbMNRZ+oDV57PF9K+WI1Z8MPnF+A8qbajg/T8PP126YrGX1f9IQx21GI2OlGz7XFJi/fNi0GTbY948XJtXg==",
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/postcss-normalize-charset/-/postcss-normalize-charset-6.0.2.tgz",
"integrity": "sha512-a8N9czmdnrjPHa3DeFlwqst5eaL5W8jYu3EBbTTkI5FHkfMhFZh1EGbku6jhHhIzTA6tquI2P42NtZ59M/H/kQ==",
"engines": {
"node": "^14 || ^16 || >=18.0"
},
@ -1027,9 +1031,9 @@
}
},
"node_modules/postcss-normalize-display-values": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/postcss-normalize-display-values/-/postcss-normalize-display-values-6.0.1.tgz",
"integrity": "sha512-mc3vxp2bEuCb4LgCcmG1y6lKJu1Co8T+rKHrcbShJwUmKJiEl761qb/QQCfFwlrvSeET3jksolCR/RZuMURudw==",
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/postcss-normalize-display-values/-/postcss-normalize-display-values-6.0.2.tgz",
"integrity": "sha512-8H04Mxsb82ON/aAkPeq8kcBbAtI5Q2a64X/mnRRfPXBq7XeogoQvReqxEfc0B4WPq1KimjezNC8flUtC3Qz6jg==",
"dependencies": {
"postcss-value-parser": "^4.2.0"
},
@ -1041,9 +1045,9 @@
}
},
"node_modules/postcss-normalize-positions": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/postcss-normalize-positions/-/postcss-normalize-positions-6.0.1.tgz",
"integrity": "sha512-HRsq8u/0unKNvm0cvwxcOUEcakFXqZ41fv3FOdPn916XFUrympjr+03oaLkuZENz3HE9RrQE9yU0Xv43ThWjQg==",
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/postcss-normalize-positions/-/postcss-normalize-positions-6.0.2.tgz",
"integrity": "sha512-/JFzI441OAB9O7VnLA+RtSNZvQ0NCFZDOtp6QPFo1iIyawyXg0YI3CYM9HBy1WvwCRHnPep/BvI1+dGPKoXx/Q==",
"dependencies": {
"postcss-value-parser": "^4.2.0"
},
@ -1055,9 +1059,9 @@
}
},
"node_modules/postcss-normalize-repeat-style": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/postcss-normalize-repeat-style/-/postcss-normalize-repeat-style-6.0.1.tgz",
"integrity": "sha512-Gbb2nmCy6tTiA7Sh2MBs3fj9W8swonk6lw+dFFeQT68B0Pzwp1kvisJQkdV6rbbMSd9brMlS8I8ts52tAGWmGQ==",
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/postcss-normalize-repeat-style/-/postcss-normalize-repeat-style-6.0.2.tgz",
"integrity": "sha512-YdCgsfHkJ2jEXwR4RR3Tm/iOxSfdRt7jplS6XRh9Js9PyCR/aka/FCb6TuHT2U8gQubbm/mPmF6L7FY9d79VwQ==",
"dependencies": {
"postcss-value-parser": "^4.2.0"
},
@ -1069,9 +1073,9 @@
}
},
"node_modules/postcss-normalize-string": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/postcss-normalize-string/-/postcss-normalize-string-6.0.1.tgz",
"integrity": "sha512-5Fhx/+xzALJD9EI26Aq23hXwmv97Zfy2VFrt5PLT8lAhnBIZvmaT5pQk+NuJ/GWj/QWaKSKbnoKDGLbV6qnhXg==",
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/postcss-normalize-string/-/postcss-normalize-string-6.0.2.tgz",
"integrity": "sha512-vQZIivlxlfqqMp4L9PZsFE4YUkWniziKjQWUtsxUiVsSSPelQydwS8Wwcuw0+83ZjPWNTl02oxlIvXsmmG+CiQ==",
"dependencies": {
"postcss-value-parser": "^4.2.0"
},
@ -1083,9 +1087,9 @@
}
},
"node_modules/postcss-normalize-timing-functions": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/postcss-normalize-timing-functions/-/postcss-normalize-timing-functions-6.0.1.tgz",
"integrity": "sha512-4zcczzHqmCU7L5dqTB9rzeqPWRMc0K2HoR+Bfl+FSMbqGBUcP5LRfgcH4BdRtLuzVQK1/FHdFoGT3F7rkEnY+g==",
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/postcss-normalize-timing-functions/-/postcss-normalize-timing-functions-6.0.2.tgz",
"integrity": "sha512-a+YrtMox4TBtId/AEwbA03VcJgtyW4dGBizPl7e88cTFULYsprgHWTbfyjSLyHeBcK/Q9JhXkt2ZXiwaVHoMzA==",
"dependencies": {
"postcss-value-parser": "^4.2.0"
},
@ -1097,11 +1101,11 @@
}
},
"node_modules/postcss-normalize-unicode": {
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/postcss-normalize-unicode/-/postcss-normalize-unicode-6.0.2.tgz",
"integrity": "sha512-Ff2VdAYCTGyMUwpevTZPZ4w0+mPjbZzLLyoLh/RMpqUqeQKZ+xMm31hkxBavDcGKcxm6ACzGk0nBfZ8LZkStKA==",
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/postcss-normalize-unicode/-/postcss-normalize-unicode-6.1.0.tgz",
"integrity": "sha512-QVC5TQHsVj33otj8/JD869Ndr5Xcc/+fwRh4HAsFsAeygQQXm+0PySrKbr/8tkDKzW+EVT3QkqZMfFrGiossDg==",
"dependencies": {
"browserslist": "^4.22.2",
"browserslist": "^4.23.0",
"postcss-value-parser": "^4.2.0"
},
"engines": {
@ -1112,9 +1116,9 @@
}
},
"node_modules/postcss-normalize-url": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/postcss-normalize-url/-/postcss-normalize-url-6.0.1.tgz",
"integrity": "sha512-jEXL15tXSvbjm0yzUV7FBiEXwhIa9H88JOXDGQzmcWoB4mSjZIsmtto066s2iW9FYuIrIF4k04HA2BKAOpbsaQ==",
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/postcss-normalize-url/-/postcss-normalize-url-6.0.2.tgz",
"integrity": "sha512-kVNcWhCeKAzZ8B4pv/DnrU1wNh458zBNp8dh4y5hhxih5RZQ12QWMuQrDgPRw3LRl8mN9vOVfHl7uhvHYMoXsQ==",
"dependencies": {
"postcss-value-parser": "^4.2.0"
},
@ -1126,9 +1130,9 @@
}
},
"node_modules/postcss-normalize-whitespace": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/postcss-normalize-whitespace/-/postcss-normalize-whitespace-6.0.1.tgz",
"integrity": "sha512-76i3NpWf6bB8UHlVuLRxG4zW2YykF9CTEcq/9LGAiz2qBuX5cBStadkk0jSkg9a9TCIXbMQz7yzrygKoCW9JuA==",
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/postcss-normalize-whitespace/-/postcss-normalize-whitespace-6.0.2.tgz",
"integrity": "sha512-sXZ2Nj1icbJOKmdjXVT9pnyHQKiSAyuNQHSgRCUgThn2388Y9cGVDR+E9J9iAYbSbLHI+UUwLVl1Wzco/zgv0Q==",
"dependencies": {
"postcss-value-parser": "^4.2.0"
},
@ -1140,11 +1144,11 @@
}
},
"node_modules/postcss-ordered-values": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/postcss-ordered-values/-/postcss-ordered-values-6.0.1.tgz",
"integrity": "sha512-XXbb1O/MW9HdEhnBxitZpPFbIvDgbo9NK4c/5bOfiKpnIGZDoL2xd7/e6jW5DYLsWxBbs+1nZEnVgnjnlFViaA==",
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/postcss-ordered-values/-/postcss-ordered-values-6.0.2.tgz",
"integrity": "sha512-VRZSOB+JU32RsEAQrO94QPkClGPKJEL/Z9PCBImXMhIeK5KAYo6slP/hBYlLgrCjFxyqvn5VC81tycFEDBLG1Q==",
"dependencies": {
"cssnano-utils": "^4.0.1",
"cssnano-utils": "^4.0.2",
"postcss-value-parser": "^4.2.0"
},
"engines": {
@ -1155,11 +1159,11 @@
}
},
"node_modules/postcss-reduce-initial": {
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/postcss-reduce-initial/-/postcss-reduce-initial-6.0.2.tgz",
"integrity": "sha512-YGKalhNlCLcjcLvjU5nF8FyeCTkCO5UtvJEt0hrPZVCTtRLSOH4z00T1UntQPj4dUmIYZgMj8qK77JbSX95hSw==",
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/postcss-reduce-initial/-/postcss-reduce-initial-6.1.0.tgz",
"integrity": "sha512-RarLgBK/CrL1qZags04oKbVbrrVK2wcxhvta3GCxrZO4zveibqbRPmm2VI8sSgCXwoUHEliRSbOfpR0b/VIoiw==",
"dependencies": {
"browserslist": "^4.22.2",
"browserslist": "^4.23.0",
"caniuse-api": "^3.0.0"
},
"engines": {
@ -1170,9 +1174,9 @@
}
},
"node_modules/postcss-reduce-transforms": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/postcss-reduce-transforms/-/postcss-reduce-transforms-6.0.1.tgz",
"integrity": "sha512-fUbV81OkUe75JM+VYO1gr/IoA2b/dRiH6HvMwhrIBSUrxq3jNZQZitSnugcTLDi1KkQh1eR/zi+iyxviUNBkcQ==",
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/postcss-reduce-transforms/-/postcss-reduce-transforms-6.0.2.tgz",
"integrity": "sha512-sB+Ya++3Xj1WaT9+5LOOdirAxP7dJZms3GRcYheSPi1PiTMigsxHAdkrbItHxwYHr4kt1zL7mmcHstgMYT+aiA==",
"dependencies": {
"postcss-value-parser": "^4.2.0"
},
@ -1184,9 +1188,9 @@
}
},
"node_modules/postcss-selector-parser": {
"version": "6.0.15",
"resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.15.tgz",
"integrity": "sha512-rEYkQOMUCEMhsKbK66tbEU9QVIxbhN18YiniAwA7XQYTVBqrBy+P2p5JcdqsHgKM2zWylp8d7J6eszocfds5Sw==",
"version": "6.0.16",
"resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.16.tgz",
"integrity": "sha512-A0RVJrX+IUkVZbW3ClroRWurercFhieevHB38sr2+l9eUClMqome3LmEmnhlNy+5Mr2EYN6B2Kaw9wYdd+VHiw==",
"dependencies": {
"cssesc": "^3.0.0",
"util-deprecate": "^1.0.2"
@ -1196,9 +1200,9 @@
}
},
"node_modules/postcss-svgo": {
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/postcss-svgo/-/postcss-svgo-6.0.2.tgz",
"integrity": "sha512-IH5R9SjkTkh0kfFOQDImyy1+mTCb+E830+9SV1O+AaDcoHTvfsvt6WwJeo7KwcHbFnevZVCsXhDmjFiGVuwqFQ==",
"version": "6.0.3",
"resolved": "https://registry.npmjs.org/postcss-svgo/-/postcss-svgo-6.0.3.tgz",
"integrity": "sha512-dlrahRmxP22bX6iKEjOM+c8/1p+81asjKT+V5lrgOH944ryx/OHpclnIbGsKVd3uWOXFLYJwCVf0eEkJGvO96g==",
"dependencies": {
"postcss-value-parser": "^4.2.0",
"svgo": "^3.2.0"
@ -1211,11 +1215,11 @@
}
},
"node_modules/postcss-unique-selectors": {
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/postcss-unique-selectors/-/postcss-unique-selectors-6.0.2.tgz",
"integrity": "sha512-8IZGQ94nechdG7Y9Sh9FlIY2b4uS8/k8kdKRX040XHsS3B6d1HrJAkXrBSsSu4SuARruSsUjW3nlSw8BHkaAYQ==",
"version": "6.0.4",
"resolved": "https://registry.npmjs.org/postcss-unique-selectors/-/postcss-unique-selectors-6.0.4.tgz",
"integrity": "sha512-K38OCaIrO8+PzpArzkLKB42dSARtC2tmG6PvD4b1o1Q2E9Os8jzfWFfSy/rixsHwohtsDdFtAWGjFVFUdwYaMg==",
"dependencies": {
"postcss-selector-parser": "^6.0.15"
"postcss-selector-parser": "^6.0.16"
},
"engines": {
"node": "^14 || ^16 || >=18.0"
@ -1246,9 +1250,9 @@
}
},
"node_modules/source-map-js": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz",
"integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==",
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.0.tgz",
"integrity": "sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==",
"engines": {
"node": ">=0.10.0"
}
@ -1263,12 +1267,12 @@
}
},
"node_modules/stylehacks": {
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/stylehacks/-/stylehacks-6.0.2.tgz",
"integrity": "sha512-00zvJGnCu64EpMjX8b5iCZ3us2Ptyw8+toEkb92VdmkEaRaSGBNKAoK6aWZckhXxmQP8zWiTaFaiMGIU8Ve8sg==",
"version": "6.1.1",
"resolved": "https://registry.npmjs.org/stylehacks/-/stylehacks-6.1.1.tgz",
"integrity": "sha512-gSTTEQ670cJNoaeIp9KX6lZmm8LJ3jPB5yJmX8Zq/wQxOsAFXV3qjWzHas3YYk1qesuVIyYWWUpZ0vSE/dTSGg==",
"dependencies": {
"browserslist": "^4.22.2",
"postcss-selector-parser": "^6.0.15"
"browserslist": "^4.23.0",
"postcss-selector-parser": "^6.0.16"
},
"engines": {
"node": "^14 || ^16 || >=18.0"
@ -1310,9 +1314,9 @@
}
},
"node_modules/terser": {
"version": "5.27.2",
"resolved": "https://registry.npmjs.org/terser/-/terser-5.27.2.tgz",
"integrity": "sha512-sHXmLSkImesJ4p5apTeT63DsV4Obe1s37qT8qvwHRmVxKTBH7Rv9Wr26VcAMmLbmk9UliiwK8z+657NyJHHy/w==",
"version": "5.30.3",
"resolved": "https://registry.npmjs.org/terser/-/terser-5.30.3.tgz",
"integrity": "sha512-STdUgOUx8rLbMGO9IOwHLpCqolkDITFFQSMYYwKE1N2lY6MVSaeoi10z/EhWxRc6ybqoVmKSkhKYH/XUpl7vSA==",
"dependencies": {
"@jridgewell/source-map": "^0.3.3",
"acorn": "^8.8.2",

View file

@ -1,9 +1,9 @@
{
"dependencies": {
"@swc/core": "^1.4.2",
"autoprefixer": "^10.4.17",
"cssnano": "^6.0.3",
"@swc/core": "^1.4.14",
"autoprefixer": "^10.4.19",
"cssnano": "^6.1.2",
"html-minifier-terser": "^7.2.0",
"postcss": "^8.4.35"
"postcss": "^8.4.38"
}
}

View file

@ -42,7 +42,7 @@
if(isCompatible) {
(function(script) {
script.src = MAMI_JS;
script.src = MAMI_MAIN_JS;
script.type = 'text/javascript';
script.charset = 'utf-8';
document.body.appendChild(script);

View file

@ -28,6 +28,6 @@
</div>
</div>
</noscript>
<script type="text/javascript" charset="utf-8" src="{mami-init.js}"></script>
<script type="text/javascript" charset="utf-8" src="{init.js}"></script>
</body>
</html>

19
src/mami.js/awaitable.js Normal file
View file

@ -0,0 +1,19 @@
const MamiSleep = durationMs => new Promise(resolve => { setTimeout(resolve, durationMs); });
const MamiWaitVisible = () => {
return new Promise(resolve => {
if(document.visibilityState === 'visible') {
resolve();
return;
}
const handler = () => {
if(document.visibilityState === 'visible') {
window.removeEventListener('visibilitychange', handler);
resolve();
}
};
window.addEventListener('visibilitychange', handler);
});
};

View file

@ -1,19 +0,0 @@
Umi.Channel = function(name, pass, temp, pm) {
name = (name || {}).toString();
pm = !!pm;
pass = !pm && !!pass;
temp = pm || !!temp;
return {
getName: function() { return name; },
setName: function(value) { name = (value || {}).toString(); },
hasPassword: function() { return pass; },
setHasPassword: function(value) { pass = !!value; },
isTemporary: function() { return temp; },
setTemporary: function(value) { temp = !!value; },
isUserChannel: function() { return pm; },
};
};

View file

@ -1,12 +1,48 @@
const MamiChannelInfo = function(name, hasPassword = false, isTemporary = true, isUserChannel = false) {
if(typeof name !== 'string')
throw 'name must be a string';
if(typeof hasPassword !== 'boolean')
throw 'hasPassword must be a boolean';
if(typeof isTemporary !== 'boolean')
throw 'isTemporary must be a boolean';
if(typeof isUserChannel !== 'boolean')
throw 'isUserChannel must be a boolean';
return {
get name() { return name; },
set name(value) {
if(typeof value !== 'string')
throw 'value must be a string';
name = value;
},
get hasPassword() { return hasPassword; },
set hasPassword(value) {
if(typeof value !== 'boolean')
throw 'value must be a boolean';
hasPassword = value;
},
get isTemporary() { return isTemporary; },
set isTemporary(value) {
if(typeof value !== 'boolean')
throw 'value must be a boolean';
isTemporary = value;
},
get isUserChannel() { return isUserChannel; },
};
};
Umi.Channels = (function() {
const chans = new Map;
let currentName = null;
const onAdd = [],
onRemove = [],
onClear = [],
onUpdate = [],
onSwitch = [];
const onAdd = [];
const onRemove = [];
const onClear = [];
const onUpdate = [];
const onSwitch = [];
return {
OnAdd: onAdd,
@ -15,7 +51,7 @@ Umi.Channels = (function() {
OnUpdate: onUpdate,
OnSwitch: onSwitch,
Add: function(channel) {
const channelName = channel.getName();
const channelName = channel.name;
if(!chans.has(channelName)) {
chans.set(channelName, channel);
@ -24,7 +60,7 @@ Umi.Channels = (function() {
}
},
Remove: function(channel) {
const channelName = channel.getName();
const channelName = channel.name;
if(chans.has(channelName)) {
chans.delete(channelName);

View file

@ -1,16 +1,32 @@
#include eventtarget.js
#include awaitable.js
#include utility.js
const MamiConnectionManager = function(urls) {
const MamiConnectionManager = function(client, settings, urls, eventTarget) {
const validateClient = value => {
if(typeof value !== 'object' || value === null)
throw 'client must be a non-null object';
};
validateClient(client);
if(typeof settings !== 'object' || settings === null)
throw 'settings must be a non-null object';
if(!Array.isArray(urls))
throw 'urls must be an array';
if(typeof eventTarget !== 'object' || eventTarget === null)
throw 'eventTarget must be a non-null object';
const eventTarget = new MamiEventTarget('mami:conn');
const delays = [0, 2000, 2000, 2000, 5000, 5000, 5000, 5000, 5000, 10000, 10000, 10000, 10000, 10000, 15000, 30000, 45000, 60000, 120000, 300000];
const delays = [
0,
2000, 2000, 2000, 2000, 2000,
5000, 5000, 5000, 5000, 5000,
10000, 10000, 10000, 10000, 10000,
15000, 30000, 45000, 60000, 120000,
300000
];
let timeout;
let attempts, started, delay, url;
let attemptConnect;
let startResolve;
const resetTimeout = () => {
@ -28,67 +44,74 @@ const MamiConnectionManager = function(urls) {
$as(urls);
const onFailure = ex => {
++attempts;
delay = attempts < delays.length ? delays[attempts] : delays[delays.length - 1];
eventTarget.dispatch('fail', {
url: url,
started: started,
attempt: attempts,
delay: delay,
error: ex,
});
attempt();
};
const attempt = () => {
started = Date.now();
url = urls[attempts % urls.length];
if(url.startsWith('//'))
url = location.protocol.replace('http', 'ws') + url;
const attempt = attempts + 1;
const attemptNo = attempts + 1;
timeout = setTimeout(() => {
resetTimeout();
eventTarget.dispatch('attempt', {
url: url,
started: started,
attempt: attempt,
});
(async () => {
if(settings.get('onlyConnectWhenVisible'))
await MamiWaitVisible();
attemptConnect(url).then(result => {
if(typeof result === 'boolean' && !result) {
onFailure();
return;
}
eventTarget.dispatch('success', {
eventTarget.dispatch('attempt', {
url: url,
started: started,
attempt: attempt,
attempt: attemptNo,
});
startResolve();
startResolve = undefined;
attemptConnect = undefined;
}).catch(ex => onFailure(ex));
try {
const result = await client.connect(url);
if(typeof result === 'boolean' && !result)
throw {};
eventTarget.dispatch('success', {
url: url,
started: started,
attempt: attemptNo,
});
startResolve();
startResolve = undefined;
} catch(ex) {
++attempts;
delay = attempts < delays.length ? delays[attempts] : delays[delays.length - 1];
eventTarget.dispatch('fail', {
url: url,
started: started,
attempt: attempts,
delay: delay,
error: ex,
});
attempt();
}
})();
}, delay);
};
const isActive = () => timeout !== undefined || startResolve !== undefined;
return {
isActive: isActive,
start: body => {
get isActive() { return isActive(); },
get client() { return client; },
set client(value) {
validateClient(value);
client = value;
},
start: () => {
return new Promise(resolve => {
if(typeof body !== 'function')
throw 'body must be a function';
if(isActive())
throw 'already attempting to connect';
attemptConnect = body;
startResolve = resolve;
clear();

View file

@ -1,15 +1,89 @@
const MamiContext = function(props) {
const pub = {};
#include events.js
for(const name in props) {
if(!props.hasOwnProperty(name))
continue;
const MamiContext = function(globalEventTarget, eventTarget) {
if(typeof globalEventTarget !== 'object' && globalEventTarget === null)
throw 'globalEventTarget must be undefined or a non-null object';
const value = props[name];
const descriptor = { enumerable: true };
descriptor[typeof value === 'function' ? 'get' : 'value'] = value;
Object.defineProperty(pub, name, descriptor);
}
if(eventTarget === undefined)
eventTarget = 'mami';
else if(typeof eventTarget !== 'object' || eventTarget === null)
throw 'eventTarget must be a string or a non-null object';
return Object.freeze(pub);
if(typeof eventTarget === 'string')
eventTarget = globalEventTarget.scopeTo(eventTarget);
let isUnloading = false;
let settings;
let views;
let sound;
let textTriggers;
let eeprom;
let conMan;
let protoWorker;
return {
get globalEvents() { return globalEventTarget; },
get events() { return eventTarget; },
get isUnloading() { return isUnloading; },
set isUnloading(state) {
isUnloading = !!state;
},
get settings() { return settings; },
set settings(value) {
if(settings !== undefined)
throw 'settings is already defined';
if(typeof value !== 'object' || value === null)
throw 'settings must be a non-null object';
settings = value;
},
get views() { return views; },
set views(value) {
if(views !== undefined)
throw 'views is already defined';
if(typeof value !== 'object' || value === null)
throw 'views must be a non-null object';
views = value;
},
get sound() { return sound; },
set sound(value) {
if(sound !== undefined)
throw 'sound is already defined';
if(typeof value !== 'object' || value === null)
throw 'sound must be a non-null object';
sound = value;
},
get textTriggers() { return textTriggers; },
set textTriggers(value) {
if(typeof value !== 'object' || value === null)
throw 'textTriggers must be a non-null object';
textTriggers = value;
},
get eeprom() { return eeprom; },
set eeprom(value) {
if(typeof value !== 'object' || value === null)
throw 'eeprom must be a non-null object';
eeprom = value;
},
get conMan() { return conMan; },
set conMan(value) {
if(typeof value !== 'object' || value === null)
throw 'conMan must be a non-null object';
conMan = value;
},
get protoWorker() { return protoWorker; },
set protoWorker(value) {
if(typeof value !== 'object' || value === null)
throw 'protoWorker must be a non-null object';
protoWorker = value;
},
};
};

31
src/mami.js/events.js Normal file
View file

@ -0,0 +1,31 @@
const MamiEventTargetScoped = function(eventTarget, prefix) {
if(typeof eventTarget !== 'object' || eventTarget === null)
throw 'eventTarget must be a non-null object';
if(typeof prefix !== 'string')
throw 'prefix must be a string';
if(!prefix.endsWith(':'))
prefix = prefix + ':';
return {
scopeTo: name => eventTarget.scopeTo(prefix + name),
create: (name, ...args) => eventTarget.create(prefix + name, ...args),
watch: (name, ...args) => eventTarget.watch(prefix + name, ...args),
unwatch: (name, ...args) => eventTarget.unwatch(prefix + name, ...args),
dispatch: (nameOrEvent, ...args) => eventTarget.dispatch(nameOrEvent instanceof Event ? nameOrEvent : (prefix + nameOrEvent), ...args),
};
};
const MamiEventTargetWindow = function() {
const createEvent = (name, detail) => new CustomEvent( name, (typeof detail === 'object' && detail !== null && 'detail' in detail ? detail : { detail: detail }));
const public = {
scopeTo: name => new MamiEventTargetScoped(public, name),
create: createEvent,
watch: (...args) => { window.addEventListener(...args); },
unwatch: (...args) => { window.removeEventListener(...args); },
dispatch: (nameOrEvent, ...args) => { window.dispatchEvent(nameOrEvent instanceof Event ? nameOrEvent : createEvent(nameOrEvent, ...args)); },
};
return public;
};

View file

@ -1,22 +0,0 @@
const MamiEventTarget = function(prefix) {
prefix = typeof prefix === 'string' ? `${prefix}:` : '';
const eventTarget = new EventTarget;
const createEvent = (name, detail) => new CustomEvent(prefix + name, (typeof detail === 'object' && detail !== null && 'detail' in detail ? detail : { detail: detail }));
return {
create: createEvent,
addEventListener: eventTarget.addEventListener.bind(eventTarget),
removeEventListener: eventTarget.removeEventListener.bind(eventTarget),
dispatchEvent: eventTarget.dispatchEvent.bind(eventTarget),
watch: (name, ...args) => {
eventTarget.addEventListener(prefix + name, ...args);
},
unwatch: (name, ...args) => {
eventTarget.removeEventListener(prefix + name, ...args);
},
dispatch: (...args) => eventTarget.dispatchEvent(createEvent(...args)),
};
};

View file

@ -1,49 +1,48 @@
window.Umi = { UI: {}, Protocol: { SockChat: { Protocol: {} } } };
#include animate.js
#include channel.js
#include channels.js
#include common.js
#include compat.js
#include conman.js
#include context.js
#include emotes.js
#include message.js
#include events.js
#include messages.js
#include mszauth.js
#include parsing.js
#include sockchat_old.js
#include txtrigs.js
#include user.js
#include users.js
#include utility.js
#include weeb.js
#include worker.js
#include audio/autoplay.js
#include audio/context.js
#include controls/views.js
#include eeprom/eeprom.js
#include settings/backup.js
#include settings/settings.js
#include sockchat/client.js
#include sockchat/handlers.js
#include sound/context.js
#include sound/osukeys.js
#include sound/umisound.js
#include ui/baka.jsx
#include ui/chat-layout.js
#include ui/hooks.js
#include ui/emotes.js
#include ui/input-menus.js
#include ui/loading-overlay.jsx
#include ui/markup.js
#include ui/menus.js
#include ui/messages.jsx
#include ui/view.js
#include ui/ping.jsx
#include ui/settings.jsx
#include ui/toggles.js
#include ui/uploads.js
#include ui/view.js
(async () => {
const eventTarget = new MamiEventTargetWindow;
const ctx = new MamiContext(eventTarget);
Object.defineProperty(window, 'mami', { enumerable: true, value: ctx });
const views = new MamiViewsControl({ body: document.body });
ctx.views = views;
const loadingOverlay = new Umi.UI.LoadingOverlay('spinner', 'Loading...');
await views.push(loadingOverlay);
@ -80,7 +79,8 @@ window.Umi = { UI: {}, Protocol: { SockChat: { Protocol: {} } } };
loadingOverlay.setMessage('Loading settings...');
const settings = new MamiSettings('umi-');
const settings = new MamiSettings('umi-', ctx.events.scopeTo('settings'));
ctx.settings = settings;
settings.define('style', 'string', 'dark');
settings.define('compactView', 'boolean', false);
@ -113,6 +113,7 @@ window.Umi = { UI: {}, Protocol: { SockChat: { Protocol: {} } } };
settings.define('seinfeld', 'boolean', false);
settings.define('flashTitle', 'boolean', true);
settings.define('showServerMsgInTitle', 'boolean', true);
settings.define('onlyConnectWhenVisible', 'boolean', true);
settings.define('playJokeSounds', 'boolean', true);
settings.define('weeaboo', 'boolean', false);
settings.define('motivationalImages', 'boolean', false);
@ -122,8 +123,6 @@ window.Umi = { UI: {}, Protocol: { SockChat: { Protocol: {} } } };
settings.define('explosionRadius', 'number', 20);
settings.define('dumpPackets', 'boolean', FUTAMI_DEBUG);
settings.define('dumpEvents', 'boolean', FUTAMI_DEBUG);
settings.define('neverUseWorker', 'boolean', false, false, true);
settings.define('forceUseWorker', 'boolean', false, false, true);
settings.define('marqueeAllNames', 'boolean', false);
settings.define('tmpDisableOldThemeSys', 'boolean', false, false, true);
@ -135,6 +134,7 @@ window.Umi = { UI: {}, Protocol: { SockChat: { Protocol: {} } } };
loadingOverlay.setMessage('Loading sounds...');
const soundCtx = new MamiSoundContext;
ctx.sound = soundCtx;
try {
const sounds = await futami.getJson('sounds2');
@ -206,33 +206,19 @@ window.Umi = { UI: {}, Protocol: { SockChat: { Protocol: {} } } };
});
let eeprom;
loadingOverlay.setMessage('Loading EEPROM...');
try {
await MamiEEPROM.init();
eeprom = new EEPROM('1', futami.get('eeprom2'), MamiMisuzuAuth.getLine);
ctx.eeprom = new EEPROM('1', futami.get('eeprom2'), MamiMisuzuAuth.getLine);
} catch(ex) {
console.error(ex);
eeprom = undefined;
ctx.eeprom = undefined;
}
loadingOverlay.setMessage('Preparing UI...');
const textTriggers = new MamiTextTriggers;
const conMan = new MamiConnectionManager(futami.get('servers'));
// define this as late as possible
const ctx = new MamiContext({
settings: settings,
views: views,
sound: soundCtx,
textTriggers: textTriggers,
eeprom: eeprom,
conMan: conMan,
});
Object.defineProperty(window, 'mami', { enumerable: true, value: ctx });
ctx.textTriggers = new MamiTextTriggers;
// should be dynamic when possible
const layout = new Umi.UI.ChatLayout;
@ -276,8 +262,8 @@ window.Umi = { UI: {}, Protocol: { SockChat: { Protocol: {} } } };
settings.watch('playJokeSounds', ev => {
if(!ev.detail.value) return;
if(!textTriggers.hasTriggers())
futami.getJson('texttriggers').then(trigInfos => textTriggers.addTriggers(trigInfos));
if(!ctx.textTriggers.hasTriggers())
futami.getJson('texttriggers').then(trigInfos => ctx.textTriggers.addTriggers(trigInfos));
});
settings.watch('weeaboo', ev => {
@ -315,11 +301,11 @@ window.Umi = { UI: {}, Protocol: { SockChat: { Protocol: {} } } };
Umi.UI.Settings.Init();
Umi.UI.Toggles.Add('menu-toggle', {
'click': function() {
const sidebar = $c('sidebar')[0],
toggle = Umi.UI.Toggles.Get('menu-toggle'),
toggleOpened = 'sidebar__selector-mode--menu-toggle-opened',
toggleClosed = 'sidebar__selector-mode--menu-toggle-closed',
isClosed = toggle.classList.contains(toggleClosed);
const sidebar = $c('sidebar')[0];
const toggle = Umi.UI.Toggles.Get('menu-toggle');
const toggleOpened = 'sidebar__selector-mode--menu-toggle-opened';
const toggleClosed = 'sidebar__selector-mode--menu-toggle-closed';
const isClosed = toggle.classList.contains(toggleClosed);
if(sidebarAnimation !== null) {
sidebarAnimation.cancel();
@ -429,12 +415,12 @@ window.Umi = { UI: {}, Protocol: { SockChat: { Protocol: {} } } };
pingToggle.appendChild(pingIndicator.getElement());
if(eeprom !== undefined) {
if(ctx.eeprom !== undefined) {
Umi.UI.Menus.Add('uploads', 'Upload History', !FUTAMI_DEBUG);
const doUpload = async file => {
const uploadEntry = Umi.UI.Uploads.create(file.name);
const uploadTask = eeprom.create(file);
const uploadTask = ctx.eeprom.create(file);
uploadTask.onProgress(prog => uploadEntry.setProgress(prog.progress));
uploadEntry.addOption('Cancel', () => uploadTask.abort());
@ -449,7 +435,7 @@ window.Umi = { UI: {}, Protocol: { SockChat: { Protocol: {} } } };
uploadEntry.addOption('Open', fileInfo.url);
uploadEntry.addOption('Insert', () => Umi.UI.Markup.InsertRaw(insertText, ''));
uploadEntry.addOption('Delete', () => {
eeprom.delete(fileInfo)
ctx.eeprom.delete(fileInfo)
.then(() => uploadEntry.remove())
.catch(ex => {
console.error(ex);
@ -537,22 +523,20 @@ window.Umi = { UI: {}, Protocol: { SockChat: { Protocol: {} } } };
Umi.UI.InputMenus.Add('markup', 'BB Code');
Umi.UI.InputMenus.Add('emotes', 'Emoticons');
let isUnloading = false;
window.addEventListener('beforeunload', function(ev) {
if(settings.get('closeTabConfirm')) {
ev.preventDefault();
return ev.returnValue = 'Are you sure you want to close the tab?';
}
isUnloading = true;
ctx.isUnloading = true;
});
// really not sure about all the watchers for the protocol just kinda being Listed here but we'll see i guess
loadingOverlay.setMessage('Connecting...');
const getLoadingOverlay = async (icon, header, message) => {
const setLoadingOverlay = async (icon, header, message, optional) => {
const currentView = views.current();
if('setIcon' in currentView) {
@ -562,377 +546,104 @@ window.Umi = { UI: {}, Protocol: { SockChat: { Protocol: {} } } };
return currentView;
}
const loading = new Umi.UI.LoadingOverlay(icon, header, message);
await views.push(loading);
return loading;
if(!optional) {
const loading = new Umi.UI.LoadingOverlay(icon, header, message);
await views.push(loading);
}
};
const wsCloseReasons = {
'_1000': 'The connection has been ended.',
'_1001': 'Something went wrong on the server side.',
'_1002': 'Your client sent broken data to the server.',
'_1003': 'Your client sent data to the server that it does not understand.',
'_1005': 'No additional information was provided.',
'_1006': 'You lost connection unexpectedly!',
'_1007': 'Your client sent broken data to the server.',
'_1008': 'Your client did something the server did not agree with.',
'_1009': 'Your client sent too much data to the server at once.',
'_1011': 'Something went wrong on the server side.',
'_1012': 'The chat server is restarting.',
'_1013': 'You cannot connect to the server right now, try again later.',
'_1015': 'Your client and the server could not establish a secure connection.',
};
const sessFailReasons = {
'authfail': 'Authentication failed.',
'sockfail': 'Too many active connections.',
'userfail': 'Name in use.',
'joinfail': 'You are banned.',
};
const protoWorker = new MamiWorker(MAMI_PROTO_JS, ctx.events.scopeTo('worker'));
ctx.protoWorker = protoWorker;
const sockChat = new Umi.Protocol.SockChat.Protocol(futami.get('ping') * 1000);
MamiCompat('Umi.Server', { get: () => sockChat, configurable: true });
const sockChat = new MamiSockChat(protoWorker);
const conMan = new MamiConnectionManager(sockChat, settings, futami.get('servers'), ctx.events.scopeTo('conn'));
ctx.conMan = conMan;
let dumpEvents = false;
settings.watch('dumpEvents', ev => dumpEvents = ev.detail.value);
settings.watch('dumpPackets', ev => sockChat.setDumpPackets(ev.detail.value));
let sockChatRestarting;
Umi.UI.Hooks.SetCallbacks(sockChat.sendMessage, sockChat.switchChannel);
MamiCompat('Umi.Server.SendMessage', { value: text => sockChat.sendMessage(text), configurable: true });
MamiCompat('Umi.Protocol.SockChat.Protocol.Instance.SendMessage', { value: text => sockChat.sendMessage(text), configurable: true });
MamiCompat('Umi.Protocol.SockLegacy.Protocol.Instance.SendMessage', { value: text => sockChat.sendMessage(text), configurable: true });
sockChat.watch('conn:lost', ev => {
if(dumpEvents) console.log('conn:lost', ev);
if(conMan.isActive() || isUnloading)
const sockChatReconnect = () => {
if(conMan.isActive)
return;
const errorCode = ev.detail.code;
const isRestarting = ev.detail.isRestarting;
pingToggle.title = '∞ms';
pingToggle.title = 'Reconnecting...';
pingIndicator.setStrength(-1);
const conManAttempt = ev => {
if(isRestarting || ev.detail.attempt > 1) {
let message = ev.detail.attempt > 2 ? `Attempt ${ev.detail.attempt}...` : 'Connecting to server...';
getLoadingOverlay('spinner', 'Connecting...', message);
}
const reconManAttempt = ev => {
if(sockChatRestarting || ev.detail.delay > 2000)
setLoadingOverlay('spinner', 'Connecting...', 'Connecting to server...');
};
const conManFail = ev => {
if(isRestarting || ev.detail.attempt > 1) {
let header = isRestarting ? 'Restarting...' : 'Disconnected';
let message = wsCloseReasons[`_${errorCode}`] ?? `Something caused an unexpected connection loss. (${errorCode})`;
if(ev.detail.delay > 0)
message += ` Retrying in ${ev.detail.delay / 1000} seconds...`;
// this is absolutely disgusting but i really don't care right now sorry
message += ' <a href="javascript:void(0)" onclick="mami.conMan.force()">Retry now</a>';
getLoadingOverlay('unlink', header, message);
}
const reconManFail = ev => {
// this is absolutely disgusting but i really don't care right now sorry
if(sockChatRestarting || ev.detail.delay > 2000)
setLoadingOverlay('unlink', sockChatRestarting ? 'Restarting...' : 'Disconnected', `Attempting to reconnect in ${(ev.detail.delay / 1000).toLocaleString()} seconds...<br><a href="javascript:void(0)" onclick="mami.conMan.force()">Retry now</a>`);
};
const reconManSuccess = () => {
conMan.unwatch('success', reconManSuccess);
conMan.unwatch('attempt', reconManAttempt);
conMan.unwatch('fail', reconManFail);
};
conMan.watch('attempt', conManAttempt);
conMan.watch('fail', conManFail);
conMan.watch('attempt', reconManAttempt);
conMan.watch('fail', reconManFail);
conMan.watch('success', reconManSuccess);
conMan.start(async url => {
await sockChat.open(url);
}).then(() => {
conMan.unwatch('attempt', conManAttempt);
conMan.unwatch('fail', conManFail);
conMan.start();
};
pingToggle.title = 'Ready~';
pingIndicator.setStrength(3);
const authInfo = MamiMisuzuAuth.getInfo();
sockChat.sendAuth(authInfo.method, authInfo.token);
});
});
sockChat.watch('ping:send', ev => {
if(dumpEvents) console.log('ping:send', ev);
});
sockChat.watch('ping:long', ev => {
if(dumpEvents) console.log('ping:long', ev);
pingToggle.title = '+2000ms';
pingIndicator.setStrength(0);
});
sockChat.watch('ping:recv', ev => {
if(dumpEvents) console.log('ping:recv', ev);
let strength = 3;
if(ev.detail.diff >= 400) --strength;
if(ev.detail.diff >= 200) --strength;
pingToggle.title = `${ev.detail.diff.toLocaleString()}ms`;
pingIndicator.setStrength(strength);
});
sockChat.watch('session:start', ev => {
if(dumpEvents) console.log('session:start', ev);
const userInfo = new Umi.User(ev.detail.user.id, ev.detail.user.name, ev.detail.user.colour, ev.detail.user.permsRaw);
Umi.User.setCurrentUser(userInfo);
Umi.Users.Add(userInfo);
Umi.UI.Markup.Reset();
Umi.UI.Emoticons.Init();
Umi.Parsing.Init();
if(views.count() > 1)
views.pop(ctx => MamiAnimate({
async: true,
duration: 120,
easing: 'inOutSine',
start: () => {
ctx.toElem.style.zIndex = '100';
ctx.fromElem.style.pointerEvents = 'none';
ctx.fromElem.style.zIndex = '200';
},
update: t => {
ctx.fromElem.style.transform = `scale(${1 + (.25 * t)})`;
ctx.fromElem.style.opacity = 1 - (1 * t).toString();
},
end: () => {
ctx.toElem.style.zIndex = null;
},
}));
});
sockChat.watch('session:fail', ev => {
if(dumpEvents) console.log('session:fail', ev);
if(ev.detail.baka !== undefined) {
new MamiForceDisconnectNotice(ev.detail.baka).pushOn(views);
return;
}
getLoadingOverlay(
'cross', 'Failed!',
sessFailReasons[ev.detail.session.reason] ?? `Unknown reason: ${ev.detail.session.reason}`
);
if(ev.detail.session.needsAuth)
setTimeout(() => location.assign(futami.get('login')), 1000);
});
sockChat.watch('session:term', ev => {
if(dumpEvents) console.log('session:term', ev);
new MamiForceDisconnectNotice(ev.detail.baka).pushOn(views);
});
sockChat.watch('user:add', ev => {
if(dumpEvents) console.log('user:add', ev);
if(ev.detail.user.self)
return;
const userInfo = new Umi.User(ev.detail.user.id, ev.detail.user.name, ev.detail.user.colour, ev.detail.user.permsRaw);
Umi.Users.Add(userInfo);
if(ev.detail.msg !== undefined)
Umi.Messages.Add(new Umi.Message(
ev.detail.msg.id, ev.detail.msg.time, undefined, '', ev.detail.msg.channel, false,
{
isError: false,
type: ev.detail.msg.botInfo.type,
args: ev.detail.msg.botInfo.args,
target: userInfo,
}
));
});
sockChat.watch('user:remove', ev => {
if(dumpEvents) console.log('user:remove', ev);
const userInfo = Umi.Users.Get(ev.detail.user.id);
if(userInfo === null)
return;
if(ev.detail.msg !== undefined)
Umi.Messages.Add(new Umi.Message(
ev.detail.msg.id,
ev.detail.msg.time,
undefined,
'',
ev.detail.msg.channel,
false,
{
isError: false,
type: ev.detail.msg.botInfo.type,
args: ev.detail.msg.botInfo.args,
target: userInfo,
},
));
Umi.Users.Remove(userInfo);
});
sockChat.watch('user:update', ev => {
if(dumpEvents) console.log('user:update', ev);
const userInfo = Umi.Users.Get(ev.detail.user.id);
userInfo.setName(ev.detail.user.name);
userInfo.setColour(ev.detail.user.colour);
userInfo.setPermissions(ev.detail.user.permsRaw);
Umi.Users.Update(userInfo.getId(), userInfo);
});
sockChat.watch('user:clear', ev => {
if(dumpEvents) console.log('user:clear', ev);
const self = Umi.User.currentUser;
Umi.Users.Clear();
if(self !== undefined)
Umi.Users.Add(self);
});
sockChat.watch('chan:add', ev => {
if(dumpEvents) console.log('chan:add', ev);
Umi.Channels.Add(new Umi.Channel(
ev.detail.channel.name,
ev.detail.channel.hasPassword,
ev.detail.channel.isTemporary,
));
});
sockChat.watch('chan:remove', ev => {
if(dumpEvents) console.log('chan:remove', ev);
Umi.Channels.Remove(Umi.Channels.Get(ev.detail.channel.name));
});
sockChat.watch('chan:update', ev => {
if(dumpEvents) console.log('chan:update', ev);
const chanInfo = Umi.Channels.Get(ev.detail.channel.previousName);
chanInfo.setName(ev.detail.channel.name);
chanInfo.setHasPassword(ev.detail.channel.hasPassword);
chanInfo.setTemporary(ev.detail.channel.isTemporary);
Umi.Channels.Update(ev.detail.channel.previousName, chanInfo);
});
sockChat.watch('chan:clear', ev => {
if(dumpEvents) console.log('chan:clear', ev);
Umi.Channels.Clear();
});
sockChat.watch('chan:focus', ev => {
if(dumpEvents) console.log('chan:focus', ev);
Umi.Channels.Switch(Umi.Channels.Get(ev.detail.channel.name));
});
sockChat.watch('chan:join', ev => {
if(dumpEvents) console.log('chan:join', ev);
const userInfo = new Umi.User(ev.detail.user.id, ev.detail.user.name, ev.detail.user.colour, ev.detail.user.permsRaw);
Umi.Users.Add(userInfo);
if(ev.detail.msg !== undefined)
Umi.Messages.Add(new Umi.Message(
ev.detail.msg.id, null, undefined, '', ev.detail.msg.channel, false,
{
isError: false,
type: leave.msg.botInfo.type,
args: [ userInfo.getName() ],
target: userInfo,
},
));
});
sockChat.watch('chan:leave', ev => {
if(dumpEvents) console.log('chan:leave', ev);
if(ev.detail.user.self)
return;
const userInfo = Umi.Users.Get(ev.detail.user.id);
if(userInfo === null)
return;
if(ev.detail.msg !== undefined)
Umi.Messages.Add(new Umi.Message(
ev.detail.msg.id, null, undefined, '', ev.detail.msg.channel, false,
{
isError: false,
type: ev.detail.msg.botInfo.type,
args: [ userInfo.getName() ],
target: userInfo,
},
));
Umi.Users.Remove(userInfo);
});
sockChat.watch('msg:add', ev => {
if(dumpEvents) console.log('msg:add', ev);
const senderInfo = ev.detail.msg.sender;
const userInfo = senderInfo.name === undefined
? Umi.Users.Get(senderInfo.id)
: new Umi.User(senderInfo.id, senderInfo.name, senderInfo.colour, senderInfo.permsRaw);
// hack
let channelName = ev.detail.msg.channel;
if(channelName !== undefined && channelName.startsWith('@~')) {
const chanUserInfo = Umi.Users.Get(channelName.substring(2));
if(chanUserInfo !== null)
channelName = `@${chanUserInfo.getName()}`;
}
// also hack
if(ev.detail.msg.flags.isPM) {
if(Umi.Channels.Get(channelName) === null)
Umi.Channels.Add(new Umi.Channel(channelName, false, true, true));
// this should be raised for other channels too, but that is not possible yet
Umi.UI.Menus.Attention('channels');
}
Umi.Messages.Add(new Umi.Message(
ev.detail.msg.id,
ev.detail.msg.time,
userInfo,
ev.detail.msg.text,
channelName,
false,
ev.detail.msg.botInfo,
ev.detail.msg.flags.isAction,
ev.detail.msg.silent,
));
});
sockChat.watch('msg:remove', ev => {
if(dumpEvents) console.log('msg:remove', ev);
Umi.Messages.Remove(Umi.Messages.Get(ev.detail.msg.id));
});
sockChat.watch('msg:clear', ev => {
if(dumpEvents) console.log('msg:clear', ev);
Umi.UI.Messages.RemoveAll();
});
const sockChatHandlers = new MamiSockChatHandlers(ctx, sockChat, setLoadingOverlay, sockChatReconnect, pingIndicator, pingToggle);
settings.watch('dumpEvents', ev => sockChatHandlers.setDumpEvents(ev.detail.value));
settings.watch('dumpPackets', ev => sockChat.setDumpPackets(ev.detail.value));
sockChatHandlers.register();
const conManAttempt = ev => {
let message = ev.detail.attempt > 2 ? `Attempt ${ev.detail.attempt}...` : 'Connecting to server...';
getLoadingOverlay('spinner', 'Connecting...', message);
setLoadingOverlay('spinner', 'Connecting...', message);
};
const conManFail = ev => {
getLoadingOverlay('cross', 'Failed to connect', `Retrying in ${ev.detail.delay / 1000} seconds...`);
setLoadingOverlay('cross', 'Failed to connect', `Retrying in ${ev.detail.delay / 1000} seconds...`);
};
const conManSuccess = () => {
conMan.unwatch('success', conManSuccess);
conMan.unwatch('attempt', conManAttempt);
conMan.unwatch('fail', conManFail);
};
conMan.watch('success', conManSuccess);
conMan.watch('attempt', conManAttempt);
conMan.watch('fail', conManFail);
await conMan.start(async url => {
await sockChat.open(url);
let workerStarting = false;
const initWorker = async () => {
if(workerStarting)
return;
workerStarting = true;
if(FUTAMI_DEBUG)
console.info('[proto] initialising worker...');
try {
await protoWorker.connect();
await sockChat.create();
conMan.client = sockChat;
await conMan.start();
} finally {
workerStarting = false;
}
};
protoWorker.watch(':timeout', ev => {
console.warn('worker timeout', ev.detail);
initWorker();
});
conMan.unwatch('attempt', conManAttempt);
conMan.unwatch('fail', conManFail);
window.addEventListener('visibilitychange', () => {
if(document.visibilityState === 'visible') {
protoWorker.ping().catch(ex => {
console.warn('worker died', ex);
initWorker();
});
}
});
getLoadingOverlay('spinner', 'Connecting...', 'Authenticating...');
const authInfo = MamiMisuzuAuth.getInfo();
sockChat.sendAuth(authInfo.method, authInfo.token);
if(window.dispatchEvent)
window.dispatchEvent(new Event('umi:connect'));
await initWorker();
})();

View file

@ -1,37 +0,0 @@
#include user.js
Umi.Message = (() => {
const chatBot = new Umi.User('-1', 'Server');
return function(msgId, time, user, text, channel, highlight, botInfo, isAction, isLog) {
msgId = (msgId || '').toString();
time = time === null ? new Date() : (typeof time === 'object' ? time : new Date(parseInt(time || 0) * 1000));
user = user !== null && typeof user === 'object' ? user : chatBot;
text = (text || '').toString();
channel = (channel || '').toString();
highlight = !!highlight;
isAction = !!isAction;
isLog = !!isLog;
hasSeen = isLog;
const msgIdInt = parseInt(msgId);
return {
getId: () => msgId,
getIdInt: () => {
const num = parseInt(msgId);
return isNaN(num) ? (Math.round(Number.MIN_SAFE_INTEGER * Math.random())) : num;
},
getTime: () => time,
getUser: () => user,
getText: () => text,
getChannel: () => channel,
shouldHighlight: () => highlight,
getBotInfo: () => botInfo,
isAction: () => isAction,
isLog: () => isLog,
hasSeen: () => hasSeen,
markSeen: () => hasSeen = true,
};
};
})();

View file

@ -1,6 +1,46 @@
#include channels.js
#include users.js
#include ui/messages.jsx
// messages should probably also be an "event" like how it is on the server
Umi.Message = (() => {
const chatBot = new MamiUserInfo('-1', 'Server');
return function(msgId, time, user, text, channel, highlight, botInfo, isAction, isLog) {
msgId = (msgId || '').toString();
time = time === null ? new Date() : (typeof time === 'object' ? time : new Date(parseInt(time || 0) * 1000));
user = user !== null && typeof user === 'object' ? user : chatBot;
text = (text || '').toString();
channel = (channel || '').toString();
highlight = !!highlight;
isAction = !!isAction;
isLog = !!isLog;
hasSeen = isLog;
const msgIdInt = parseInt(msgId);
return {
getId: () => msgId,
getIdInt: () => {
const num = parseInt(msgId);
return isNaN(num) ? (Math.round(Number.MIN_SAFE_INTEGER * Math.random())) : num;
},
getTime: () => time,
getUser: () => MamiConvertUserInfoToUmi(user),
getUserV2: () => user,
getText: () => text,
getChannel: () => channel,
shouldHighlight: () => highlight,
getBotInfo: () => botInfo,
isAction: () => isAction,
isLog: () => isLog,
hasSeen: () => hasSeen,
markSeen: () => hasSeen = true,
};
};
})();
Umi.Messages = (function() {
const msgs = new Map;
@ -10,11 +50,7 @@ Umi.Messages = (function() {
if(!msgs.has(msgId)) {
msgs.set(msgId, msg);
Umi.UI.Messages.Add(msg);
if(window.CustomEvent)
window.dispatchEvent(new CustomEvent('umi:message_add', {
detail: msg,
}));
mami.globalEvents.dispatch('umi:message_add', msg);
}
},
Remove: function(msg) {

View file

@ -1,9 +1,8 @@
#include eventtarget.js
#include settings/scoped.js
#include settings/virtual.js
#include settings/webstorage.js
const MamiSettings = function(storageOrPrefix) {
const MamiSettings = function(storageOrPrefix, eventTarget) {
if(typeof storageOrPrefix === 'string')
storageOrPrefix = new MamiSettingsWebStorage(window.localStorage, storageOrPrefix);
else if(typeof storageOrPrefix !== 'object')
@ -17,15 +16,14 @@ const MamiSettings = function(storageOrPrefix) {
const storage = new MamiSettingsVirtualStorage(storageOrPrefix);
const settings = new Map;
const eventTarget = new MamiEventTarget('mami:setting');
const createUpdateEvent = (name, value, initial) => eventTarget.create(name, {
name: name,
value: value,
initial: !!initial,
});
const dispatchUpdate = (name, value) => eventTarget.dispatchEvent(createUpdateEvent(name, value));
const dispatchUpdate = (name, value) => eventTarget.dispatch(createUpdateEvent(name, value));
const broadcast = new BroadcastChannel(`${MAMI_JS}:settings:${storage.name()}`);
const broadcast = new BroadcastChannel(`${MAMI_MAIN_JS}:settings:${storage.name()}`);
const broadcastUpdate = (name, value) => {
setTimeout(() => broadcast.postMessage({ act: 'update', name: name, value: value }), 0);
};

View file

@ -1 +0,0 @@
const MamiSleep = durationMs => new Promise(resolve => { setTimeout(resolve, durationMs); });

View file

@ -0,0 +1,51 @@
#include compat.js
#include mszauth.js
#include ui/hooks.js
const MamiSockChat = function(protoWorker) {
const events = protoWorker.eventTarget('sockchat');
let restarting = false;
let client;
let dumpPackets = false;
return {
get client() { return client; },
watch: events.watch,
unwatch: events.unwatch,
create: async () => {
if(client !== undefined && typeof client.close === 'function')
// intentional fire & forget, worker may be gone, don't want to wait for it to time out
client.close();
restarting = false;
client = await protoWorker.root.create('sockchat', { ping: futami.get('ping') });
await client.setDumpPackets(dumpPackets);
Umi.UI.Hooks.SetCallbacks(client.sendMessage, client.switchChannel);
MamiCompat('Umi.Server', { get: () => client, configurable: true });
MamiCompat('Umi.Server.SendMessage', { value: text => client.sendMessage(text), configurable: true });
MamiCompat('Umi.Protocol.SockChat.Protocol.Instance.SendMessage', { value: text => client.sendMessage(text), configurable: true });
MamiCompat('Umi.Protocol.SockLegacy.Protocol.Instance.SendMessage', { value: text => client.sendMessage(text), configurable: true });
},
connect: async url => {
try {
await client.open(url);
} catch(ex) {
return ex.wasKicked === true;
}
},
authenticate: async () => {
const authInfo = MamiMisuzuAuth.getInfo();
await client.sendAuth(authInfo.method, authInfo.token);
},
setDumpPackets: async state => {
dumpPackets = !!state;
if(client !== undefined && typeof client.setDumpPackets === 'function')
await client.setDumpPackets(dumpPackets);
},
};
};

View file

@ -0,0 +1,375 @@
#include animate.js
#include channels.js
#include messages.js
#include parsing.js
#include users.js
#include ui/baka.jsx
#include ui/emotes.js
#include ui/markup.js
#include ui/menus.js
#include ui/messages.jsx
const MamiSockChatHandlers = function(ctx, client, setLoadingOverlay, sockChatReconnect, pingIndicator, pingToggle) {
if(typeof ctx !== 'object' || ctx === null)
throw 'ctx must be an non-null object';
if(typeof client !== 'object' || client === null)
throw 'client must be an non-null object';
if(typeof setLoadingOverlay !== 'function')
throw 'setLoadingOverlay must be a function';
if(typeof sockChatReconnect !== 'function')
throw 'sockChatReconnect must be a function';
if(typeof pingIndicator !== 'object' || pingIndicator === null)
throw 'pingIndicator must be an non-null object';
if(!(pingToggle instanceof Element))
throw 'pingToggle must be an instance of Element';
const handlers = {};
let dumpEvents = false;
handlers['conn:open'] = ev => {
if(dumpEvents) console.log('conn:open', ev.detail);
setLoadingOverlay('spinner', 'Connecting...', 'Authenticating...', true);
};
let legacyUmiConnectFired = false;
handlers['conn:ready'] = ev => {
if(dumpEvents) console.log('conn:ready');
client.authenticate().then(() => {
if(!legacyUmiConnectFired) {
legacyUmiConnectFired = true;
ctx.globalEvents.dispatch('umi:connect');
}
});
};
handlers['conn:lost'] = ev => {
if(dumpEvents) console.log('conn:lost', ev.detail);
if(ctx.conMan.isActive || ctx.isUnloading || ev.detail.wasKicked)
return;
sockChatRestarting = ev.detail.isRestarting;
sockChatReconnect();
};
handlers['ping:send'] = () => {
if(dumpEvents) console.log('ping:send');
};
handlers['ping:long'] = ev => {
if(dumpEvents) console.log('ping:long', ev.detail);
sockChatReconnect();
};
handlers['ping:recv'] = ev => {
if(dumpEvents) console.log('ping:recv', ev.detail);
let strength = 3;
if(ev.detail.diff >= 800) --strength;
if(ev.detail.diff >= 400) --strength;
if(ev.detail.diff >= 200) --strength;
pingToggle.title = `${ev.detail.diff.toLocaleString()}ms`;
pingIndicator.setStrength(strength);
};
handlers['session:start'] = ev => {
if(dumpEvents) console.log('session:start', ev.detail);
sockChatRestarting = false;
const userInfo = new MamiUserInfo(
ev.detail.user.id,
ev.detail.user.name,
ev.detail.user.colour,
new MamiUserStatusInfo(ev.detail.user.status.isAway, ev.detail.user.status.message),
new MamiUserPermsInfo(
ev.detail.user.perms.rank, ev.detail.user.perms.kick,
ev.detail.user.perms.nick, ev.detail.user.perms.chan,
),
);
Umi.User.setCurrentUser(userInfo);
Umi.Users.Add(userInfo);
Umi.UI.Markup.Reset();
Umi.UI.Emoticons.Init();
Umi.Parsing.Init();
if(ctx.views.count() > 1)
ctx.views.pop(ctx => MamiAnimate({
async: true,
duration: 120,
easing: 'inOutSine',
start: () => {
ctx.toElem.style.zIndex = '100';
ctx.fromElem.style.pointerEvents = 'none';
ctx.fromElem.style.zIndex = '200';
},
update: t => {
ctx.fromElem.style.transform = `scale(${1 + (.25 * t)})`;
ctx.fromElem.style.opacity = 1 - (1 * t).toString();
},
end: () => {
ctx.toElem.style.zIndex = null;
},
}));
};
handlers['session:fail'] = ev => {
if(dumpEvents) console.log('session:fail', ev.detail);
if(ev.detail.baka !== undefined) {
new MamiForceDisconnectNotice(ev.detail.baka).pushOn(ctx.views);
return;
}
if(ev.detail.session.needsAuth) {
location.assign(futami.get('login'));
return;
}
setLoadingOverlay('cross', 'Failed!', ev.detail.session.outOfConnections ? 'Too many active connections.' : 'Unspecified reason.');
};
handlers['session:term'] = ev => {
if(dumpEvents) console.log('session:term', ev.detail);
new MamiForceDisconnectNotice(ev.detail.baka).pushOn(ctx.views);
};
handlers['user:add'] = ev => {
if(dumpEvents) console.log('user:add', ev.detail);
if(ev.detail.user.self)
return;
const userInfo = new MamiUserInfo(
ev.detail.user.id,
ev.detail.user.name,
ev.detail.user.colour,
new MamiUserStatusInfo(ev.detail.user.status.isAway, ev.detail.user.status.message),
new MamiUserPermsInfo(
ev.detail.user.perms.rank, ev.detail.user.perms.kick,
ev.detail.user.perms.nick, ev.detail.user.perms.chan,
),
);
Umi.Users.Add(userInfo);
if(ev.detail.msg !== undefined)
Umi.Messages.Add(new Umi.Message(
ev.detail.msg.id, ev.detail.msg.time, undefined, '', ev.detail.msg.channel, false,
{
isError: false,
type: ev.detail.msg.botInfo.type,
args: ev.detail.msg.botInfo.args,
target: userInfo,
}
));
};
handlers['user:remove'] = ev => {
if(dumpEvents) console.log('user:remove', ev.detail);
const userInfo = Umi.Users.Get(ev.detail.user.id);
if(userInfo === null)
return;
if(ev.detail.msg !== undefined)
Umi.Messages.Add(new Umi.Message(
ev.detail.msg.id,
ev.detail.msg.time,
undefined,
'',
ev.detail.msg.channel,
false,
{
isError: false,
type: ev.detail.msg.botInfo.type,
args: ev.detail.msg.botInfo.args,
target: userInfo,
},
));
Umi.Users.Remove(userInfo);
};
handlers['user:update'] = ev => {
if(dumpEvents) console.log('user:update', ev.detail);
const userInfo = Umi.Users.Get(ev.detail.user.id);
userInfo.name = ev.detail.user.name;
userInfo.colour = ev.detail.user.colour;
userInfo.avatarChangeTime = Date.now();
userInfo.status = new MamiUserStatusInfo(ev.detail.user.status.isAway, ev.detail.user.status.message);
userInfo.perms = new MamiUserPermsInfo(
ev.detail.user.perms.rank, ev.detail.user.perms.kick,
ev.detail.user.perms.nick, ev.detail.user.perms.chan,
);
Umi.Users.Update(userInfo.id, userInfo);
};
handlers['user:clear'] = () => {
if(dumpEvents) console.log('user:clear');
const self = Umi.User.getCurrentUser();
Umi.Users.Clear();
if(self !== undefined)
Umi.Users.Add(self);
};
handlers['chan:add'] = ev => {
if(dumpEvents) console.log('chan:add', ev.detail);
Umi.Channels.Add(new MamiChannelInfo(
ev.detail.channel.name,
ev.detail.channel.hasPassword,
ev.detail.channel.isTemporary,
));
};
handlers['chan:remove'] = ev => {
if(dumpEvents) console.log('chan:remove', ev.detail);
Umi.Channels.Remove(Umi.Channels.Get(ev.detail.channel.name));
};
handlers['chan:update'] = ev => {
if(dumpEvents) console.log('chan:update', ev.detail);
const chanInfo = Umi.Channels.Get(ev.detail.channel.previousName);
chanInfo.name = ev.detail.channel.name;
chanInfo.hasPassword = ev.detail.channel.hasPassword;
chanInfo.isTemporary = ev.detail.channel.isTemporary;
Umi.Channels.Update(ev.detail.channel.previousName, chanInfo);
};
handlers['chan:clear'] = () => {
if(dumpEvents) console.log('chan:clear');
Umi.Channels.Clear();
};
handlers['chan:focus'] = ev => {
if(dumpEvents) console.log('chan:focus', ev.detail);
Umi.Channels.Switch(Umi.Channels.Get(ev.detail.channel.name));
};
handlers['chan:join'] = ev => {
if(dumpEvents) console.log('chan:join', ev.detail);
const userInfo = new MamiUserInfo(
ev.detail.user.id,
ev.detail.user.name,
ev.detail.user.colour,
new MamiUserStatusInfo(ev.detail.user.status.isAway, ev.detail.user.status.message),
new MamiUserPermsInfo(
ev.detail.user.perms.rank, ev.detail.user.perms.kick,
ev.detail.user.perms.nick, ev.detail.user.perms.chan,
)
);
Umi.Users.Add(userInfo);
if(ev.detail.msg !== undefined)
Umi.Messages.Add(new Umi.Message(
ev.detail.msg.id, null, undefined, '', ev.detail.msg.channel, false,
{
isError: false,
type: leave.msg.botInfo.type,
args: [ userInfo.name ],
target: userInfo,
},
));
};
handlers['chan:leave'] = ev => {
if(dumpEvents) console.log('chan:leave', ev.detail);
if(ev.detail.user.self)
return;
const userInfo = Umi.Users.Get(ev.detail.user.id);
if(userInfo === null)
return;
if(ev.detail.msg !== undefined)
Umi.Messages.Add(new Umi.Message(
ev.detail.msg.id, null, undefined, '', ev.detail.msg.channel, false,
{
isError: false,
type: ev.detail.msg.botInfo.type,
args: [ userInfo.name ],
target: userInfo,
},
));
Umi.Users.Remove(userInfo);
};
handlers['msg:add'] = ev => {
if(dumpEvents) console.log('msg:add', ev.detail);
const senderInfo = ev.detail.msg.sender;
const userInfo = senderInfo.name === undefined
? Umi.Users.Get(senderInfo.id)
: new MamiUserInfo(
senderInfo.id,
senderInfo.name,
senderInfo.colour,
new MamiUserStatusInfo(senderInfo.status.isAway, senderInfo.status.message),
new MamiUserPermsInfo(
senderInfo.perms.rank, senderInfo.perms.kick,
senderInfo.perms.nick, senderInfo.perms.chan,
)
);
// hack
let channelName = ev.detail.msg.channel;
if(channelName !== undefined && channelName.startsWith('@~')) {
const chanUserInfo = Umi.Users.Get(channelName.substring(2));
if(chanUserInfo !== null)
channelName = `@${chanUserInfo.name}`;
}
// also hack
if(ev.detail.msg.flags.isPM) {
if(Umi.Channels.Get(channelName) === null)
Umi.Channels.Add(new MamiChannelInfo(channelName, false, true, true));
// this should be raised for other channels too, but that is not possible yet
Umi.UI.Menus.Attention('channels');
}
Umi.Messages.Add(new Umi.Message(
ev.detail.msg.id,
ev.detail.msg.time,
userInfo,
ev.detail.msg.text,
channelName,
false,
ev.detail.msg.botInfo,
ev.detail.msg.flags.isAction,
ev.detail.msg.silent,
));
};
handlers['msg:remove'] = ev => {
if(dumpEvents) console.log('msg:remove', ev.detail);
Umi.Messages.Remove(Umi.Messages.Get(ev.detail.msg.id));
};
handlers['msg:clear'] = () => {
if(dumpEvents) console.log('msg:clear');
Umi.UI.Messages.RemoveAll();
};
return {
setDumpEvents: state => {
dumpEvents = !!state;
},
register: () => {
for(const name in handlers)
client.watch(name, handlers[name]);
},
unregister: () => {
for(const name in handlers)
client.unwatch(name, handlers[name]);
},
};
};

View file

@ -1,613 +0,0 @@
#include eventtarget.js
#include websock.js
Umi.Protocol.SockChat.Protocol = function(pingDuration) {
if(typeof pingDuration !== 'number')
throw 'pingDuration must be a number';
const eventTarget = new MamiEventTarget('mami:proto');
const parseUserColour = str => {
// todo
return str;
};
let parseUserPermsSep;
const parseUserPerms = str => {
parseUserPermsSep ??= str.includes("\f") ? "\f" : ' ';
return str.split(parseUserPermsSep);
};
const parseMsgFlags = str => {
return {
nameBold: str[0] !== '0',
nameItalics: str[1] !== '0',
nameUnderline: str[2] !== '0',
showColon: str[3] !== '0',
isPM: str[4] !== '0',
isAction: str[1] !== '0' && str[3] === '0',
};
};
let wasConnected = false;
let wasKicked = false;
let isRestarting = false;
let dumpPackets = false;
let sock;
let selfUserId, selfChannelName, selfPseudoChannelName;
let lastPing, lastPong, pingTimer, pingWatcher;
let openResolve, openReject;
const handlers = {};
const stopPingWatcher = () => {
if(pingWatcher !== undefined) {
clearTimeout(pingWatcher);
pingWatcher = undefined;
}
};
const startPingWatcher = () => {
if(pingWatcher === undefined)
pingWatcher = setTimeout(() => {
stopPingWatcher();
if(lastPong === undefined)
eventTarget.dispatch('ping:long');
}, 2000);
};
const send = (...args) => {
if(args.length < 1)
throw 'you must specify at least one argument as an opcode';
const pack = args.join("\t");
if(dumpPackets)
console.log(pack);
sock?.send(pack);
};
const onSendPing = () => {
if(selfUserId === undefined)
return;
eventTarget.dispatch('ping:send');
startPingWatcher();
lastPong = undefined;
lastPing = Date.now();
};
const sendAuth = (...args) => {
if(selfUserId === undefined)
send('1', ...args);
};
const sendMessage = text => {
if(selfUserId === undefined)
return;
if(text.substring(0, 1) !== '/' && selfPseudoChannelName !== undefined)
text = `/msg ${selfPseudoChannelName} ${text}`;
send('2', selfUserId, text);
};
const startKeepAlive = () => sock?.sendInterval(`0\t${selfUserId}`, pingDuration);
const stopKeepAlive = () => sock?.clearIntervals();
const onOpen = ev => {
if(dumpPackets)
console.log(ev);
isRestarting = false;
if(typeof openResolve === 'function') {
openResolve();
openResolve = undefined;
}
};
const onClose = ev => {
if(dumpPackets)
console.log(ev);
selfUserId = undefined;
selfChannelName = undefined;
selfPseudoChannelName = undefined;
stopPingWatcher();
stopKeepAlive();
if(wasKicked)
return;
let code = ev.detail.code;
if(isRestarting && code === 1006) {
code = 1012;
} else if(code === 1012)
isRestarting = true;
if(typeof openReject === 'function') {
openReject({
code: code,
wasConnected: wasConnected,
isRestarting: isRestarting,
});
openReject = undefined;
}
eventTarget.dispatch('conn:lost', {
wasConnected: wasConnected,
isRestarting: isRestarting,
code: code,
});
};
const unfuckText = (text, isAction) => {
// P7.1 doesn't wrap in <i>, likely a bug in SharpChat
// check if this is the case with the PHPChat impl
if(isAction && text.startsWith('<i>'))
text = text.slice(3, -4);
return text.replace(/ <br\/> /g, "\n")
.replace(/&lt;/g, '<')
.replace(/&gt;/g, '>');
};
const onMessage = ev => {
const args = ev.detail.data.split("\t");
let handler = handlers;
if(dumpPackets)
console.log(args);
for(;;) {
handler = handler[args.shift()];
if(handler === undefined)
break;
if(typeof handler === 'function') {
handler(...args);
break;
}
}
};
// pong handler
handlers['0'] = () => {
lastPong = Date.now();
eventTarget.dispatch('ping:recv', {
ping: lastPing,
pong: lastPong,
diff: lastPong - lastPing,
});
};
// join/auth
handlers['1'] = (successOrTimeStamp, userIdOrReason, userNameOrExpiry, userColour, userPerms, chanNameOrMsgId, maxLength) => {
if(successOrTimeStamp === 'y') {
selfUserId = userIdOrReason;
selfChannelName = chanNameOrMsgId;
eventTarget.dispatch('session:start', {
wasConnected: wasConnected,
session: { success: true },
ctx: {
maxMsgLength: parseInt(maxLength),
},
user: {
id: selfUserId,
self: true,
name: userNameOrExpiry,
colour: parseUserColour(userColour),
perms: parseUserPerms(userPerms),
permsRaw: userPerms,
},
channel: {
name: selfChannelName,
},
});
startKeepAlive();
wasConnected = true;
return;
}
if(successOrTimeStamp === 'n') {
wasKicked = true;
const failInfo = {
session: {
success: false,
reason: userIdOrReason,
needsAuth: userIdOrReason === 'authfail',
},
};
if(userNameOrExpiry !== undefined)
failInfo.baka = {
type: 'join',
perma: userNameOrExpiry === '-1',
until: userNameOrExpiry === '-1' ? undefined : new Date(parseInt(userNameOrExpiry) * 1000),
};
eventTarget.dispatch('session:fail', failInfo);
return;
}
eventTarget.dispatch('user:add', {
msg: {
id: chanNameOrMsgId,
time: new Date(parseInt(successOrTimeStamp) * 1000),
channel: selfChannelName,
botInfo: {
type: 'join',
args: [userNameOrExpiry],
},
},
user: {
id: userIdOrReason,
self: userIdOrReason === selfUserId,
name: userNameOrExpiry,
colour: parseUserColour(userColour),
perms: parseUserPerms(userPerms),
permsRaw: userPerms,
},
});
};
// message add
handlers['2'] = (timeStamp, userId, msgText, msgId, msgFlags) => {
const mFlags = parseMsgFlags(msgFlags);
let mText = unfuckText(msgText, mFlags.isAction);
let mChannelName = selfChannelName;
if(msgFlags[4] !== '0') {
if(userId === selfUserId) {
const mTextParts = mText.split(' ');
mChannelName = `@${mTextParts.shift()}`;
mText = mTextParts.join(' ');
} else {
mChannelName = `@~${userId}`;
}
}
const msgInfo = {
msg: {
id: msgId,
time: new Date(parseInt(timeStamp) * 1000),
channel: mChannelName,
sender: {
id: userId,
self: userId === selfUserId,
},
flags: mFlags,
flagsRaw: msgFlags,
isBot: userId === '-1',
text: mText,
},
};
if(msgInfo.msg.isBot) {
const botParts = msgText.split("\f");
msgInfo.msg.botInfo = {
isError: botParts[0] === '1',
type: botParts[1],
args: botParts.slice(2),
};
}
eventTarget.dispatch('msg:add', msgInfo);
};
// user leave
handlers['3'] = (userId, userName, reason, timeStamp, msgId) => {
eventTarget.dispatch('user:remove', {
leave: { type: reason },
msg: {
id: msgId,
time: new Date(parseInt(timeStamp) * 1000),
channel: selfChannelName,
botInfo: {
type: reason,
args: [userName],
},
},
user: {
id: userId,
self: userId === selfUserId,
name: userName,
},
});
};
// channel add/upd/del
handlers['4'] = {};
// channel add
handlers['4']['0'] = (name, hasPass, isTemp) => {
eventTarget.dispatch('chan:add', {
channel: {
name: name,
hasPassword: hasPass !== '0',
isTemporary: isTemp !== '0',
},
});
};
// channel update
handlers['4']['1'] = (prevName, name, hasPass, isTemp) => {
eventTarget.dispatch('chan:update', {
channel: {
previousName: prevName,
name: name,
hasPassword: hasPass !== '0',
isTemporary: isTemp !== '0',
},
});
};
// channel remove
handlers['4']['2'] = name => {
eventTarget.dispatch('chan:remove', {
channel: { name: name },
});
};
// user channel move
handlers['5'] = {};
// user join channel
handlers['5']['0'] = (userId, userName, userColour, userPerms, msgId) => {
eventTarget.dispatch('chan:join', {
user: {
id: userId,
self: userId === selfUserId,
name: userName,
colour: parseUserColour(userColour),
perms: parseUserPerms(userPerms),
permsRaw: userPerms,
},
msg: {
id: msgId,
channel: selfChannelName,
botInfo: {
type: 'jchan',
args: [userName],
},
},
});
};
// user leave channel
handlers['5']['1'] = (userId, msgId) => {
eventTarget.dispatch('chan:leave', {
user: {
id: userId,
self: userId === selfUserId,
},
msg: {
id: msgId,
channel: selfChannelName,
botInfo: {
type: 'lchan',
args: [userId],
},
},
});
};
// user forced switch channel
handlers['5']['2'] = name => {
selfChannelName = name;
eventTarget.dispatch('chan:focus', {
channel: { name: selfChannelName },
});
};
// message delete
handlers['6'] = msgId => {
eventTarget.dispatch('msg:remove', {
msg: {
id: msgId,
channel: selfChannelName,
},
});
};
// context populate
handlers['7'] = {};
// existing users
handlers['7']['0'] = (count, ...args) => {
count = parseInt(count);
eventTarget.dispatch('user:clear');
for(let i = 0; i < count; ++i) {
const offset = 5 * i;
eventTarget.dispatch('user:add', {
user: {
id: args[offset],
self: args[offset] === selfUserId,
name: args[offset + 1],
colour: parseUserColour(args[offset + 2]),
perms: parseUserPerms(args[offset + 3]),
permsRaw: args[offset + 3],
hidden: args[offset + 4] !== '0',
},
});
}
};
// existing message
handlers['7']['1'] = (timeStamp, userId, userName, userColour, userPerms, msgText, msgId, msgNotify, msgFlags) => {
const mFlags = parseMsgFlags(msgFlags);
const info = {
msg: {
id: msgId,
time: new Date(parseInt(timeStamp) * 1000),
channel: selfChannelName,
sender: {
id: userId,
self: userId === selfUserId,
name: userName,
colour: parseUserColour(userColour),
perms: parseUserColour(userPerms),
permsRaw: userPerms,
},
isBot: userId === '-1',
silent: msgNotify === '0',
flags: mFlags,
flagsRaw: msgFlags,
text: unfuckText(msgText, mFlags.isAction),
},
};
const msgIdFirst = info.msg.id.charCodeAt(0);
if(msgIdFirst < 48 || msgIdFirst > 57)
info.msg.id = (Math.round(Number.MIN_SAFE_INTEGER * Math.random())).toString();
if(info.msg.isBot) {
const botParts = msgText.split("\f");
info.msg.botInfo = {
isError: botParts[0] === '1',
type: botParts[1],
args: botParts.slice(2),
};
// i think this is more Inaccurate Behaviour on the server side
if(info.msg.botInfo.type === 'say')
info.msg.botInfo.args[0] = unfuckText(info.msg.botInfo.args[0]);
}
eventTarget.dispatch('msg:add', info);
};
// existing channels
handlers['7']['2'] = (count, ...args) => {
count = parseInt(count);
eventTarget.dispatch('chan:clear');
for(let i = 0; i < count; ++i) {
const offset = 3 * i;
eventTarget.dispatch('chan:add', {
channel: {
name: args[offset],
hasPassword: args[offset + 1] !== '0',
isTemporary: args[offset + 2] !== '0',
isCurrent: args[offset] === selfChannelName,
},
});
}
eventTarget.dispatch('chan:focus', {
channel: { name: selfChannelName },
});
};
// context clear
handlers['8'] = mode => {
if(mode === '0' || mode === '3' || mode === '4')
eventTarget.dispatch('msg:clear');
if(mode === '1' || mode === '3' || mode === '4')
eventTarget.dispatch('user:clear');
if(mode === '2' || mode === '4')
eventTarget.dispatch('chan:clear');
};
// baka (ban/kick)
handlers['9'] = (type, expiry) => {
wasKicked = true;
const bakaInfo = {
session: { success: false },
baka: {
type: type === '0' ? 'kick' : 'ban',
},
};
if(bakaInfo.baka.type === 'ban') {
bakaInfo.baka.perma = expiry === '-1';
bakaInfo.baka.until = expiry === '-1' ? undefined : new Date(parseInt(expiry) * 1000);
}
eventTarget.dispatch('session:term', bakaInfo);
};
// user update
handlers['10'] = (userId, userName, userColour, userPerms) => {
eventTarget.dispatch('user:update', {
user: {
id: userId,
self: userId === selfUserId,
name: userName,
colour: parseUserColour(userColour),
perms: parseUserPerms(userPerms),
permsRaw: userPerms,
},
});
};
return {
sendAuth: sendAuth,
sendMessage: sendMessage,
open: url => {
return new Promise((resolve, reject) => {
if(typeof url !== 'string')
throw 'url must be a string';
if(url.startsWith('//'))
url = location.protocol.replace('http', 'ws') + url;
openResolve = resolve;
openReject = reject;
sock?.close();
sock = new UmiWebSocket(url);
sock.watch('open', onOpen);
sock.watch('close', onClose);
sock.watch('message', onMessage);
sock.watch('create_interval', ev => {
pingTimer = ev.detail.id;
});
sock.watch('call_interval', ev => {
if(ev.detail.id === pingTimer)
onSendPing();
});
sock.watch('create_intervals', ev => {
pingTimer = undefined;
});
});
},
close: () => {
sock?.close();
sock = undefined;
},
watch: eventTarget.watch,
unwatch: eventTarget.unwatch,
setDumpPackets: state => dumpPackets = !!state,
switchChannel: channelInfo => {
if(selfUserId === undefined)
return;
const name = channelInfo.getName();
if(channelInfo.isUserChannel()) {
selfPseudoChannelName = name.substring(1);
} else {
selfPseudoChannelName = undefined;
if(selfChannelName === name)
return;
selfChannelName = name;
sendMessage(`/join ${name}`);
}
},
};
};

View file

@ -24,15 +24,15 @@ Umi.UI.Channels = (function() {
return {
Add: function(channel) {
const id = 'channel-' + channel.getName().toLowerCase().replace(' ', '-'),
const id = 'channel-' + channel.name.toLowerCase().replace(' ', '-'),
cBase = $e({ attrs: { 'class': sidebarChannel, id: id } }),
cDetails = $e({ attrs: { 'class': sidebarChannel + '-details' } }),
cName = $e({ attrs: { 'class': sidebarChannel + '-name' } });
cBase.setAttribute('data-umi-channel', channel.getName());
cBase.setAttribute('data-umi-channel', channel.name);
cBase.setAttribute('onclick', 'Umi.UI.Channels.Switch(this.getAttribute(\'data-umi-channel\'))');
cName.appendChild($t(channel.getName()));
cName.appendChild($t(channel.name));
cDetails.appendChild(cName);
cBase.appendChild(cDetails);
@ -40,18 +40,18 @@ Umi.UI.Channels = (function() {
},
Update: function(id, channel) {
const cBase = $i('channel-' + id.toLowerCase().replace(' ', '-'));
cBase.id = channel.getName().toLowerCase().replace(' ', '-');
cBase.innerText = channel.getName();
cBase.id = channel.name.toLowerCase().replace(' ', '-');
cBase.innerText = channel.name;
},
Remove: function(channel) {
$ri('channel-' + channel.getName().toLowerCase().replace(' ', '-'));
$ri('channel-' + channel.name.toLowerCase().replace(' ', '-'));
},
RemoveAll: function() {
Umi.UI.Menus.Get('channels').innerHTML = '';
},
Reload: function(initial) {
const current = Umi.Channels.Current(),
channel = $i('channel-' + current.getName().toLowerCase().replace(' ', '-')),
channel = $i('channel-' + current.name.toLowerCase().replace(' ', '-')),
prev = $c(sidebarChannelCurrent)[0];
if((typeof prev).toLowerCase() !== 'undefined')
@ -63,7 +63,7 @@ Umi.UI.Channels = (function() {
return;
Umi.UI.Messages.RemoveAll();
const channelMsgs = Umi.Messages.All(current.getName());
const channelMsgs = Umi.Messages.All(current.name);
for(const channelMsg of channelMsgs)
Umi.UI.Messages.Add(channelMsg);
},

View file

@ -1,5 +1,4 @@
#include emotes.js
#include user.js
#include utility.js
#include ui/input-menus.js
#include ui/view.js
@ -10,7 +9,7 @@ Umi.UI.Emoticons = (function() {
const menu = Umi.UI.InputMenus.Get('emotes');
menu.innerHTML = '';
MamiEmotes.forEach(Umi.User.getCurrentUser().getRank(), function(emote) {
MamiEmotes.forEach(Umi.User.getCurrentUser().perms.rank, function(emote) {
menu.appendChild($e({
tag: 'button',
attrs: {
@ -39,7 +38,7 @@ Umi.UI.Emoticons = (function() {
let inner = element.innerHTML;
MamiEmotes.forEach(message.getUser().getRank(), function(emote) {
MamiEmotes.forEach(message.getUserV2().perms.rank, function(emote) {
const image = $e({
tag: 'img',
attrs: {

View file

@ -1,7 +1,6 @@
#include channels.js
#include user.js
#include users.js
#include utility.js
#include sound/umisound.js
#include ui/channels.js
#include ui/messages.jsx
#include ui/users.js
@ -78,7 +77,8 @@ Umi.UI.Hooks = (function() {
textField.value = '';
text = text.replace(/\t/g, ' ');
sendMessage(text);
if(text.length > 0)
sendMessage(text);
}
});
@ -117,14 +117,14 @@ Umi.UI.Hooks = (function() {
if(snippet.indexOf(':') === 0) {
let emoteRank = 0;
if(Umi.User.hasCurrentUser())
emoteRank = Umi.User.getCurrentUser().getRank();
emoteRank = Umi.User.getCurrentUser().perms.rank;
const emotes = MamiEmotes.findByName(emoteRank, snippet.substring(1), true);
if(emotes.length > 0)
insertText = ':' + emotes[0] + ':';
} else {
const users = Umi.Users.Find(snippet);
if(users.length === 1)
insertText = users[0].getName();
insertText = users[0].name;
}
if(insertText !== undefined) {

View file

@ -117,10 +117,11 @@ Umi.UI.Messages = (function() {
Add: function(msg) {
const currentChannel = Umi.Channels.Current();
const channelName = msg.getChannel();
const sender = msg.getUser();
const sender = msg.getUserV2();
const isBot = sender.id === '-1';
const isOutgoing = Umi.User.isCurrentUser(sender);
const hasSeen = msg.hasSeen();
const displayMessage = currentChannel === null || channelName === null || channelName === currentChannel.getName();
const displayMessage = currentChannel === null || channelName === null || channelName === currentChannel.name;
const notifyPM = !displayMessage && !isOutgoing && !hasSeen && channelName.startsWith('@');
let isTiny = false,
@ -136,28 +137,25 @@ Umi.UI.Messages = (function() {
let soundName = isOutgoing ? 'outgoing' : 'incoming',
soundVolume, soundRate, soundIsLegacy = true;
const userClass = `message--user-${sender.getId()}`;
const userClass = `message--user-${sender.id}`;
const classes = ['message', userClass];
const styles = {};
const avatarClasses = ['message__avatar'];
const msgIsFirst = forceUserInfo || lastMsgUser !== sender.getId() || lastMsgChannel !== msg.getChannel();
const msgIsFirst = forceUserInfo || lastMsgUser !== sender.id || lastMsgChannel !== msg.getChannel();
if(msgIsFirst) {
forceUserInfo = false;
classes.push('message--first');
}
if(msg.shouldHighlight())
classes.push('message--highlight');
if(msg.isAction()) {
isTiny = true;
classes.push('message-action');
}
if(sender.getId() === "136")
if(sender.id === "136")
styles.transform = 'scaleY(' + (0.76 + (0.01 * Math.max(0, Math.ceil(Date.now() / (7 * 24 * 60 * 60000)) - 2813))).toString() + ')';
const msgDateTimeObj = msg.getTime();
@ -165,7 +163,7 @@ Umi.UI.Messages = (function() {
+ ':' + msgDateTimeObj.getMinutes().toString().padStart(2, '0')
+ ':' + msgDateTimeObj.getSeconds().toString().padStart(2, '0');
if(sender.isBot()) {
if(isBot) {
const botInfo = msg.getBotInfo();
soundName = botInfo.isError ? 'error' : 'server';
@ -220,9 +218,9 @@ Umi.UI.Messages = (function() {
if(typeof avatarUrl !== 'string' || avatarUrl.length < 1)
avatarUrl = undefined;
else
avatarUrl = avatarUrl.replace('{user:id}', avatarUser.getId())
avatarUrl = avatarUrl.replace('{user:id}', avatarUser.id)
.replace('{resolution}', avatarSize)
.replace('{user:avatar_change}', avatarUser.getAvatarTime().toString());
.replace('{user:avatar_change}', avatarUser.avatarChangeTime);
if(displayMessage) {
if(isTiny) {
@ -239,7 +237,7 @@ Umi.UI.Messages = (function() {
{eAvatar = <div class={avatarClasses}/>}
<div class="message__container">
{eMeta = <div class="message__meta">
{eUser = <div class="message__user" style={{ color: avatarUser.getColour() }}>{avatarUser.getName()}</div>}
{eUser = <div class="message__user" style={{ color: avatarUser.colour }}>{avatarUser.name}</div>}
{eText = <div class="message-tiny-text"/>}
<div class="message__time">{msgDateTime}</div>
</div>}
@ -250,7 +248,7 @@ Umi.UI.Messages = (function() {
{eAvatar = <div class={avatarClasses}/>}
<div class="message__container">
{eMeta = <div class="message__meta">
{eUser = <div class="message__user" style={{ color: avatarUser.getColour() }}>{avatarUser.getName()}</div>}
{eUser = <div class="message__user" style={{ color: avatarUser.colour }}>{avatarUser.name}</div>}
<div class="message__time">{msgDateTime}</div>
</div>}
{eText = <div class="message__text"/>}
@ -309,7 +307,7 @@ Umi.UI.Messages = (function() {
const msgsList = $i('umi-messages');
msgsList.appendChild(eBase);
lastMsgUser = sender.getId();
lastMsgUser = sender.id;
lastMsgChannel = msg.getChannel();
if(mami.settings.get('autoEmbedV1')) {
@ -326,8 +324,8 @@ Umi.UI.Messages = (function() {
let isMentioned = false;
const mentionTriggers = (mami.settings.get('notificationTriggers') || '').toLowerCase().split(' ');
const currentUser = Umi.User.getCurrentUser();
if(typeof currentUser === 'object' && typeof currentUser.getName === 'function')
mentionTriggers.push(currentUser.getName().toLowerCase());
if(typeof currentUser === 'object' && typeof currentUser.name === 'string')
mentionTriggers.push(currentUser.name.toLowerCase());
const mentionText = ` ${msgTextLong} `.toLowerCase();
for(const trigger of mentionTriggers) {
@ -345,12 +343,12 @@ Umi.UI.Messages = (function() {
if(document.hidden) {
if(mami.settings.get('flashTitle')) {
let titleText = sender.isBot() && mami.settings.get('showServerMsgInTitle')
let titleText = isBot && mami.settings.get('showServerMsgInTitle')
? ` ${msgTextLong}`
: ` ${sender.getName()}`;
: ` ${sender.name}`;
// oops this won't work lol, we're filtering at the top
if(currentChannel !== null && currentChannel.getName() !== channelName)
if(currentChannel !== null && currentChannel.name !== channelName)
titleText += ` @ ${channelName}`;
title.strobe([
@ -372,7 +370,7 @@ Umi.UI.Messages = (function() {
if(avatarUrl !== undefined)
options.icon = avatarUrl;
const notif = new Notification(`${sender.getName()} mentioned you!`, options);
const notif = new Notification(`${sender.name} mentioned you!`, options);
notif.addEventListener('click', () => {
window.focus();
});
@ -392,12 +390,10 @@ Umi.UI.Messages = (function() {
}
if(eBase instanceof HTMLElement)
window.dispatchEvent(new CustomEvent('umi:ui:message_add', {
detail: {
element: eBase,
message: msg,
},
}));
mami.globalEvents.dispatch('umi:ui:message_add', {
element: eBase,
message: msg,
});
msg.markSeen();
},

View file

@ -1,7 +1,7 @@
#include animate.js
#include awaitable.js
#include common.js
#include emotes.js
#include sleep.js
#include utility.js
#include settings/backup.js
#include ui/baka.jsx
@ -181,7 +181,7 @@ Umi.UI.Settings = (function() {
},
{
name: 'onlySoundOnMention',
title: 'Only play receive sound on mention',
title: 'Do Not Play Any Sound Effects, Unless the Message Contains the Username of the User Controlling the Current Flashii Chat Session, Including but Not Limited To Joke Triggers, Receive Sounds and Join Sounds, but Excluding Typing Sounds',
type: 'checkbox',
},
{
@ -227,6 +227,12 @@ Umi.UI.Settings = (function() {
name: 'misc',
title: 'Misc',
items: [
{
name: 'onlyConnectWhenVisible',
title: 'Only connect when the tab is in the foreground',
type: 'checkbox',
confirm: 'Please only disable this setting if you are using a desktop or laptop computer, this should always remain on on a phone, tablet or other device of that sort. Ignoring this warning may carry consequences.',
},
{
name: 'playJokeSounds',
title: 'Run joke triggers',
@ -309,6 +315,28 @@ Umi.UI.Settings = (function() {
$r(meow);
},
},
{
title: 'Manual reconnect',
type: 'button',
invoke: async button => {
const textOrig = button.value;
let lock = 10;
button.disabled = true;
button.value = 'Reconnecting...';
try {
await mami.conMan.start();
while(--lock > 0) {
button.value = textOrig + ` (${lock}s)`;
await MamiSleep(1000);
}
} finally {
button.value = textOrig;
button.disabled = false;
}
},
},
],
},
{
@ -332,7 +360,7 @@ Umi.UI.Settings = (function() {
const user = Umi.User.getCurrentUser();
let fileName;
if(user !== null)
fileName = `${user.getName()}'s settings.mami`;
fileName = `${user.name}'s settings.mami`;
(new MamiSettingsBackup(mami.settings)).exportDownload(document.body, fileName);
},
@ -365,17 +393,6 @@ Umi.UI.Settings = (function() {
title: 'Dump events to console',
type: 'checkbox',
},
{
name: 'neverUseWorker',
title: 'Never use Worker for connection',
type: 'checkbox',
confirm: "If you're here it likely means that you mistakenly believe that your browser doesn't suck. You may go ahead but if disabling this causes any annoyances for other users you will be expunged.",
},
{
name: 'forceUseWorker',
title: 'Always use Worker for connection',
type: 'checkbox',
},
{
name: 'marqueeAllNames',
title: 'Apply marquee on everyone',

View file

@ -6,7 +6,7 @@ Umi.UI.Uploads = (function() {
create: function(fileName) {
const uploadHistory = Umi.UI.Menus.Get('uploads');
if(!uploadHistory) {
console.log('Upload history missing???');
console.error('Upload history missing???');
return;
}

View file

@ -1,6 +1,6 @@
#include animate.js
#include common.js
#include user.js
#include users.js
#include utility.js
#include ui/menus.js
#include ui/view.js
@ -9,10 +9,10 @@ Umi.UI.Users = (function() {
const toggleTimeouts = {};
const toggleUser = function(id) {
const prefix = 'user-' + id.toString(),
userOptions = $i(prefix + '-options-wrapper'),
closedClass = 'sidebar__user-options--hidden',
isClosed = userOptions.classList.contains(closedClass);
const prefix = 'user-' + id.toString();
const userOptions = $i(prefix + '-options-wrapper');
const closedClass = 'sidebar__user-options--hidden';
const isClosed = userOptions.classList.contains(closedClass);
let start, update, end, height;
if(prefix in toggleTimeouts) {
@ -72,22 +72,22 @@ Umi.UI.Users = (function() {
return {
Add: function(user) {
const id = 'user-' + user.getId(),
uBase = $e({ attrs: { 'class': 'sidebar__user', id: id } }),
uDetails = $e({ attrs: { 'class': 'sidebar__user-details', id: id + '-details' } }),
uAvatar = $e({ attrs: { 'class': 'sidebar__user-avatar', id: id + '-avatar' } }),
uName = $e({ attrs: { 'class': 'sidebar__user-name', id: id + '-name' } }),
uOptions = $e({ tag: 'ul', attrs: { 'class': 'sidebar__user-options', id: id + '-options' } });
const id = 'user-' + user.id;
const uBase = $e({ attrs: { 'class': 'sidebar__user', id: id } });
const uDetails = $e({ attrs: { 'class': 'sidebar__user-details', id: id + '-details' } });
const uAvatar = $e({ attrs: { 'class': 'sidebar__user-avatar', id: id + '-avatar' } });
const uName = $e({ attrs: { 'class': 'sidebar__user-name', id: id + '-name' } });
const uOptions = $e({ tag: 'ul', attrs: { 'class': 'sidebar__user-options', id: id + '-options' } });
uDetails.addEventListener('click', function() {
toggleUser(user.getId());
toggleUser(user.id);
}.bind(this));
const profileUrl = futami.get('profile');
if (profileUrl !== null || profileUrl.length > 1) {
uOptions.appendChild(createAction('View profile', {
'click': function() {
window.open(profileUrl.replace('{user:id}', user.getId()), '_blank');
window.open(profileUrl.replace('{user:id}', user.id), '_blank');
}
}));
}
@ -97,14 +97,14 @@ Umi.UI.Users = (function() {
Umi.UI.View.SetText('/me ');
}
}));
if (user.canSetNickName()) {
if (user.perms.canSetNick) {
uOptions.appendChild(createAction('Set nickname', {
'click': function() {
Umi.UI.View.SetText('/nick ');
}
}));
}
if (user.canBan()) {
if (user.perms.canKick) {
uOptions.appendChild(createAction('View bans', {
'click': function() {
Umi.UI.View.SetText('/bans');
@ -115,7 +115,7 @@ Umi.UI.Users = (function() {
if(confirm('You are about to detonate the fucking bomb. Are you sure?')) {
const targets = Umi.Users.All();
for(const target of targets) // this shouldn't call it like this but will have to leave it for now
Umi.Server.sendMessage('/kick ' + target.getName());
Umi.Server.sendMessage('/kick ' + target.name);
}
}
}));
@ -124,38 +124,29 @@ Umi.UI.Users = (function() {
else {
/*uOptions.appendChild(createAction('Send PM', {
'click': function() {
Umi.UI.View.SetText('/msg ' + user.getName() + ' ');
Umi.UI.View.SetText('/msg ' + user.name + ' ');
}
}));*/
if (Umi.User.getCurrentUser().canBan()) {
if (Umi.User.getCurrentUser().perms.canKick) {
uOptions.appendChild(createAction('Kick', {
'click': function() {
Umi.UI.View.SetText('/kick ' + user.getName());
Umi.UI.View.SetText('/kick ' + user.name);
}
}));
uOptions.appendChild(createAction('View IP', {
'click': function() {
Umi.UI.View.SetText('/ip ' + user.getName());
Umi.UI.View.SetText('/ip ' + user.name);
}
}));
}
}
uName.style.color = user.getColour();
uBase.style.backgroundColor = user.getColour() === 'inherit' ? '#fff' : user.getColour();
uName.style.color = user.colour;
uBase.style.backgroundColor = user.colour === 'inherit' ? '#fff' : user.colour;
let afkText = '',
sbUserName = user.getName();
if(sbUserName.indexOf('<') === 0) {
afkText = sbUserName.substring(1, sbUserName.indexOf('>'));
sbUserName = sbUserName.substring(afkText.length + 3);
}
if(user.status.isAway)
uName.appendChild($e({ attrs: { 'class': 'user-sidebar-afk' }, child: user.status.message }));
const isAFK = afkText.length > 0;
if(isAFK)
uName.appendChild($e({ attrs: { 'class': 'user-sidebar-afk' }, child: afkText }));
if(sbUserName.length > 16 || mami.settings.get('marqueeAllNames')) {
if(user.name.length > 16 || mami.settings.get('marqueeAllNames')) {
uName.appendChild($e({
tag: 'marquee',
attrs: {
@ -164,15 +155,15 @@ Umi.UI.Users = (function() {
overflow: 'hidden',
},
},
child: sbUserName,
child: user.name,
}));
} else {
uName.appendChild($t(sbUserName));
uName.appendChild($t(user.name));
}
const avatarUrl = futami.get('avatar');
if(avatarUrl !== null && avatarUrl.length > 1) {
uAvatar.style.backgroundImage = 'url({0})'.replace('{0}', avatarUrl.replace('{user:id}', user.getId()).replace('{resolution}', '80').replace('{user:avatar_change}', user.getAvatarTime().toString()));
uAvatar.style.backgroundImage = 'url({0})'.replace('{0}', avatarUrl.replace('{user:id}', user.id).replace('{resolution}', '80').replace('{user:avatar_change}', user.avatarChangeTime));
uDetails.appendChild(uAvatar);
}
@ -182,30 +173,21 @@ Umi.UI.Users = (function() {
Umi.UI.Menus.Get('users').appendChild(uBase);
},
Update: function(user) {
const uBase = $i('user-' + user.getId()),
uAvatar = $i('user-' + user.getId() + '-avatar'),
uName = $i('user-' + user.getId() + '-name');
uName.style.color = user.getColour();
uBase.style.backgroundColor = user.getColour() === 'inherit' ? '#fff' : user.getColour();
const uBase = $i('user-' + user.id);
const uAvatar = $i('user-' + user.id + '-avatar');
const uName = $i('user-' + user.id + '-name');
uName.style.color = user.colour;
uBase.style.backgroundColor = user.colour === 'inherit' ? '#fff' : user.colour;
uName.textContent = '';
let afkText = '',
sbUserName = user.getName();
const avatarUrl = futami.get('avatar');
if(avatarUrl !== null && avatarUrl.length > 1)
uAvatar.style.backgroundImage = 'url({0})'.replace('{0}', avatarUrl.replace('{user:id}', user.getId()).replace('{resolution}', '80').replace('{user:avatar_change}', user.getAvatarTime().toString()));
uAvatar.style.backgroundImage = 'url({0})'.replace('{0}', avatarUrl.replace('{user:id}', user.id).replace('{resolution}', '80').replace('{user:avatar_change}', user.avatarChangeTime));
if(sbUserName.indexOf('<') === 0) {
afkText = sbUserName.substring(1, sbUserName.indexOf('>'));
sbUserName = sbUserName.substring(afkText.length + 3);
}
if(user.status.isAway)
uName.appendChild($e({ attrs: { 'class': 'user-sidebar-afk' }, child: user.status.message }));
const isAFK = afkText.length > 0;
if(isAFK)
uName.appendChild($e({ attrs: { 'class': 'user-sidebar-afk' }, child: afkText }));
if(sbUserName.length > 16 || mami.settings.get('marqueeAllNames')) {
if(user.name.length > 16 || mami.settings.get('marqueeAllNames')) {
uName.appendChild($e({
tag: 'marquee',
attrs: {
@ -214,13 +196,13 @@ Umi.UI.Users = (function() {
overflow: 'hidden',
},
},
child: sbUserName,
child: user.name,
}));
} else
uName.appendChild($t(sbUserName));
uName.appendChild($t(user.name));
},
Remove: function(user) {
$ri('user-' + user.getId());
$ri('user-' + user.id);
},
RemoveAll: function() {
Umi.UI.Menus.Get('users').innerHTML = '';

View file

@ -1,4 +1,4 @@
const MamiRandomInt = function(min, max) {
const MamiRandomInt = (min, max) => {
let ret = 0;
const range = max - min;
@ -26,10 +26,10 @@ const MamiRandomInt = function(min, max) {
return min + ret;
};
const MamiUniqueStr = (function() {
const MamiUniqueStr = (() => {
const chars = 'AaBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQqRrSsTtUuVvWwXxYyZz0123456789';
return function(length) {
return length => {
let str = '';
for(let i = 0; i < length; ++i)
str += chars[MamiRandomInt(0, chars.length)];

View file

@ -1,66 +0,0 @@
Umi.User = function(userId, userName, userColour, userPerms) {
userId = (userId || '').toString();
userColour = (userColour || 'inherit').toString();
const userIdInt = parseInt(userId);
const setName = name => {
userName = (name || '').toString().replace('&lt;', '<').replace('&gt;', '>');
};
setName(userName);
const setColour = colour => {
userColour = (colour || 'inherit').toString();
};
setColour(userColour);
let userRank = 0;
let canBan = false;
let canSetNickName = false;
let canCreateChannel = false;
const setPerms = function(perms) {
perms = (perms || '').toString();
perms = perms.split(perms.includes("\f") ? "\f" : ' ');
userRank = parseInt(perms[0] || 0);
canBan = (perms[1] || '0') === '1';
canSetNickName = (perms[3] || '0') === '1';
canCreateChannel = (perms[4] || '0') === '1' || (perms[4] || '0') === '2';
};
setPerms(userPerms);
let avatarTime = Date.now();
return {
getId: () => userId,
getIdInt: () => userIdInt,
getName: () => userName,
setName: setName,
getColour: () => userColour,
setColour: setColour,
setPermissions: setPerms,
getRank: () => userRank,
isCurrentUser: () => Umi.User.currentUser && Umi.User.currentUser.userId === userId,
canBan: () => canBan,
canSilence: () => canBan,
canCreateChannel: () => canCreateChannel,
canSetNickName: () => canSetNickName,
getAvatarTime: () => avatarTime,
bumpAvatarTime: () => { avatarTime = Date.now(); },
isBot: () => userId === '-1',
};
};
Umi.User.currentUser = undefined;
Umi.User.hasCurrentUser = () => Umi.User.currentUser !== undefined;
Umi.User.getCurrentUser = () => Umi.User.currentUser;
Umi.User.setCurrentUser = user => { Umi.User.currentUser = user; };
Umi.User.isCurrentUser = user => user !== null && typeof user === 'object' && typeof user.getId === 'function'
&& Umi.User.currentUser !== null && typeof Umi.User.currentUser === 'object'
&& typeof Umi.User.currentUser.getId === 'function'
&& (Umi.User.currentUser === user || Umi.User.currentUser.getId() === user.getId());

View file

@ -1,10 +1,145 @@
const MamiUserPermsInfo = function(rank = 0, canKick = false, canSetNick = false, canCreateChannels = false) {
if(typeof rank !== 'number')
throw 'rank must be a number';
if(typeof canKick !== 'boolean')
throw 'canKick must be a boolean';
if(typeof canSetNick !== 'boolean')
throw 'canSetNick must be a boolean';
if(typeof canCreateChannels !== 'boolean')
throw 'canCreateChannels must be a boolean';
return {
get rank() { return rank; },
get canKick() { return canKick; },
get canSetNick() { return canSetNick; },
get canCreateChannels() { return canCreateChannels; },
};
};
const MamiUserStatusInfo = function(isAway = false, message = '') {
if(typeof isAway !== 'boolean')
throw 'isAway must be a boolean';
if(typeof message !== 'string')
throw 'message must be a string';
return {
get isAway() { return isAway; },
get message() { return message; },
};
};
const MamiUserInfo = function(id, name, colour = 'inherit', status = null, perms = null) {
if(typeof id !== 'string')
throw 'id must be a string';
if(typeof name !== 'string')
throw 'name must be a string';
if(typeof colour !== 'string') // should be like, object or something maybe
throw 'colour must be a string';
if(status === null)
status = new MamiUserStatusInfo;
else if(typeof status !== 'object')
throw 'status must be an object';
if(perms === null)
perms = new MamiUserPermsInfo;
else if(typeof perms !== 'object')
throw 'perms must be an object';
let avatarChangeTime = Date.now();
return {
get id() { return id; },
get name() { return name; },
set name(value) {
if(typeof value !== 'string')
throw 'value must be a string';
name = value;
},
get colour() { return colour; },
set colour(value) {
if(typeof value !== 'string') // ^
throw 'value must be a string';
colour = value;
},
get status() { return status; },
set status(value) {
if(typeof value !== 'object' || value === null)
throw 'value must be an object';
status = value;
},
get perms() { return perms; },
set perms(value) {
if(typeof value !== 'object' || value === null)
throw 'value must be an object';
perms = value;
},
get avatarChangeTime() { return avatarChangeTime; },
set avatarChangeTime(value) {
if(typeof value !== 'number')
throw 'value must be a number';
avatarChangeTime = value;
},
};
};
const MamiConvertUserInfoToUmi = info => {
return {
getId: () => info.id,
getIdInt: () => parseInt(info.id),
getName: () => {
let name = info.name;
if(info.status.isAway)
name = `<${info.status.message.substring(0, 5).toUpperCase()}>_${name}`;
return name;
},
setName: () => {},
getColour: () => info.colour,
setColour: () => {},
setPermissions: () => {},
getRank: () => info.perms.rank,
canBan: () => info.perms.canKick,
canSilence: () => false,
canCreateChannel: () => info.perms.canCreateChannels,
canSetNickName: () => info.perms.canSetNick,
getAvatarTime: () => info.avatarChangeTime,
bumpAvatarTime: () => {
info.avatarChangeTime = Date.now();
},
isBot: () => info.id === '-1',
};
};
Umi.User = (() => {
let userInfo;
return {
hasCurrentUser: () => userInfo !== undefined,
getCurrentUser: () => userInfo,
setCurrentUser: value => { userInfo = value; },
isCurrentUser: otherInfo => otherInfo !== null && typeof otherInfo === 'object' && typeof otherInfo.id === 'string'
&& userInfo !== null && typeof userInfo === 'object'
&& typeof userInfo.id === 'string'
&& (userInfo === otherInfo || userInfo.id === otherInfo.id),
};
})();
Umi.Users = (function() {
const users = new Map;
const onAdd = [],
onRemove = [],
onClear = [],
onUpdate = [];
const onAdd = [];
const onRemove = [];
const onClear = [];
const onUpdate = [];
return {
OnAdd: onAdd,
@ -12,7 +147,7 @@ Umi.Users = (function() {
OnClear: onClear,
OnUpdate: onUpdate,
Add: function(user) {
const userId = user.getId();
const userId = user.id;
if(!users.has(userId)) {
users.set(userId, user);
@ -21,7 +156,7 @@ Umi.Users = (function() {
}
},
Remove: function(user) {
const userId = user.getId();
const userId = user.id;
if(users.has(userId)) {
users.delete(userId);
@ -49,7 +184,7 @@ Umi.Users = (function() {
userName = userName.toLowerCase();
users.forEach(function(user) {
if(user.getName().toLowerCase().includes(userName))
if(user.name.toLowerCase().includes(userName))
found.push(user);
});
@ -59,16 +194,14 @@ Umi.Users = (function() {
userName = userName.toLowerCase();
for(const user of users.values())
if(user.getName().toLowerCase() === userName)
if(user.name.toLowerCase() === userName)
return user;
return null;
},
Update: function(userId, user) {
userId = userId.toString();
users.set(userId, user);
user.bumpAvatarTime();
for(const i in onUpdate)
onUpdate[i](userId, user);

View file

@ -1,99 +0,0 @@
#include eventtarget.js
const UmiWebSocket = function(url, useWorker) {
if(typeof useWorker !== 'boolean')
useWorker = (() => {
// Overrides
if(mami.settings.get('neverUseWorker'))
return false;
if(mami.settings.get('forceUseWorker'))
return true;
// Detect chromosomes
if((!!window.chrome || (!!window.Intl && !!Intl.v8BreakIterator)) && 'CSS' in window)
return true;
// Phones
if((/iphone|ipod|android|ie|blackberry|fennec/i).test(navigator.userAgent.toLowerCase()))
return true;
return false;
})();
const eventTarget = new MamiEventTarget('ws');
let send, close, sendInterval, clearIntervals;
if(useWorker) {
const worker = new Worker(MAMI_WS);
worker.addEventListener('message', ev => {
if(ev.data.act.startsWith('ws:'))
eventTarget.dispatch(ev.data.act.substring(3), ev.data.detail);
});
worker.postMessage({act: 'ws:open', url: url});
send = text => {
worker.postMessage({act: 'ws:send', text: text});
};
close = () => {
worker.postMessage({act: 'ws:close'});
};
sendInterval = (text, interval) => {
worker.postMessage({act: 'ws:send_interval', text: text, interval: interval});
};
clearIntervals = () => {
worker.postMessage({act: 'ws:clear_intervals'});
};
} else {
const websocket = new WebSocket(url), intervals = [];
websocket.addEventListener('open', ev => {
eventTarget.dispatch('open');
});
websocket.addEventListener('close', ev => {
eventTarget.dispatch('close', {
code: ev.code,
reason: ev.reason,
wasClean: ev.wasClean,
});
});
websocket.addEventListener('error', ev => {
eventTarget.dispatch('error');
});
websocket.addEventListener('message', ev => {
eventTarget.dispatch('message', {
data: ev.data,
origin: ev.origin,
lastEventId: ev.lastEventId,
});
});
send = text => {
websocket.send(text);
};
close = () => {
websocket.close();
};
sendInterval = (text, interval) => {
const intervalId = setInterval(() => {
if(websocket) {
websocket.send(text);
eventTarget.dispatch('call_interval', { id: intervalId });
}
}, interval);
intervals.push(intervalId);
eventTarget.dispatch('create_interval', { id: intervalId });
};
clearIntervals = () => {
for(let i = 0; i < intervals.length; ++i)
clearInterval(intervals[i]);
};
}
return {
isUsingWorker: useWorker,
send: send,
close: close,
sendInterval: sendInterval,
clearIntervals: clearIntervals,
watch: eventTarget.watch,
unwatch: eventTarget.unwatch,
};
};

View file

@ -36,19 +36,19 @@ const Weeaboo = (function() {
};
pub.getNameSuffix = function(user) {
if((typeof user).toLowerCase() !== 'object' || user === null)
if(typeof user !== 'object' || user === null)
return '';
if(user.getRank() >= 10)
if(user.perms.rank >= 10)
return '-sama';
if(user.getRank() >= 5)
if(user.perms.rank >= 5)
return '-sensei';
if(user.getColour().toLowerCase() === '#f02d7d')
if(user.colour.toLowerCase() === '#f02d7d')
return '-san';
if(user.getColour().toLowerCase() === '#0099ff')
if(user.colour.toLowerCase() === '#0099ff')
return '-wan';
switch(user.getIdInt() % 3) {
switch(parseInt(user.id) % 3) {
default:
return '-chan';
case 1:
@ -59,15 +59,15 @@ const Weeaboo = (function() {
};
pub.getTextSuffix = function(user) {
if((typeof user).toLowerCase() !== 'object' || user === null)
if(typeof user !== 'object' || user === null)
return '';
const userId = user.getId();
const userId = user.id;
if(userId === '3' || userId === '242')
return ' de geso';
if(!userSfx.has(userId)) {
const rng = new MamiRNG(0x51DEB00B | user.getIdInt());
const rng = new MamiRNG(0x51DEB00B | parseInt(user.id));
let str = ' ';
str += textSfx[rng.next() % textSfx.length];

281
src/mami.js/worker.js Normal file
View file

@ -0,0 +1,281 @@
#include uniqstr.js
const MamiWorker = function(url, eventTarget) {
const timeOutMs = 30000;
let worker, workerId;
let connectTimeout;
let pingId;
let hasTimedout;
const root = {};
const objects = new Map;
const pending = new Map;
const clearObjects = () => {
for(const [name, object] of objects)
for(const method in object)
delete object[method];
objects.clear();
objects.set('', root);
};
clearObjects();
const broadcastTimeoutZone = body => {
const localWorkerId = workerId;
body(detail => {
if(localWorkerId !== workerId || hasTimedout)
return;
hasTimedout = true;
eventTarget.dispatch(':timeout', detail);
});
};
const handlers = {};
const handleMessage = ev => {
if(typeof ev.data === 'object' && ev.data !== null && typeof ev.data.type === 'string') {
if(ev.data.type in handlers)
handlers[ev.data.type](ev.data.detail);
return;
}
};
const callObjectMethod = (objName, metName, ...args) => {
return new Promise((resolve, reject) => {
if(typeof objName !== 'string')
throw 'objName must be a string';
if(typeof metName !== 'string')
throw 'metName must be a string';
const id = MamiUniqueStr(8);
const info = { id: id, resolve: resolve, reject: reject };
pending.set(id, info);
worker.postMessage({ type: 'metcall', detail: { id: id, object: objName, method: metName, args: args } });
broadcastTimeoutZone(timeout => {
info.timeOut = setTimeout(() => {
const reject = info.reject;
info.resolve = info.reject = undefined;
info.timeOut = undefined;
pending.delete(id);
timeout({ at: 'call', obj: objName, met: metName });
if(typeof reject === 'function')
reject('timeout');
}, timeOutMs);
});
});
};
const defineObjectMethod = (object, objName, method) => object[method] = (...args) => callObjectMethod(objName, method, ...args);
handlers['objdef'] = info => {
let object = objects.get(info.object);
if(object === undefined)
objects.set(info.object, object = {});
if(typeof info.eventPrefix === 'string') {
const scopedTarget = eventTarget.scopeTo(info.eventPrefix);
object.watch = scopedTarget.watch;
object.unwatch = scopedTarget.unwatch;
}
for(const method of info.methods)
defineObjectMethod(object, info.object, method);
};
handlers['objdel'] = info => {
// this should never happen
if(info.object === '') {
console.error('Worker attempted to delete root object!!!!!');
return;
}
const object = objects.get(info.object);
if(object === undefined)
return;
objects.delete(info.object);
const methods = Object.keys(object);
for(const method of methods)
delete object[method];
};
handlers['metdef'] = info => {
const object = objects.get(info.object);
if(object === undefined) {
console.error('Worker attempted to define method on undefined object.');
return;
}
defineObjectMethod(object, info.object, info.method);
};
handlers['metdel'] = info => {
const object = objects.get(info.object);
if(object === undefined) {
console.error('Worker attempted to delete method on undefined object.');
return;
}
delete object[info.method];
};
handlers['funcret'] = resp => {
const info = pending.get(resp.id);
if(info === undefined)
return;
pending.delete(info.id);
if(info.timeOut !== undefined)
clearTimeout(info.timeOut);
const handler = resp.success ? info.resolve : info.reject;
info.resolve = info.reject = undefined;
if(handler !== undefined) {
let result = resp.result;
if(resp.object)
result = objects.get(result);
handler(result);
}
};
handlers['evtdisp'] = resp => {
eventTarget.dispatch(resp.name, resp.detail);
};
return {
get root() { return root; },
watch: eventTarget.watch,
unwatch: eventTarget.unwatch,
eventTarget: prefix => eventTarget.scopeTo(prefix),
ping: () => {
return new Promise((resolve, reject) => {
if(worker === undefined)
throw 'no worker active';
let pingTimeout;
let localPingId = pingId;
const pingHandleMessage = ev => {
if(typeof ev.data === 'string' && ev.data.startsWith('pong:') && ev.data.substring(5) === localPingId)
try {
reject = undefined;
pingId = undefined;
if(pingTimeout !== undefined)
clearTimeout(pingTimeout);
worker?.removeEventListener('message', pingHandleMessage);
if(typeof resolve === 'function')
resolve();
} finally {
resolve = undefined;
}
};
worker.addEventListener('message', pingHandleMessage);
if(localPingId === undefined) {
pingId = localPingId = MamiUniqueStr(8);
broadcastTimeoutZone(timeout => {
pingTimeout = setTimeout(() => {
try {
resolve = undefined;
worker?.removeEventListener('message', pingHandleMessage);
timeout({ at: 'ping' });
if(typeof reject === 'function')
reject('ping timeout');
} finally {
reject = undefined;
}
}, 200);
});
worker.postMessage(`ping:${localPingId}`);
}
});
},
sabotage: () => {
worker?.terminate();
},
connect: () => {
return new Promise((resolve, reject) => {
const connectFinally = () => {
if(connectTimeout !== undefined) {
clearTimeout(connectTimeout);
connectTimeout = undefined;
}
};
const connectHandleMessage = ev => {
worker?.removeEventListener('message', connectHandleMessage);
if(typeof ev.data !== 'object' || ev.data === null || ev.data.type !== 'objdef'
|| typeof ev.data.detail !== 'object' || ev.data.detail.object !== '') {
callReject('data');
} else
callResolve();
};
const callResolve = () => {
reject = undefined;
connectFinally();
try {
if(typeof resolve === 'function')
resolve(root);
} finally {
resolve = undefined;
}
};
const callReject = (...args) => {
resolve = undefined;
connectFinally();
try {
broadcastTimeoutZone(timeout => {
timeout({ at: 'connect' });
});
if(typeof reject === 'function')
reject(...args);
} finally {
reject = undefined;
}
};
if(worker !== undefined) {
worker.terminate();
workerId = worker = undefined;
}
hasTimedout = false;
workerId = MamiUniqueStr(5);
worker = new Worker(url);
worker.addEventListener('message', handleMessage);
worker.addEventListener('message', connectHandleMessage);
connectTimeout = setTimeout(() => callReject('timeout'), timeOutMs);
worker.postMessage({ type: 'init' });
});
},
};
};

19
src/proto.js/main.js Normal file
View file

@ -0,0 +1,19 @@
#include skel.js
#include sockchat/proto.js
const skel = new WorkerSkeleton;
skel.defineMethod('create', (name, options) => {
if(typeof name !== 'string')
throw 'name must be a string';
let proto, prefix;
if(name === 'sockchat')
proto = new SockChatProtocol(
skel.createDispatcher(prefix = 'sockchat'),
options
);
return skel.defineObject(proto, prefix);
}, true);

214
src/proto.js/skel.js Normal file
View file

@ -0,0 +1,214 @@
#include uniqstr.js
const WorkerSkeletonObject = function(name, defineObjectMethod, deleteObjectMethod, deleteObject) {
if(typeof name !== 'string')
throw 'name must be a string';
if(typeof defineObjectMethod !== 'function')
throw 'defineObjectMethod must be a function';
if(typeof deleteObjectMethod !== 'function')
throw 'deleteObjectMethod must be a function';
if(typeof deleteObject !== 'function')
throw 'deleteObject must be a function';
return {
getObjectName: () => name,
defineMethod: (...args) => defineObjectMethod(name, ...args),
deleteMethod: (...args) => deleteObjectMethod(name, ...args),
destroy: () => deleteObject(name),
};
};
const WorkerSkeleton = function(globalScope) {
if(globalScope === undefined)
globalScope = self;
const objects = new Map;
const handlers = {};
let initialised = false;
const sendPayload = (type, detail) => {
globalScope.postMessage({ type: type, detail: detail });
};
const sendObjectDefinePayload = (objName, objBody, eventPrefix) => {
sendPayload('objdef', { object: objName, methods: Object.keys(objBody), eventPrefix: eventPrefix });
};
const defineObject = (objName, objBody, eventPrefix) => {
if(typeof objName !== 'string')
throw 'objName must be a string';
if(typeof eventPrefix !== 'string' && eventPrefix !== undefined)
throw 'eventPrefix must be string or undefined';
if(objects.has(objName))
throw 'objName is already defined';
const object = {};
for(const name in objBody) {
const item = objBody[name];
if(typeof item === 'function')
object[name] = { body: item };
}
objects.set(objName, object);
if(initialised)
sendObjectDefinePayload(objName, object, eventPrefix);
};
const deleteObject = objName => {
if(typeof objName !== 'string')
throw 'objName must be a string';
if(!objects.has(objName))
throw 'objName is not defined';
objects.delete(objName);
if(initialised)
sendPayload('objdel', { object: objName });
};
const defineObjectMethod = (objName, metName, metBody, returnsObject) => {
if(typeof objName !== 'string')
throw 'objName must be a string';
if(typeof metName !== 'string')
throw 'metName must be a string';
if(typeof metBody !== 'function')
throw 'metBody must be a function';
const objBody = objects.get(objName);
if(objBody === undefined)
throw 'objName has not been defined';
objBody[metName] = { body: metBody, returnsObject: returnsObject === true };
if(initialised)
sendPayload('metdef', { object: objName, method: metName });
};
const deleteObjectMethod = (objName, metName) => {
if(typeof objName !== 'string')
throw 'objName must be a string';
if(typeof metName !== 'string')
throw 'metName must be a string';
const objBody = objects.get(objName);
if(objBody === undefined)
throw 'objName has not been defined';
delete objBody[objName];
if(initialised)
sendPayload('metdel', { object: objName, method: metName });
};
const createDispatcher = prefix => {
if(prefix === undefined)
prefix = '';
else if(typeof prefix !== 'string')
throw 'prefix must be a string or undefined';
if(prefix !== '' && !prefix.endsWith(':'))
prefix += ':';
return (name, detail) => sendPayload('evtdisp', { name: prefix + name, detail: detail });
};
defineObject('', {});
const defineHandler = (name, handler) => {
if(typeof name !== 'string')
throw 'name must be a string';
if(typeof handler !== 'function')
throw 'handler must be a function';
if(name in handlers)
throw 'name is already defined';
handlers[name] = handler;
};
defineHandler('init', () => {
if(initialised)
return;
initialised = true;
sendObjectDefinePayload('', objects.get(''));
});
defineHandler('metcall', req => {
if(typeof req.id !== 'string')
throw 'call id is not a string';
const respond = (id, success, result, mightBeObject) => {
let isObject = false;
if(mightBeObject) {
const resultType = typeof result;
if(resultType === 'string' && objects.has(result))
isObject = true;
else if(resultType === 'object' && result !== null && typeof result.getObjectName === 'function') {
const objectName = result.getObjectName();
if(objects.has(objectName)) {
isObject = true;
result = objectName;
}
}
}
sendPayload('funcret', {
id: id,
success: success,
result: result,
object: isObject,
});
};
try {
if(typeof req.object !== 'string')
throw 'object name is not a string';
if(typeof req.method !== 'string')
throw 'method name is not a string';
const object = objects.get(req.object);
if(object === undefined)
throw 'object is not defined';
if(!(req.method in object))
throw 'method is not defined in object';
const args = Array.isArray(req.args) ? req.args : [];
const info = object[req.method];
let result = info.body(...args);
if(result instanceof Promise) {
result.then(result => respond(req.id, true, result, info.returnsObject)).catch(ex => respond(req.id, false, ex));
} else
respond(req.id, true, result, info.returnsObject);
} catch(ex) {
respond(req.id, false, ex);
}
});
globalScope.addEventListener('message', ev => {
if(typeof ev.data === 'string') {
if(ev.data.startsWith('ping:')) {
globalScope.postMessage(`pong:${ev.data.substring(5)}`);
return;
}
}
if(typeof ev.data === 'object' && ev.data !== null && typeof ev.data.type === 'string') {
if(ev.data.type in handlers)
handlers[ev.data.type](ev.data.detail);
return;
}
});
return {
sendPayload: sendPayload,
createDispatcher: createDispatcher,
defineObject: (object, eventPrefix) => {
if(typeof object !== 'object' || object === null)
return undefined;
const name = MamiUniqueStr(8);
defineObject(name, object, eventPrefix);
return new WorkerSkeletonObject(name, defineObjectMethod, deleteObjectMethod, deleteObject);
},
defineMethod: (...args) => defineObjectMethod('', ...args),
deleteMethod: (...args) => deleteObjectMethod('', ...args),
};
};

View file

@ -0,0 +1,322 @@
#include sockchat/utils.js
const SockChatS2CPong = ctx => {
const lastPong = Date.now();
const lastPing = ctx.lastPing;
ctx.lastPing = undefined;
if(lastPing === undefined)
throw 'unexpected pong received??';
ctx.pingPromise?.resolve({
ping: lastPing,
pong: lastPong,
diff: lastPong - lastPing,
});
};
const SockChatS2CBanKick = (ctx, type, expiresTime) => {
ctx.wasKicked = true;
const bakaInfo = {
session: { success: false },
baka: {
type: type === '0' ? 'kick' : 'ban',
},
};
if(bakaInfo.baka.type === 'ban') {
bakaInfo.baka.perma = expiresTime === '-1';
bakaInfo.baka.until = expiresTime === '-1' ? undefined : new Date(parseInt(expiresTime) * 1000);
}
ctx.dispatch('session:term', bakaInfo);
};
const SockChatS2CContextClear = (ctx, mode) => {
if(mode === '0' || mode === '3' || mode === '4')
ctx.dispatch('msg:clear');
if(mode === '1' || mode === '3' || mode === '4')
ctx.dispatch('user:clear');
if(mode === '2' || mode === '4')
ctx.dispatch('chan:clear');
};
const SockChatS2CChannelPopulate = (ctx, count, ...args) => {
count = parseInt(count);
ctx.dispatch('chan:clear');
for(let i = 0; i < count; ++i) {
const offset = 3 * i;
ctx.dispatch('chan:add', {
channel: {
name: args[offset],
hasPassword: args[offset + 1] !== '0',
isTemporary: args[offset + 2] !== '0',
isCurrent: args[offset] === ctx.channelName,
},
});
}
ctx.dispatch('chan:focus', {
channel: { name: ctx.channelName },
});
};
const SockChatS2CChannelAdd = (ctx, name, hasPass, isTemp) => {
ctx.dispatch('chan:add', {
channel: {
name: name,
hasPassword: hasPass !== '0',
isTemporary: isTemp !== '0',
},
});
};
const SockChatS2CChannelUpdate = (ctx, prevName, name, hasPass, isTemp) => {
ctx.dispatch('chan:update', {
channel: {
previousName: prevName,
name: name,
hasPassword: hasPass !== '0',
isTemporary: isTemp !== '0',
},
});
};
const SockChatS2CChannelRemove = (ctx, name) => {
ctx.dispatch('chan:remove', {
channel: { name: name },
});
};
const SockChatS2CUserPopulate = (ctx, count, ...args) => {
count = parseInt(count);
ctx.dispatch('user:clear');
for(let i = 0; i < count; ++i) {
const offset = 5 * i;
const statusInfo = SockChatParseStatusInfo(args[offset + 1]);
ctx.dispatch('user:add', {
user: {
id: args[offset],
self: args[offset] === ctx.userId,
name: statusInfo.name,
status: statusInfo.status,
colour: SockChatParseUserColour(args[offset + 2]),
perms: SockChatParseUserPerms(args[offset + 3]),
hidden: args[offset + 4] !== '0',
},
});
}
};
const SockChatS2CUserAdd = (ctx, timeStamp, userId, userName, userColour, userPerms, msgId) => {
const statusInfo = SockChatParseStatusInfo(userName);
ctx.dispatch('user:add', {
msg: {
id: msgId,
time: new Date(parseInt(timeStamp) * 1000),
channel: ctx.channelName,
botInfo: {
type: 'join',
args: [statusInfo.name],
},
},
user: {
id: userId,
self: userId === ctx.userId,
name: statusInfo.name,
status: statusInfo.status,
colour: SockChatParseUserColour(userColour),
perms: SockChatParseUserPerms(userPerms),
},
});
};
const SockChatS2CUserUpdate = (ctx, userId, userName, userColour, userPerms) => {
const statusInfo = SockChatParseStatusInfo(userName);
ctx.dispatch('user:update', {
user: {
id: userId,
self: userId === ctx.userId,
name: statusInfo.name,
status: statusInfo.status,
colour: SockChatParseUserColour(userColour),
perms: SockChatParseUserPerms(userPerms),
},
});
};
const SockChatS2CUserRemove = (ctx, userId, userName, reason, timeStamp, msgId) => {
const statusInfo = SockChatParseStatusInfo(userName);
ctx.dispatch('user:remove', {
leave: { type: reason },
msg: {
id: msgId,
time: new Date(parseInt(timeStamp) * 1000),
channel: ctx.channelName,
botInfo: {
type: reason,
args: [statusInfo.name],
},
},
user: {
id: userId,
self: userId === ctx.userId,
name: statusInfo.name,
status: statusInfo.status,
},
});
};
const SockChatS2CUserChannelJoin = (ctx, userId, userName, userColour, userPerms, msgId) => {
const statusInfo = SockChatParseStatusInfo(userName);
ctx.dispatch('chan:join', {
user: {
id: userId,
self: userId === ctx.userId,
name: statusInfo.name,
status: statusInfo.status,
colour: SockChatParseUserColour(userColour),
perms: SockChatParseUserPerms(userPerms),
},
msg: {
id: msgId,
channel: ctx.channelName,
botInfo: {
type: 'jchan',
args: [statusInfo.name],
},
},
});
};
const SockChatS2CUserChannelLeave = (ctx, userId, msgId) => {
ctx.dispatch('chan:leave', {
user: {
id: userId,
self: userId === ctx.userId,
},
msg: {
id: msgId,
channel: ctx.channelName,
botInfo: {
type: 'lchan',
args: [userId],
},
},
});
};
const SockChatS2CUserChannelFocus = (ctx, name) => {
ctx.channelName = name;
ctx.dispatch('chan:focus', {
channel: { name: ctx.channelName },
});
};
const SockChatS2CMessagePopulate = (ctx, timeStamp, userId, userName, userColour, userPerms, msgText, msgId, msgNotify, msgFlags) => {
const mFlags = SockChatParseMsgFlags(msgFlags);
const statusInfo = SockChatParseStatusInfo(userName);
const info = {
msg: {
id: msgId,
time: new Date(parseInt(timeStamp) * 1000),
channel: ctx.channelName,
sender: {
id: userId,
self: userId === ctx.userId,
name: statusInfo.name,
status: statusInfo.status,
colour: SockChatParseUserColour(userColour),
perms: SockChatParseUserColour(userPerms),
},
isBot: userId === '-1',
silent: msgNotify === '0',
flags: mFlags,
flagsRaw: msgFlags,
text: SockChatUnfuckText(msgText, mFlags.isAction),
},
};
const msgIdFirst = info.msg.id.charCodeAt(0);
if(msgIdFirst < 48 || msgIdFirst > 57)
info.msg.id = (Math.round(Number.MIN_SAFE_INTEGER * Math.random())).toString();
if(info.msg.isBot) {
const botParts = msgText.split("\f");
info.msg.botInfo = {
isError: botParts[0] === '1',
type: botParts[1],
args: botParts.slice(2),
};
// i think this is more Inaccurate Behaviour on the server side
if(info.msg.botInfo.type === 'say')
info.msg.botInfo.args[0] = SockChatUnfuckText(info.msg.botInfo.args[0]);
}
ctx.dispatch('msg:add', info);
};
const SockChatS2CMessageAdd = (ctx, timeStamp, userId, msgText, msgId, msgFlags) => {
const mFlags = SockChatParseMsgFlags(msgFlags);
let mText = SockChatUnfuckText(msgText, mFlags.isAction);
let mChannelName = ctx.channelName;
if(msgFlags[4] !== '0') {
if(userId === ctx.userId) {
const mTextParts = mText.split(' ');
mChannelName = `@${mTextParts.shift()}`;
mText = mTextParts.join(' ');
} else {
mChannelName = `@~${userId}`;
}
}
const msgInfo = {
msg: {
id: msgId,
time: new Date(parseInt(timeStamp) * 1000),
channel: mChannelName,
sender: {
id: userId,
self: userId === ctx.userId,
},
flags: mFlags,
flagsRaw: msgFlags,
isBot: userId === '-1',
text: mText,
},
};
if(msgInfo.msg.isBot) {
const botParts = msgText.split("\f");
msgInfo.msg.botInfo = {
isError: botParts[0] === '1',
type: botParts[1],
args: botParts.slice(2),
};
}
ctx.dispatch('msg:add', msgInfo);
};
const SockChatS2CMessageRemove = (ctx, msgId) => {
ctx.dispatch('msg:remove', {
msg: {
id: msgId,
channel: ctx.channelName,
},
});
};

View file

@ -0,0 +1,43 @@
#include sockchat/keepalive.js
const SockChatContext = function(dispatch, sendPing, pingDelay) {
if(typeof dispatch !== 'function')
throw 'dispatch must be a function';
let userId;
const public = {
get userId() { return userId; },
set userId(value) { userId = value; },
get isAuthed() { return userId !== undefined; },
channelName: undefined,
pseudoChannelName: undefined,
wasConnected: false,
wasKicked: false,
isRestarting: false,
lastPing: undefined,
openPromise: undefined,
authPromise: undefined,
pingPromise: undefined,
dispatch: dispatch,
};
public.keepAlive = new SockChatKeepAlive(public, sendPing, pingDelay);
// what is this? C#?
public.dispose = () => {
public.keepAlive?.stop();
public.openPromise?.cancel();
public.authPromise?.cancel();
public.pingPromise?.cancel();
};
return public;
};

View file

@ -0,0 +1,55 @@
const SockChatKeepAlive = function(ctx, sendPing, delay) {
if(typeof ctx !== 'object')
throw 'ctx must be a function';
if(typeof sendPing !== 'function')
throw 'sendPing must be a function';
if(typeof delay !== 'number')
throw 'delay must be a number';
let timeout;
let failures = 0;
delay *= 1000;
const clear = () => {
if(timeout !== undefined) {
clearTimeout(timeout);
timeout = undefined;
}
};
const schedule = () => {
clear();
timeout = setTimeout(run, delay);
};
const run = () => {
clear();
if(!ctx.isAuthed) {
schedule();
return;
}
ctx.dispatch('ping:send');
sendPing()
.then(info => {
failures = 0;
ctx.dispatch('ping:recv', info);
})
.catch(error => {
++failures;
if(error === 'timeout')
ctx.dispatch('ping:long', { failures: failures });
})
.finally(() => { schedule(); });
};
return {
start: run,
stop: () => {
clear();
ctx.pingPromise?.cancel();
},
};
};

View file

@ -0,0 +1,248 @@
#include timedp.js
#include sockchat/authed.js
#include sockchat/ctx.js
#include sockchat/unauthed.js
const SockChatProtocol = function(dispatch, options) {
if(typeof dispatch !== 'function')
throw 'dispatch must be a function';
if(typeof options !== 'object' || options === null)
throw 'options must be an object';
if(typeof options.ping !== 'number')
throw 'options.ping must be a number';
let ctx, sock;
let dumpPackets = false;
const handlers = {
unauthed: {
'1': {
'y': SockChatS2CAuthSuccess,
'n': SockChatS2CAuthFail,
},
'7': {
// MOTD gets sent before auth success :D
'1': SockChatS2CMessagePopulate,
},
},
authed: {
'0': SockChatS2CPong,
'1': SockChatS2CUserAdd,
'2': SockChatS2CMessageAdd,
'3': SockChatS2CUserRemove,
'4': {
'0': SockChatS2CChannelAdd,
'1': SockChatS2CChannelUpdate,
'2': SockChatS2CChannelRemove,
},
'5': {
'0': SockChatS2CUserChannelJoin,
'1': SockChatS2CUserChannelLeave,
'2': SockChatS2CUserChannelFocus,
},
'6': SockChatS2CMessageRemove,
'7': {
'0': SockChatS2CUserPopulate,
'1': SockChatS2CMessagePopulate,
'2': SockChatS2CChannelPopulate,
},
'8': SockChatS2CContextClear,
'9': SockChatS2CBanKick,
'10': SockChatS2CUserUpdate,
},
};
const handleOpen = () => {
if(dumpPackets)
console.log('[sockchat:open]');
ctx.isRestarting = false;
ctx.keepAlive.start();
const detail = {
opitons: options,
wasConnected: ctx.wasConnected,
wasKicked: ctx.wasKicked,
};
dispatch('conn:open', detail);
// event (and maybe also resolve?) should be delayed until after capability negotiation
dispatch('conn:ready');
ctx.openPromise?.resolve(detail);
};
const handleClose = ev => {
if(dumpPackets)
console.log('[sockchat:close]', ev.code, ev.reason, ev.wasClean);
ctx.keepAlive.stop();
ctx.userId = undefined;
ctx.channelName = undefined;
ctx.pseudoChannelName = undefined;
if(ev.code === 1012)
ctx.isRestarting = true;
const detail = {
code: ev.code,
wasConnected: ctx.wasConnected,
isRestarting: ctx.isRestarting,
wasKicked: ctx.wasKicked,
};
dispatch('conn:lost', detail);
ctx.openPromise?.reject(detail);
};
const handleMessage = ev => {
const args = ev.data.split("\t");
if(dumpPackets)
console.log('[sockchat:incoming]', args);
let handler = handlers[ctx.isAuthed ? 'authed' : 'unauthed'];
for(;;) {
handler = handler[args.shift()];
if(handler === undefined)
break;
if(typeof handler === 'function') {
handler(ctx, ...args);
break;
}
}
};
const send = (...args) => {
if(args.length < 1)
throw 'you must specify at least one argument as an opcode';
if(ctx?.dumpPackets)
console.log('[sockchat:outgoing]', args);
sock?.send(args.join("\t"));
};
const sendPing = () => {
return new Promise((resolve, reject) => {
if(ctx === undefined)
throw 'no connection opened';
if(!ctx.isAuthed)
throw 'must be authenticated';
if(ctx.pingPromise !== undefined)
throw 'already sending a ping';
ctx.lastPing = Date.now();
ctx.pingPromise = new TimedPromise(resolve, reject, () => ctx.pingPromise = undefined, 2000);
send('0', ctx.userId);
});
};
const sendAuth = (...args) => {
return new Promise((resolve, reject) => {
if(ctx === undefined)
throw 'no connection opened';
if(ctx.isAuthed)
throw 'already authenticated';
if(ctx.authPromise !== undefined)
throw 'already authenticating';
// HttpClient in C# has its Moments, so lets give this way too long to do its thing
ctx.authPromise = new TimedPromise(resolve, reject, () => ctx.authPromise = undefined, 10000);
send('1', ...args);
});
};
const sendMessage = text => {
return new Promise((resolve, reject) => {
if(typeof text !== 'string')
throw 'text must be a string';
if(ctx === undefined)
throw 'no connection opened';
if(!ctx.isAuthed)
throw 'must be authenticated';
// there's actually a pretty big bug here lol
// any unsupported command is gonna fall through to the actual channel you're in
if(!text.startsWith('/') && ctx.pseudoChannelName !== undefined)
text = `/msg ${ctx.pseudoChannelName} ${text}`;
send('2', ctx.userId, text);
// server doesn't send a direct ACK and we can't tell what message
// which response messages is actually associated with this
// an ACK or request/response extension to the protocol will be required
if(typeof resolve === 'function')
resolve();
});
};
const createContext = () => {
ctx = new SockChatContext(dispatch, sendPing, options.ping);
};
const closeWebSocket = () => {
try {
const localSock = sock;
sock = undefined;
if(localSock !== undefined) {
localSock.removeEventListener('message', handleMessage);
localSock.removeEventListener('close', handleClose);
localSock.removeEventListener('open', handleOpen);
localSock.close();
}
} finally {
ctx?.dispose();
}
};
return {
setDumpPackets: state => {
dumpPackets = !!state;
},
open: url => {
return new Promise((resolve, reject) => {
if(typeof url !== 'string')
throw 'url must be a string';
if(ctx?.openPromise !== undefined)
throw 'already opening a connection';
closeWebSocket();
createContext();
ctx.openPromise = new TimedPromise(resolve, reject, () => ctx.openPromise = undefined, 5000);
sock = new WebSocket(url);
sock.addEventListener('open', handleOpen);
sock.addEventListener('close', handleClose);
sock.addEventListener('message', handleMessage);
});
},
close: () => { closeWebSocket(); },
sendPing: sendPing,
sendAuth: sendAuth,
sendMessage: sendMessage,
switchChannel: async info => {
if(!ctx.isAuthed)
return;
const name = info.name;
if(info.isUserChannel) {
selfPseudoChannelName = name.substring(1);
} else {
ctx.pseudoChannelName = undefined;
if(ctx.channelName === name)
return;
ctx.channelName = name;
await sendMessage(`/join ${name}`);
}
},
};
};

View file

@ -0,0 +1,54 @@
#include sockchat/utils.js
const SockChatS2CAuthSuccess = (ctx, userId, userName, userColour, userPerms, chanName, maxLength) => {
ctx.userId = userId;
ctx.channelName = chanName;
const statusInfo = SockChatParseStatusInfo(userName);
const info = {
wasConnected: ctx.wasConnected,
session: { success: true },
ctx: {
maxMsgLength: parseInt(maxLength),
},
user: {
id: ctx.userId,
self: true,
name: statusInfo.name,
status: statusInfo.status,
colour: SockChatParseUserColour(userColour),
perms: SockChatParseUserPerms(userPerms),
},
channel: {
name: ctx.channelName,
},
};
ctx.wasConnected = true;
ctx.keepAlive.start();
ctx.dispatch('session:start', info);
ctx.authPromise?.resolve(info);
};
const SockChatS2CAuthFail = (ctx, reason, expiresTime) => {
ctx.wasKicked = true;
const info = {
session: {
success: false,
needsAuth: reason === 'authfail',
outOfConnections: reason === 'sockfail',
},
};
if(expiresTime !== undefined)
info.baka = {
type: 'join',
perma: expiresTime === '-1',
until: expiresTime === '-1' ? undefined : new Date(parseInt(expiresTime) * 1000),
};
ctx.dispatch('session:fail', info);
ctx.authPromise?.reject(info);
};

View file

@ -0,0 +1,55 @@
const SockChatUnfuckText = (text, isAction) => {
// P7.1 doesn't wrap in <i>, likely a bug in SharpChat
// check if this is the case with the PHPChat impl
if(isAction && text.startsWith('<i>'))
text = text.slice(3, -4);
return text.replace(/ <br\/> /g, "\n")
.replace(/&lt;/g, '<')
.replace(/&gt;/g, '>');
};
const SockChatParseUserColour = str => {
// todo
return str;
};
const SockChatParseUserPerms = str => {
const parts = str.split(str.includes("\f") ? "\f" : ' ');
return {
rank: parseInt(parts[0] ?? '0'),
kick: parts[1] !== undefined && parts[1] !== '0',
nick: parts[3] !== undefined && parts[3] !== '0',
chan: parts[4] !== undefined && parts[4] !== '0',
};
};
const SockChatParseMsgFlags = str => {
return {
nameBold: str[0] !== '0',
nameItalics: str[1] !== '0',
nameUnderline: str[2] !== '0',
showColon: str[3] !== '0',
isPM: str[4] !== '0',
isAction: str[1] !== '0' && str[3] === '0',
};
};
const SockChatParseStatusInfo = name => {
const isAway = name.startsWith('&lt;');
let message;
if(isAway) {
const index = name.indexOf('&gt;_');
message = name.substring(4, index);
name = name.substring(index + 5);
}
return {
name: name,
status: {
isAway: isAway,
message: message,
},
};
};

48
src/proto.js/timedp.js Normal file
View file

@ -0,0 +1,48 @@
const TimedPromise = function(resolve, reject, always, timeoutMs) {
let timeout, resolved = false;
const cancelTimeout = () => {
if(timeout === undefined) {
clearTimeout(timeout);
timeout = undefined;
}
};
const doResolve = (...args) => {
if(resolved) return;
resolved = true;
cancelTimeout();
reject = undefined;
if(typeof resolve === 'function')
resolve(...args);
if(typeof always === 'function')
always();
};
const doReject = (...args) => {
if(resolved) return;
resolved = true;
cancelTimeout();
resolve = undefined;
if(typeof reject === 'function')
reject(...args);
if(typeof always === 'function')
always();
};
timeout = setTimeout(() => doReject('timeout'), timeoutMs);
return {
resolve: doResolve,
reject: doReject,
cancel: () => {
if(timeout === undefined)
return;
doReject('timeout');
},
};
};

38
src/proto.js/uniqstr.js Normal file
View file

@ -0,0 +1,38 @@
const MamiRandomInt = (min, max) => {
let ret = 0;
const range = max - min;
const bitsNeeded = Math.ceil(Math.log2(range));
if(bitsNeeded > 53)
return -1;
const bytesNeeded = Math.ceil(bitsNeeded / 8),
mask = Math.pow(2, bitsNeeded) - 1;
const bytes = new Uint8Array(bytesNeeded);
crypto.getRandomValues(bytes);
let p = (bytesNeeded - 1) * 8;
for(let i = 0; i < bytesNeeded; ++i) {
ret += bytes[i] * Math.pow(2, p);
p -= 8;
}
ret &= mask;
if(ret >= range)
return MamiRandomInt(min, max);
return min + ret;
};
const MamiUniqueStr = (() => {
const chars = 'AaBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQqRrSsTtUuVvWwXxYyZz0123456789';
return length => {
let str = '';
for(let i = 0; i < length; ++i)
str += chars[MamiRandomInt(0, chars.length)];
return str;
};
})();

View file

@ -1,77 +0,0 @@
let websocket, intervals = [];
addEventListener('message', function(ev) {
if(!ev || !ev.data || !ev.data.act)
return;
switch(ev.data.act) {
case 'ws:open':
if(!ev.data.url)
break;
websocket = new WebSocket(ev.data.url);
websocket.addEventListener('open', function(ev) {
postMessage({ act: 'ws:open' });
});
websocket.addEventListener('close', function(ev) {
postMessage({
act: 'ws:close',
detail: {
code: ev.code,
reason: ev.reason,
wasClean: ev.wasClean,
},
});
});
websocket.addEventListener('error', function(ev) {
postMessage({ act: 'ws:error' });
});
websocket.addEventListener('message', function(ev) {
postMessage({
act: 'ws:message',
detail: {
data: ev.data,
origin: ev.origin,
lastEventId: ev.lastEventId,
},
});
});
break;
case 'ws:close':
for(const interval of intervals)
clearInterval(interval);
if(!websocket)
break;
websocket.close();
websocket = null;
intervals = [];
break;
case 'ws:send':
if(!websocket)
break;
websocket.send(ev.data.text);
break;
case 'ws:send_interval':
(function(interval, text) {
const intervalId = setInterval(function() {
if(websocket) {
websocket.send(text);
postMessage({ act: 'ws:call_interval', detail: { id: intervalId } });
}
}, interval);
intervals.push(intervalId);
postMessage({ act: 'ws:create_interval', detail: { id: intervalId } });
})(ev.data.interval, ev.data.text);
break;
case 'ws:clear_intervals':
for(const interval of intervals)
clearInterval(interval);
intervals = [];
break;
}
});