From cf71bab92d40efbbace729b8d71565ea07d718fa Mon Sep 17 00:00:00 2001 From: flashwave Date: Wed, 17 Apr 2024 15:42:50 +0000 Subject: [PATCH] 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. --- build.js | 6 +- package-lock.json | 466 ++++++++++---------- package.json | 8 +- src/{mami-init.js => init.js}/main.js | 2 +- src/mami.html | 2 +- src/mami.js/awaitable.js | 19 + src/mami.js/channel.js | 19 - src/mami.js/channels.js | 50 ++- src/mami.js/conman.js | 109 +++-- src/mami.js/context.js | 96 +++- src/mami.js/events.js | 31 ++ src/mami.js/eventtarget.js | 22 - src/mami.js/main.js | 497 +++++---------------- src/mami.js/message.js | 37 -- src/mami.js/messages.js | 46 +- src/mami.js/settings/settings.js | 8 +- src/mami.js/sleep.js | 1 - src/mami.js/sockchat/client.js | 51 +++ src/mami.js/sockchat/handlers.js | 375 ++++++++++++++++ src/mami.js/sockchat_old.js | 613 -------------------------- src/mami.js/ui/channels.js | 16 +- src/mami.js/ui/emotes.js | 5 +- src/mami.js/ui/hooks.js | 10 +- src/mami.js/ui/messages.jsx | 48 +- src/mami.js/ui/settings.jsx | 45 +- src/mami.js/ui/uploads.js | 2 +- src/mami.js/ui/users.js | 98 ++-- src/mami.js/uniqstr.js | 6 +- src/mami.js/user.js | 66 --- src/mami.js/users.js | 153 ++++++- src/mami.js/websock.js | 99 ----- src/mami.js/weeb.js | 18 +- src/mami.js/worker.js | 281 ++++++++++++ src/proto.js/main.js | 19 + src/proto.js/skel.js | 214 +++++++++ src/proto.js/sockchat/authed.js | 322 ++++++++++++++ src/proto.js/sockchat/ctx.js | 43 ++ src/proto.js/sockchat/keepalive.js | 55 +++ src/proto.js/sockchat/proto.js | 248 +++++++++++ src/proto.js/sockchat/unauthed.js | 54 +++ src/proto.js/sockchat/utils.js | 55 +++ src/proto.js/timedp.js | 48 ++ src/proto.js/uniqstr.js | 38 ++ src/websock.js/main.js | 77 ---- 44 files changed, 2703 insertions(+), 1775 deletions(-) rename src/{mami-init.js => init.js}/main.js (97%) create mode 100644 src/mami.js/awaitable.js delete mode 100644 src/mami.js/channel.js create mode 100644 src/mami.js/events.js delete mode 100644 src/mami.js/eventtarget.js delete mode 100644 src/mami.js/message.js delete mode 100644 src/mami.js/sleep.js create mode 100644 src/mami.js/sockchat/client.js create mode 100644 src/mami.js/sockchat/handlers.js delete mode 100644 src/mami.js/sockchat_old.js delete mode 100644 src/mami.js/user.js delete mode 100644 src/mami.js/websock.js create mode 100644 src/mami.js/worker.js create mode 100644 src/proto.js/main.js create mode 100644 src/proto.js/skel.js create mode 100644 src/proto.js/sockchat/authed.js create mode 100644 src/proto.js/sockchat/ctx.js create mode 100644 src/proto.js/sockchat/keepalive.js create mode 100644 src/proto.js/sockchat/proto.js create mode 100644 src/proto.js/sockchat/unauthed.js create mode 100644 src/proto.js/sockchat/utils.js create mode 100644 src/proto.js/timedp.js create mode 100644 src/proto.js/uniqstr.js delete mode 100644 src/websock.js/main.js diff --git a/build.js b/build.js index 13c5eb6..a69998a 100644 --- a/build.js +++ b/build.js @@ -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' }, diff --git a/package-lock.json b/package-lock.json index 6556fbc..9458807 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", diff --git a/package.json b/package.json index 51fcd7b..2cac0fb 100644 --- a/package.json +++ b/package.json @@ -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" } } diff --git a/src/mami-init.js/main.js b/src/init.js/main.js similarity index 97% rename from src/mami-init.js/main.js rename to src/init.js/main.js index 2b2f719..67ca06e 100644 --- a/src/mami-init.js/main.js +++ b/src/init.js/main.js @@ -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); diff --git a/src/mami.html b/src/mami.html index 99d8b07..a31a1c8 100644 --- a/src/mami.html +++ b/src/mami.html @@ -28,6 +28,6 @@ - + diff --git a/src/mami.js/awaitable.js b/src/mami.js/awaitable.js new file mode 100644 index 0000000..0e60877 --- /dev/null +++ b/src/mami.js/awaitable.js @@ -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); + }); +}; diff --git a/src/mami.js/channel.js b/src/mami.js/channel.js deleted file mode 100644 index 7670789..0000000 --- a/src/mami.js/channel.js +++ /dev/null @@ -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; }, - }; -}; diff --git a/src/mami.js/channels.js b/src/mami.js/channels.js index 6a856ad..f3c7056 100644 --- a/src/mami.js/channels.js +++ b/src/mami.js/channels.js @@ -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); diff --git a/src/mami.js/conman.js b/src/mami.js/conman.js index c9d2249..b448fd9 100644 --- a/src/mami.js/conman.js +++ b/src/mami.js/conman.js @@ -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(); diff --git a/src/mami.js/context.js b/src/mami.js/context.js index 8b2eb82..f90b534 100644 --- a/src/mami.js/context.js +++ b/src/mami.js/context.js @@ -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; + }, + }; }; diff --git a/src/mami.js/events.js b/src/mami.js/events.js new file mode 100644 index 0000000..f4bcdff --- /dev/null +++ b/src/mami.js/events.js @@ -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; +}; diff --git a/src/mami.js/eventtarget.js b/src/mami.js/eventtarget.js deleted file mode 100644 index b90bac3..0000000 --- a/src/mami.js/eventtarget.js +++ /dev/null @@ -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)), - }; -}; diff --git a/src/mami.js/main.js b/src/mami.js/main.js index 3068ae5..e805c0d 100644 --- a/src/mami.js/main.js +++ b/src/mami.js/main.js @@ -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 += ' Retry now'; - - 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...
Retry now`); + }; + 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(); })(); diff --git a/src/mami.js/message.js b/src/mami.js/message.js deleted file mode 100644 index 0288956..0000000 --- a/src/mami.js/message.js +++ /dev/null @@ -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, - }; - }; -})(); diff --git a/src/mami.js/messages.js b/src/mami.js/messages.js index 5c1a05a..f3e787c 100644 --- a/src/mami.js/messages.js +++ b/src/mami.js/messages.js @@ -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) { diff --git a/src/mami.js/settings/settings.js b/src/mami.js/settings/settings.js index f75c687..fec5b97 100644 --- a/src/mami.js/settings/settings.js +++ b/src/mami.js/settings/settings.js @@ -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); }; diff --git a/src/mami.js/sleep.js b/src/mami.js/sleep.js deleted file mode 100644 index 4122f14..0000000 --- a/src/mami.js/sleep.js +++ /dev/null @@ -1 +0,0 @@ -const MamiSleep = durationMs => new Promise(resolve => { setTimeout(resolve, durationMs); }); diff --git a/src/mami.js/sockchat/client.js b/src/mami.js/sockchat/client.js new file mode 100644 index 0000000..d7f6c0e --- /dev/null +++ b/src/mami.js/sockchat/client.js @@ -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); + }, + }; +}; diff --git a/src/mami.js/sockchat/handlers.js b/src/mami.js/sockchat/handlers.js new file mode 100644 index 0000000..9f5cdae --- /dev/null +++ b/src/mami.js/sockchat/handlers.js @@ -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]); + }, + }; +}; diff --git a/src/mami.js/sockchat_old.js b/src/mami.js/sockchat_old.js deleted file mode 100644 index 6dbc219..0000000 --- a/src/mami.js/sockchat_old.js +++ /dev/null @@ -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 , likely a bug in SharpChat - // check if this is the case with the PHPChat impl - if(isAction && text.startsWith('')) - text = text.slice(3, -4); - - return text.replace(/ /g, "\n") - .replace(/</g, '<') - .replace(/>/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}`); - } - }, - }; -}; diff --git a/src/mami.js/ui/channels.js b/src/mami.js/ui/channels.js index acfc1fa..b342135 100644 --- a/src/mami.js/ui/channels.js +++ b/src/mami.js/ui/channels.js @@ -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); }, diff --git a/src/mami.js/ui/emotes.js b/src/mami.js/ui/emotes.js index d7dfba0..d095df1 100644 --- a/src/mami.js/ui/emotes.js +++ b/src/mami.js/ui/emotes.js @@ -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: { diff --git a/src/mami.js/ui/hooks.js b/src/mami.js/ui/hooks.js index c068e4f..2b7b740 100644 --- a/src/mami.js/ui/hooks.js +++ b/src/mami.js/ui/hooks.js @@ -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) { diff --git a/src/mami.js/ui/messages.jsx b/src/mami.js/ui/messages.jsx index 5aa04dd..dc26d9b 100644 --- a/src/mami.js/ui/messages.jsx +++ b/src/mami.js/ui/messages.jsx @@ -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 =
}
{eMeta =
- {eUser =
{avatarUser.getName()}
} + {eUser =
{avatarUser.name}
} {eText =
}
{msgDateTime}
} @@ -250,7 +248,7 @@ Umi.UI.Messages = (function() { {eAvatar =
}
{eMeta =
- {eUser =
{avatarUser.getName()}
} + {eUser =
{avatarUser.name}
}
{msgDateTime}
} {eText =
} @@ -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(); }, diff --git a/src/mami.js/ui/settings.jsx b/src/mami.js/ui/settings.jsx index 76cfda7..4a87327 100644 --- a/src/mami.js/ui/settings.jsx +++ b/src/mami.js/ui/settings.jsx @@ -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', diff --git a/src/mami.js/ui/uploads.js b/src/mami.js/ui/uploads.js index e78c6ca..672dfff 100644 --- a/src/mami.js/ui/uploads.js +++ b/src/mami.js/ui/uploads.js @@ -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; } diff --git a/src/mami.js/ui/users.js b/src/mami.js/ui/users.js index 86491dd..b39df3c 100644 --- a/src/mami.js/ui/users.js +++ b/src/mami.js/ui/users.js @@ -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 = ''; diff --git a/src/mami.js/uniqstr.js b/src/mami.js/uniqstr.js index 3efc8a0..72a1111 100644 --- a/src/mami.js/uniqstr.js +++ b/src/mami.js/uniqstr.js @@ -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)]; diff --git a/src/mami.js/user.js b/src/mami.js/user.js deleted file mode 100644 index 7e912c3..0000000 --- a/src/mami.js/user.js +++ /dev/null @@ -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('<', '<').replace('>', '>'); - }; - 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()); diff --git a/src/mami.js/users.js b/src/mami.js/users.js index e002ae9..b743f41 100644 --- a/src/mami.js/users.js +++ b/src/mami.js/users.js @@ -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); diff --git a/src/mami.js/websock.js b/src/mami.js/websock.js deleted file mode 100644 index b219685..0000000 --- a/src/mami.js/websock.js +++ /dev/null @@ -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, - }; -}; diff --git a/src/mami.js/weeb.js b/src/mami.js/weeb.js index 1368fea..5a620a2 100644 --- a/src/mami.js/weeb.js +++ b/src/mami.js/weeb.js @@ -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]; diff --git a/src/mami.js/worker.js b/src/mami.js/worker.js new file mode 100644 index 0000000..9474904 --- /dev/null +++ b/src/mami.js/worker.js @@ -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' }); + }); + }, + }; +}; diff --git a/src/proto.js/main.js b/src/proto.js/main.js new file mode 100644 index 0000000..344e684 --- /dev/null +++ b/src/proto.js/main.js @@ -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); diff --git a/src/proto.js/skel.js b/src/proto.js/skel.js new file mode 100644 index 0000000..f97f776 --- /dev/null +++ b/src/proto.js/skel.js @@ -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), + }; +}; diff --git a/src/proto.js/sockchat/authed.js b/src/proto.js/sockchat/authed.js new file mode 100644 index 0000000..8e4004c --- /dev/null +++ b/src/proto.js/sockchat/authed.js @@ -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, + }, + }); +}; diff --git a/src/proto.js/sockchat/ctx.js b/src/proto.js/sockchat/ctx.js new file mode 100644 index 0000000..dc4f851 --- /dev/null +++ b/src/proto.js/sockchat/ctx.js @@ -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; +}; diff --git a/src/proto.js/sockchat/keepalive.js b/src/proto.js/sockchat/keepalive.js new file mode 100644 index 0000000..302fc2d --- /dev/null +++ b/src/proto.js/sockchat/keepalive.js @@ -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(); + }, + }; +}; diff --git a/src/proto.js/sockchat/proto.js b/src/proto.js/sockchat/proto.js new file mode 100644 index 0000000..00424ac --- /dev/null +++ b/src/proto.js/sockchat/proto.js @@ -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}`); + } + }, + }; +}; diff --git a/src/proto.js/sockchat/unauthed.js b/src/proto.js/sockchat/unauthed.js new file mode 100644 index 0000000..684d6ae --- /dev/null +++ b/src/proto.js/sockchat/unauthed.js @@ -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); +}; diff --git a/src/proto.js/sockchat/utils.js b/src/proto.js/sockchat/utils.js new file mode 100644 index 0000000..8aa7fe2 --- /dev/null +++ b/src/proto.js/sockchat/utils.js @@ -0,0 +1,55 @@ +const SockChatUnfuckText = (text, isAction) => { + // P7.1 doesn't wrap in , likely a bug in SharpChat + // check if this is the case with the PHPChat impl + if(isAction && text.startsWith('')) + text = text.slice(3, -4); + + return text.replace(/ /g, "\n") + .replace(/</g, '<') + .replace(/>/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('<'); + let message; + + if(isAway) { + const index = name.indexOf('>_'); + message = name.substring(4, index); + name = name.substring(index + 5); + } + + return { + name: name, + status: { + isAway: isAway, + message: message, + }, + }; +}; diff --git a/src/proto.js/timedp.js b/src/proto.js/timedp.js new file mode 100644 index 0000000..d871dc6 --- /dev/null +++ b/src/proto.js/timedp.js @@ -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'); + }, + }; +}; diff --git a/src/proto.js/uniqstr.js b/src/proto.js/uniqstr.js new file mode 100644 index 0000000..72a1111 --- /dev/null +++ b/src/proto.js/uniqstr.js @@ -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; + }; +})(); diff --git a/src/websock.js/main.js b/src/websock.js/main.js deleted file mode 100644 index e7134df..0000000 --- a/src/websock.js/main.js +++ /dev/null @@ -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; - } -});