serverless-ircd 0.5.0 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/ci.yml +40 -3
- package/.github/workflows/deploy-aws.yml +1 -2
- package/.github/workflows/deploy-cf-tcp.yml +1 -5
- package/.github/workflows/deploy-cf.yml +1 -2
- package/.gitmodules +3 -0
- package/CHANGELOG.md +474 -0
- package/README.md +219 -22
- package/apps/aws-stack/README.md +33 -0
- package/apps/aws-stack/package.json +1 -1
- package/apps/aws-stack/src/aws-stack.ts +74 -9
- package/apps/aws-stack/tests/coverage-command.test.ts +155 -0
- package/apps/aws-stack/tests/stack.test.ts +2 -2
- package/apps/aws-stack/tests/synth-no-bundle.test.ts +129 -0
- package/apps/aws-stack/tests/vitest.setup.ts +18 -0
- package/apps/aws-stack/vitest.config.ts +3 -0
- package/apps/cf-tcp-container/package.json +1 -1
- package/apps/cf-tcp-container/src/config-loader.ts +6 -7
- package/apps/cf-tcp-container/src/container-server.ts +3 -0
- package/apps/cf-tcp-container/tests/config-loader.test.ts +10 -3
- package/apps/cf-tcp-container/wrangler.toml +1 -1
- package/apps/cf-worker/package.json +2 -1
- package/apps/cf-worker/src/origin-allowlist.ts +99 -0
- package/apps/cf-worker/src/worker.ts +89 -9
- package/apps/cf-worker/tests/fixtures/web-dist/app/index.html +18 -0
- package/apps/cf-worker/tests/fixtures/web-dist/index.html +18 -0
- package/apps/cf-worker/tests/origin-allowlist.test.ts +101 -0
- package/apps/cf-worker/tests/smoke.test.ts +28 -2
- package/apps/cf-worker/tests/ws-origin.test.ts +89 -0
- package/apps/cf-worker/vitest.config.ts +1 -1
- package/apps/cf-worker/wrangler.test.toml +23 -0
- package/apps/cf-worker/wrangler.toml +59 -3
- package/apps/local-cli/package.json +1 -1
- package/apps/local-cli/src/config-loader.ts +6 -6
- package/apps/local-cli/src/main.ts +20 -0
- package/apps/local-cli/src/server.ts +9 -7
- package/apps/local-cli/tests/e2e.test.ts +112 -28
- package/apps/local-cli/tests/tcp.test.ts +71 -19
- package/apps/local-cli/tests/ws-subprotocol.test.ts +12 -7
- package/apps/web/landing/index.html +217 -0
- package/apps/web/package.json +28 -0
- package/apps/web/scripts/build.mjs +125 -0
- package/apps/web/src/build-env.ts +43 -0
- package/apps/web/src/config-schema.ts +138 -0
- package/apps/web/static/config.json +28 -0
- package/apps/web/static/config.prod.json +28 -0
- package/apps/web/static/config.staging.json +28 -0
- package/apps/web/tests/build-env.test.ts +63 -0
- package/apps/web/tests/build-smoke.test.ts +103 -0
- package/apps/web/tests/config-schema.test.ts +432 -0
- package/apps/web/tests/workspace.test.ts +12 -0
- package/apps/web/tsconfig.json +10 -0
- package/apps/web/upstream/.browserslistrc +3 -0
- package/apps/web/upstream/.editorconfig +9 -0
- package/apps/web/upstream/.eslintignore +3 -0
- package/apps/web/upstream/.eslintrc.js +90 -0
- package/apps/web/upstream/.github/FUNDING.yml +12 -0
- package/apps/web/upstream/.github/ISSUE_TEMPLATE/bug_report.md +38 -0
- package/apps/web/upstream/.github/ISSUE_TEMPLATE/feature_request.md +20 -0
- package/apps/web/upstream/.github/workflows/push.yml +26 -0
- package/apps/web/upstream/.github/workflows/release.yml +33 -0
- package/apps/web/upstream/.github/workflows/release_manual.yml +55 -0
- package/apps/web/upstream/.prettierrc.js +9 -0
- package/apps/web/upstream/.stylelintignore +5 -0
- package/apps/web/upstream/.stylelintrc.js +21 -0
- package/apps/web/upstream/LICENSE +201 -0
- package/apps/web/upstream/README.md +76 -0
- package/apps/web/upstream/babel.config.js +25 -0
- package/apps/web/upstream/crowdin.yml +3 -0
- package/apps/web/upstream/index.html +35 -0
- package/apps/web/upstream/jest.config.js +7 -0
- package/apps/web/upstream/jsdoc.json +22 -0
- package/apps/web/upstream/package.json +80 -0
- package/apps/web/upstream/scripts/pre-commit +43 -0
- package/apps/web/upstream/src/components/App.vue +516 -0
- package/apps/web/upstream/src/components/AppSettings.vue +587 -0
- package/apps/web/upstream/src/components/AutoComplete.vue +284 -0
- package/apps/web/upstream/src/components/Avatar.vue +105 -0
- package/apps/web/upstream/src/components/AwayStatusIndicator.vue +77 -0
- package/apps/web/upstream/src/components/BufferKey.vue +121 -0
- package/apps/web/upstream/src/components/BufferSettings.vue +97 -0
- package/apps/web/upstream/src/components/Captcha.vue +85 -0
- package/apps/web/upstream/src/components/ChannelBanlist.vue +96 -0
- package/apps/web/upstream/src/components/ChannelInfo.vue +159 -0
- package/apps/web/upstream/src/components/ChannelInvitelist.vue +276 -0
- package/apps/web/upstream/src/components/ChannelList.vue +345 -0
- package/apps/web/upstream/src/components/Container.vue +401 -0
- package/apps/web/upstream/src/components/ContainerHeader.vue +522 -0
- package/apps/web/upstream/src/components/ControlInput.vue +1016 -0
- package/apps/web/upstream/src/components/LoadingAnimation.vue +104 -0
- package/apps/web/upstream/src/components/MediaViewer.vue +199 -0
- package/apps/web/upstream/src/components/MessageInfo.vue +192 -0
- package/apps/web/upstream/src/components/MessageList.vue +1028 -0
- package/apps/web/upstream/src/components/MessageListAvatar.vue +20 -0
- package/apps/web/upstream/src/components/MessageListMessageCompact.vue +361 -0
- package/apps/web/upstream/src/components/MessageListMessageInline.vue +257 -0
- package/apps/web/upstream/src/components/MessageListMessageModern.vue +430 -0
- package/apps/web/upstream/src/components/NetworkSettings.vue +528 -0
- package/apps/web/upstream/src/components/Nicklist.vue +309 -0
- package/apps/web/upstream/src/components/NicklistUser.vue +181 -0
- package/apps/web/upstream/src/components/NotConnected.vue +271 -0
- package/apps/web/upstream/src/components/SelfUser.vue +283 -0
- package/apps/web/upstream/src/components/ServerSelector.vue +162 -0
- package/apps/web/upstream/src/components/ServerView.vue +126 -0
- package/apps/web/upstream/src/components/SettingsAdvanced.vue +232 -0
- package/apps/web/upstream/src/components/SettingsAliases.vue +121 -0
- package/apps/web/upstream/src/components/Sidebar.vue +429 -0
- package/apps/web/upstream/src/components/SidebarAboutBuffer.vue +288 -0
- package/apps/web/upstream/src/components/SidebarState.vue +145 -0
- package/apps/web/upstream/src/components/StartupError.vue +35 -0
- package/apps/web/upstream/src/components/StateBrowser.vue +420 -0
- package/apps/web/upstream/src/components/StateBrowserBuffer.vue +124 -0
- package/apps/web/upstream/src/components/StateBrowserNetwork.vue +823 -0
- package/apps/web/upstream/src/components/StateBrowserUsermenu.vue +162 -0
- package/apps/web/upstream/src/components/TypingStatusIndicator.vue +34 -0
- package/apps/web/upstream/src/components/TypingUsersList.vue +56 -0
- package/apps/web/upstream/src/components/UrlEmbed.vue +149 -0
- package/apps/web/upstream/src/components/UserBox.vue +728 -0
- package/apps/web/upstream/src/components/inputtools/Emoji.vue +67 -0
- package/apps/web/upstream/src/components/inputtools/TextStyle.vue +142 -0
- package/apps/web/upstream/src/components/startups/CommonLayout.vue +208 -0
- package/apps/web/upstream/src/components/startups/CustomServer.vue +495 -0
- package/apps/web/upstream/src/components/startups/KiwiBnc.vue +361 -0
- package/apps/web/upstream/src/components/startups/Personal.vue +317 -0
- package/apps/web/upstream/src/components/startups/Welcome.vue +544 -0
- package/apps/web/upstream/src/components/startups/ZncLogin.vue +229 -0
- package/apps/web/upstream/src/components/utils/InputConfirm.vue +68 -0
- package/apps/web/upstream/src/components/utils/InputPrompt.vue +118 -0
- package/apps/web/upstream/src/components/utils/InputText.vue +172 -0
- package/apps/web/upstream/src/components/utils/IrcInput.vue +579 -0
- package/apps/web/upstream/src/components/utils/PluginWrapper.vue +26 -0
- package/apps/web/upstream/src/components/utils/TabbedView.vue +149 -0
- package/apps/web/upstream/src/components/utils/TransitionExpand.vue +97 -0
- package/apps/web/upstream/src/helpers/Colours.js +128 -0
- package/apps/web/upstream/src/helpers/IrcdDiffs.js +26 -0
- package/apps/web/upstream/src/helpers/Md5.js +193 -0
- package/apps/web/upstream/src/helpers/Misc.js +401 -0
- package/apps/web/upstream/src/helpers/TextFormatting.js +237 -0
- package/apps/web/upstream/src/libs/AliasRewriter.js +157 -0
- package/apps/web/upstream/src/libs/AudioManager.js +60 -0
- package/apps/web/upstream/src/libs/BouncerMiddleware.js +247 -0
- package/apps/web/upstream/src/libs/BouncerProvider.js +551 -0
- package/apps/web/upstream/src/libs/ChathistoryMiddleware.js +153 -0
- package/apps/web/upstream/src/libs/ConfigLoader.js +94 -0
- package/apps/web/upstream/src/libs/EmojiProvider.js +46 -0
- package/apps/web/upstream/src/libs/GlobalApi.js +346 -0
- package/apps/web/upstream/src/libs/IPC.js +51 -0
- package/apps/web/upstream/src/libs/InputHandler.js +921 -0
- package/apps/web/upstream/src/libs/IrcClient.js +1506 -0
- package/apps/web/upstream/src/libs/Logger.js +71 -0
- package/apps/web/upstream/src/libs/Message.js +169 -0
- package/apps/web/upstream/src/libs/MessageFormatter.js +364 -0
- package/apps/web/upstream/src/libs/MessageParser.js +249 -0
- package/apps/web/upstream/src/libs/Notifications.js +78 -0
- package/apps/web/upstream/src/libs/ServerConnection.js +270 -0
- package/apps/web/upstream/src/libs/ServerSession.js +102 -0
- package/apps/web/upstream/src/libs/SoundBleep.js +20 -0
- package/apps/web/upstream/src/libs/StatePersistence.js +90 -0
- package/apps/web/upstream/src/libs/ThemeManager.js +128 -0
- package/apps/web/upstream/src/libs/TypingMiddleware.js +105 -0
- package/apps/web/upstream/src/libs/WindowTitle.js +63 -0
- package/apps/web/upstream/src/libs/batchedAdd.js +74 -0
- package/apps/web/upstream/src/libs/bufferTools.js +177 -0
- package/apps/web/upstream/src/libs/polyfill/Element.closest.js +18 -0
- package/apps/web/upstream/src/libs/renderers/Html.js +113 -0
- package/apps/web/upstream/src/libs/settingTools.js +31 -0
- package/apps/web/upstream/src/libs/state/BufferState.js +703 -0
- package/apps/web/upstream/src/libs/state/NetworkState.js +158 -0
- package/apps/web/upstream/src/libs/state/UserState.js +89 -0
- package/apps/web/upstream/src/libs/state/common.js +26 -0
- package/apps/web/upstream/src/libs/state.js +961 -0
- package/apps/web/upstream/src/libs/storage/Local.js +51 -0
- package/apps/web/upstream/src/main.js +566 -0
- package/apps/web/upstream/src/res/autocompleteCommands.js +31 -0
- package/apps/web/upstream/src/res/configTemplates.js +373 -0
- package/apps/web/upstream/src/res/globalStyle.css +277 -0
- package/apps/web/upstream/src/res/kiwiLoadingLogo.png +0 -0
- package/apps/web/upstream/src/res/locales/app.af-ZA.po +1145 -0
- package/apps/web/upstream/src/res/locales/app.ar-SA.po +1145 -0
- package/apps/web/upstream/src/res/locales/app.bg-BG.po +1145 -0
- package/apps/web/upstream/src/res/locales/app.bs-BA.po +1145 -0
- package/apps/web/upstream/src/res/locales/app.ca-ES.po +1145 -0
- package/apps/web/upstream/src/res/locales/app.cs-CZ.po +1145 -0
- package/apps/web/upstream/src/res/locales/app.da-DK.po +1145 -0
- package/apps/web/upstream/src/res/locales/app.de-DE.po +1145 -0
- package/apps/web/upstream/src/res/locales/app.dev.po +1246 -0
- package/apps/web/upstream/src/res/locales/app.el-GR.po +1145 -0
- package/apps/web/upstream/src/res/locales/app.en-US.po +1145 -0
- package/apps/web/upstream/src/res/locales/app.es-419.po +1145 -0
- package/apps/web/upstream/src/res/locales/app.es-AR.po +1145 -0
- package/apps/web/upstream/src/res/locales/app.es-EM.po +536 -0
- package/apps/web/upstream/src/res/locales/app.es-ES.po +1145 -0
- package/apps/web/upstream/src/res/locales/app.es-US.po +1145 -0
- package/apps/web/upstream/src/res/locales/app.eu-ES.po +1145 -0
- package/apps/web/upstream/src/res/locales/app.fi-FI.po +1145 -0
- package/apps/web/upstream/src/res/locales/app.fr-FR.po +1145 -0
- package/apps/web/upstream/src/res/locales/app.gl-ES.po +1145 -0
- package/apps/web/upstream/src/res/locales/app.he-IL.po +1145 -0
- package/apps/web/upstream/src/res/locales/app.hi-IN.po +1145 -0
- package/apps/web/upstream/src/res/locales/app.hu-HU.po +1145 -0
- package/apps/web/upstream/src/res/locales/app.id-ID.po +1145 -0
- package/apps/web/upstream/src/res/locales/app.it-IT.po +1145 -0
- package/apps/web/upstream/src/res/locales/app.ja-JP.po +1145 -0
- package/apps/web/upstream/src/res/locales/app.ko-KR.po +1145 -0
- package/apps/web/upstream/src/res/locales/app.nl-NL.po +1145 -0
- package/apps/web/upstream/src/res/locales/app.no-NO.po +1145 -0
- package/apps/web/upstream/src/res/locales/app.pl-PL.po +1145 -0
- package/apps/web/upstream/src/res/locales/app.pt-BR.po +1145 -0
- package/apps/web/upstream/src/res/locales/app.pt-PT.po +1145 -0
- package/apps/web/upstream/src/res/locales/app.ro-RO.po +1145 -0
- package/apps/web/upstream/src/res/locales/app.ru-RU.po +1145 -0
- package/apps/web/upstream/src/res/locales/app.sl-SI.po +1145 -0
- package/apps/web/upstream/src/res/locales/app.sq-AL.po +1145 -0
- package/apps/web/upstream/src/res/locales/app.sr-SP.po +1145 -0
- package/apps/web/upstream/src/res/locales/app.sv-SE.po +1145 -0
- package/apps/web/upstream/src/res/locales/app.tr-TR.po +1145 -0
- package/apps/web/upstream/src/res/locales/app.uk-UA.po +1145 -0
- package/apps/web/upstream/src/res/locales/app.vi-VN.po +1145 -0
- package/apps/web/upstream/src/res/locales/app.zh-CN.po +1145 -0
- package/apps/web/upstream/src/res/locales/app.zh-TW.po +1145 -0
- package/apps/web/upstream/src/res/localesList.json +27 -0
- package/apps/web/upstream/src/res/logo.png +0 -0
- package/apps/web/upstream/src/thirdparty/about.html +43 -0
- package/apps/web/upstream/src/thirdparty/index.js +5 -0
- package/apps/web/upstream/src/thirdparty/kiwiirccom.vue +185 -0
- package/apps/web/upstream/static/config.json +28 -0
- package/apps/web/upstream/static/emoticons/smile.png +0 -0
- package/apps/web/upstream/static/favicon.png +0 -0
- package/apps/web/upstream/static/highlight.mp3 +0 -0
- package/apps/web/upstream/static/highlight.ogg +0 -0
- package/apps/web/upstream/static/locales/.gitignore +2 -0
- package/apps/web/upstream/static/plugins/customise.html.example +10 -0
- package/apps/web/upstream/static/themes/coffee/theme.css +145 -0
- package/apps/web/upstream/static/themes/common/base.css +1064 -0
- package/apps/web/upstream/static/themes/dark/theme.css +196 -0
- package/apps/web/upstream/static/themes/default/theme.css +92 -0
- package/apps/web/upstream/static/themes/elite/theme.css +248 -0
- package/apps/web/upstream/static/themes/grayfox/theme.css +143 -0
- package/apps/web/upstream/static/themes/nightswatch/theme.css +367 -0
- package/apps/web/upstream/static/themes/osprey/theme.css +115 -0
- package/apps/web/upstream/static/themes/radioactive/theme.css +1211 -0
- package/apps/web/upstream/static/themes/sky/theme.css +107 -0
- package/apps/web/upstream/tests/unit/BatchAdd.spec.js +174 -0
- package/apps/web/upstream/tests/unit/MessageParser.spec.js +125 -0
- package/apps/web/upstream/tests/unit/Misc.spec.js +24 -0
- package/apps/web/upstream/tests/unit/NetworkState.spec.js +58 -0
- package/apps/web/upstream/tests/unit/StartupError.spec.js +20 -0
- package/apps/web/upstream/vue.config.js +150 -0
- package/apps/web/upstream/yarn.lock +9767 -0
- package/apps/web/vitest.config.ts +13 -0
- package/biome.json +2 -0
- package/docs/ADR-001-pure-reducers-and-effect-system.md +74 -0
- package/docs/ADR-002-location-of-authority.md +82 -0
- package/docs/ADR-003-durable-object-sharding.md +110 -0
- package/docs/ADR-004-dynamodb-schema.md +96 -0
- package/docs/ADR-005-wss-only-transport-v1.md +83 -0
- package/docs/ADR-006-sasl-mechanism-scope.md +96 -0
- package/docs/ADR-007-deterministic-ports.md +82 -0
- package/docs/ADR-008-monorepo-tooling.md +60 -0
- package/docs/ADR-009-dual-transport-wss-and-irc-tls.md +138 -0
- package/docs/ADR-Index.md +15 -0
- package/docs/AWS-Adapter-Architecture.md +496 -0
- package/docs/AWS-Deployment.md +1275 -0
- package/docs/AWS-TCP-Deployment.md +410 -0
- package/docs/Cloudflare-Deployment-Guide.md +719 -0
- package/docs/Cloudflare-TCP-Deployment.md +510 -0
- package/docs/Home.md +16 -0
- package/docs/Observability.md +87 -0
- package/docs/PlanExtensions.md +812 -0
- package/docs/PlanIRCv3Websocket.md +503 -0
- package/docs/PlanWebClient.md +451 -0
- package/docs/Release-Process.md +443 -0
- package/docs/WebClientGuide.md +535 -0
- package/package.json +2 -2
- package/packages/aws-adapter/package.json +1 -1
- package/packages/aws-adapter/src/handlers/default.ts +3 -0
- package/packages/aws-adapter/src/handlers/nlb-stream.ts +9 -0
- package/packages/aws-adapter/src/serialize.ts +29 -1
- package/packages/aws-adapter/tests/account-store-dynamo.test.ts +49 -8
- package/packages/aws-adapter/tests/account-store.test.ts +4 -7
- package/packages/aws-adapter/tests/aws-harness.ts +5 -2
- package/packages/aws-adapter/tests/aws-integration.test.ts +8 -4
- package/packages/aws-adapter/tests/aws-runtime.test.ts +79 -0
- package/packages/aws-adapter/tests/config-loader.test.ts +11 -2
- package/packages/aws-adapter/tests/connect.test.ts +9 -9
- package/packages/aws-adapter/tests/disconnect-fanout.test.ts +3 -4
- package/packages/aws-adapter/tests/global-setup.ts +55 -30
- package/packages/aws-adapter/tests/gone-exception.test.ts +4 -9
- package/packages/aws-adapter/tests/handlers.test.ts +90 -4
- package/packages/aws-adapter/tests/message-store.test.ts +6 -5
- package/packages/aws-adapter/tests/nlb-stream.test.ts +60 -8
- package/packages/cf-adapter/package.json +2 -1
- package/packages/cf-adapter/src/connection-do.ts +3 -0
- package/packages/cf-adapter/src/serialize.ts +25 -4
- package/packages/cf-adapter/tests/cf-harness.ts +12 -1
- package/packages/cf-adapter/tests/cf-runtime.test.ts +269 -0
- package/packages/cf-adapter/tests/channel-do.test.ts +117 -0
- package/packages/cf-adapter/tests/config-loader.test.ts +49 -0
- package/packages/cf-adapter/tests/connection-do-pure.test.ts +91 -0
- package/packages/cf-adapter/tests/d1-account-store.test.ts +48 -0
- package/packages/cf-adapter/tests/registry-do.test.ts +104 -0
- package/packages/cf-adapter/tests/serialize.test.ts +308 -0
- package/packages/cf-adapter/vitest.config.ts +1 -1
- package/packages/in-memory-runtime/package.json +1 -1
- package/packages/irc-core/package.json +1 -1
- package/packages/irc-core/reports/mutation/mutation.html +342 -0
- package/packages/irc-core/src/caps/capabilities.ts +37 -3
- package/packages/irc-core/src/caps/index.ts +1 -0
- package/packages/irc-core/src/caps/sts.ts +84 -0
- package/packages/irc-core/src/commands/account-notify.ts +53 -0
- package/packages/irc-core/src/commands/away.ts +9 -3
- package/packages/irc-core/src/commands/cap.ts +23 -5
- package/packages/irc-core/src/commands/chathistory.ts +40 -11
- package/packages/irc-core/src/commands/index.ts +34 -0
- package/packages/irc-core/src/commands/isupport.ts +53 -0
- package/packages/irc-core/src/commands/mode.ts +8 -1
- package/packages/irc-core/src/commands/monitor.ts +327 -0
- package/packages/irc-core/src/commands/multiline.ts +256 -0
- package/packages/irc-core/src/commands/pre-away.ts +112 -0
- package/packages/irc-core/src/commands/privmsg.ts +19 -12
- package/packages/irc-core/src/commands/read-marker.ts +108 -0
- package/packages/irc-core/src/commands/registration.ts +8 -0
- package/packages/irc-core/src/commands/sasl.ts +24 -1
- package/packages/irc-core/src/commands/tagmsg.ts +71 -1
- package/packages/irc-core/src/commands/whois.ts +5 -0
- package/packages/irc-core/src/config.ts +65 -10
- package/packages/irc-core/src/effects.ts +14 -0
- package/packages/irc-core/src/ports.ts +389 -0
- package/packages/irc-core/src/protocol/index.ts +14 -0
- package/packages/irc-core/src/protocol/labeled-response.ts +133 -0
- package/packages/irc-core/src/protocol/numerics.ts +15 -0
- package/packages/irc-core/src/protocol/outbound.ts +16 -1
- package/packages/irc-core/src/protocol/standard-replies.ts +277 -0
- package/packages/irc-core/src/state/connection.ts +32 -1
- package/packages/irc-core/src/types.ts +112 -0
- package/packages/irc-core/stryker.commands.conf.json +1 -2
- package/packages/irc-core/tests/away-store.test.ts +73 -0
- package/packages/irc-core/tests/batch.test.ts +15 -0
- package/packages/irc-core/tests/caps/capabilities.test.ts +119 -2
- package/packages/irc-core/tests/caps/sts.test.ts +123 -0
- package/packages/irc-core/tests/commands/account-notify.test.ts +171 -0
- package/packages/irc-core/tests/commands/away.test.ts +85 -2
- package/packages/irc-core/tests/commands/cap.test.ts +165 -0
- package/packages/irc-core/tests/commands/chathistory.test.ts +148 -1
- package/packages/irc-core/tests/commands/echo-message.test.ts +9 -3
- package/packages/irc-core/tests/commands/isupport.test.ts +80 -0
- package/packages/irc-core/tests/commands/mode.test.ts +72 -0
- package/packages/irc-core/tests/commands/monitor.test.ts +411 -0
- package/packages/irc-core/tests/commands/multiline.test.ts +428 -0
- package/packages/irc-core/tests/commands/pre-away.test.ts +241 -0
- package/packages/irc-core/tests/commands/privmsg.test.ts +154 -3
- package/packages/irc-core/tests/commands/read-marker.test.ts +211 -0
- package/packages/irc-core/tests/commands/registration.test.ts +64 -0
- package/packages/irc-core/tests/commands/sasl.test.ts +116 -2
- package/packages/irc-core/tests/commands/tagmsg.test.ts +357 -0
- package/packages/irc-core/tests/commands/whois.test.ts +34 -0
- package/packages/irc-core/tests/config.test.ts +103 -13
- package/packages/irc-core/tests/labeled-response.test.ts +181 -0
- package/packages/irc-core/tests/message-tags.test.ts +114 -0
- package/packages/irc-core/tests/monitor-store.test.ts +200 -0
- package/packages/irc-core/tests/numerics.test.ts +13 -0
- package/packages/irc-core/tests/parser.test.ts +109 -0
- package/packages/irc-core/tests/read-marker-store.test.ts +108 -0
- package/packages/irc-core/tests/standard-replies.test.ts +412 -0
- package/packages/irc-core/tests/types.test.ts +27 -0
- package/packages/irc-server/package.json +1 -1
- package/packages/irc-server/src/actor.ts +450 -3
- package/packages/irc-server/src/dispatch.ts +108 -16
- package/packages/irc-server/tests/actor.test.ts +1432 -39
- package/packages/irc-server/tests/dispatch.test.ts +263 -2
- package/packages/irc-test-support/package.json +1 -1
- package/packages/irc-test-support/src/index.ts +6 -0
- package/packages/irc-test-support/src/scenarios.ts +24 -2
- package/packages/irc-test-support/src/test-config.ts +54 -0
- package/packages/irc-test-support/tests/test-config.test.ts +51 -0
- package/tools/ci-hardening/package.json +1 -1
- package/tools/ci-hardening/tests/ddb-local.test.ts +82 -0
- package/tools/load-test/package.json +33 -0
- package/tools/load-test/src/client.ts +351 -0
- package/tools/load-test/src/config.ts +313 -0
- package/tools/load-test/src/harness.ts +116 -0
- package/tools/load-test/src/main.ts +120 -0
- package/tools/load-test/src/metrics.ts +168 -0
- package/tools/load-test/src/report.ts +106 -0
- package/tools/load-test/tests/client.test.ts +212 -0
- package/tools/load-test/tests/config.test.ts +152 -0
- package/tools/load-test/tests/framing.test.ts +37 -0
- package/tools/load-test/tests/harness.test.ts +165 -0
- package/tools/load-test/tests/metrics.test.ts +174 -0
- package/tools/load-test/tests/report.test.ts +161 -0
- package/tools/load-test/tests/smoke.test.ts +67 -0
- package/tools/load-test/tsconfig.build.json +12 -0
- package/tools/load-test/tsconfig.test.json +10 -0
- package/tools/load-test/vitest.config.ts +29 -0
- package/tools/tcp-ws-forwarder/package.json +1 -1
|
@@ -27,35 +27,50 @@
|
|
|
27
27
|
|
|
28
28
|
import {
|
|
29
29
|
type AccountStore,
|
|
30
|
+
type AwayStore,
|
|
30
31
|
type ChanName,
|
|
31
32
|
type ChannelState,
|
|
32
33
|
type Clock,
|
|
33
34
|
type ConnSnapshot,
|
|
34
35
|
type ConnectionState,
|
|
36
|
+
DEFAULT_MONITOR_LIMIT,
|
|
37
|
+
DEFAULT_MULTILINE_MAX_BYTES,
|
|
35
38
|
Effect,
|
|
36
39
|
type Effect as EffectType,
|
|
37
40
|
EmptyMotdProvider,
|
|
38
41
|
type IdFactory,
|
|
39
42
|
type IrcMessage,
|
|
43
|
+
LABELED_RESPONSE_CAP_NAME,
|
|
40
44
|
type Logger,
|
|
41
45
|
type MessageStore,
|
|
46
|
+
type MonitorStore,
|
|
42
47
|
type MotdProvider,
|
|
43
48
|
type MtlsIdentityProvider,
|
|
49
|
+
type MultilineEntry,
|
|
44
50
|
type NickHistoryStore,
|
|
45
51
|
NoopLoggerInstance,
|
|
46
52
|
Numerics,
|
|
47
53
|
type RawLine,
|
|
54
|
+
type ReadMarkerStore,
|
|
48
55
|
type ServerConfig,
|
|
49
56
|
type ServerStats,
|
|
57
|
+
applyLabelToEffects,
|
|
50
58
|
buildCtx,
|
|
59
|
+
buildMultilineFanout,
|
|
51
60
|
chathistoryReducer,
|
|
61
|
+
extractLabel,
|
|
62
|
+
finalizeMultilineBatch,
|
|
52
63
|
handleJoinZero,
|
|
64
|
+
hostmaskOf,
|
|
53
65
|
isChannelTarget,
|
|
54
66
|
isonReducer,
|
|
55
67
|
killReducer,
|
|
56
68
|
listReducer,
|
|
57
69
|
lusersReducer,
|
|
70
|
+
monitorReducer,
|
|
71
|
+
multilineByteLength,
|
|
58
72
|
parse,
|
|
73
|
+
recordMultilineMessage,
|
|
59
74
|
rehashLine,
|
|
60
75
|
rehashReducer,
|
|
61
76
|
statsReducer,
|
|
@@ -153,6 +168,42 @@ export interface ConnectionActorOptions {
|
|
|
153
168
|
* preserves the no-store behaviour: `WHOWAS <nick>` emits `406`+`369`.
|
|
154
169
|
*/
|
|
155
170
|
readonly history?: NickHistoryStore;
|
|
171
|
+
/**
|
|
172
|
+
* Read-marker persistence source for IRCv3 `draft/read-marker` (absent
|
|
173
|
+
* when the deployment does not persist read markers). When bound,
|
|
174
|
+
* `ctx.readMarkers` is defined so a `+draft/read-marker=<msgid>` TAGMSG
|
|
175
|
+
* from an identified, cap-enabled client updates the per-`(account,
|
|
176
|
+
* channel)` marker, and SASL identify seeds the connection's
|
|
177
|
+
* `lastReadMarkers` from the stored values. Omitted (→ `ctx.readMarkers`
|
|
178
|
+
* undefined) preserves the session-only marker behaviour: the connection
|
|
179
|
+
* still tracks its own read position via chathistory playback, but the
|
|
180
|
+
* marker is not shared across connections or restored on reconnect.
|
|
181
|
+
*/
|
|
182
|
+
readonly readMarkers?: ReadMarkerStore;
|
|
183
|
+
/**
|
|
184
|
+
* Away-status persistence source for IRCv3 `draft/pre-away` (absent
|
|
185
|
+
* when the deployment does not persist away reasons). When bound,
|
|
186
|
+
* `ctx.away` is defined so a set/clear of `AWAY` from an identified
|
|
187
|
+
* connection mirrors the change into the per-`account` record, and
|
|
188
|
+
* SASL identify replays the stored reason onto the connection (emitting
|
|
189
|
+
* `306 RPL_NOWAWAY`) so a reconnect restores the user's away state.
|
|
190
|
+
* Omitted (→ `ctx.away` undefined) preserves the session-only
|
|
191
|
+
* behaviour: the connection still tracks its own `AWAY` but the reason
|
|
192
|
+
* is not shared across connections or restored on reconnect.
|
|
193
|
+
*/
|
|
194
|
+
readonly away?: AwayStore;
|
|
195
|
+
/**
|
|
196
|
+
* Monitor-subscription registry for the IRCv3 `MONITOR` command (absent
|
|
197
|
+
* when MONITOR push notifications are disabled). When bound, the actor
|
|
198
|
+
* syncs each connection's watchlist into the store and the store's
|
|
199
|
+
* online/offline hooks fan `730`/`731` lines out to subscribed watchers.
|
|
200
|
+
* Omitted (→ no push notifications) preserves the existing behaviour
|
|
201
|
+
* for callers that have not opted in: `MONITOR +nick` still works
|
|
202
|
+
* synchronously (the reducer resolves online/offline status against
|
|
203
|
+
* the store's `onlineNicks` snapshot) but no async push fires when a
|
|
204
|
+
* watched nick later connects or quits.
|
|
205
|
+
*/
|
|
206
|
+
readonly monitor?: MonitorStore;
|
|
156
207
|
/**
|
|
157
208
|
* Network-statistics source for the `LUSERS` and `STATS` commands
|
|
158
209
|
* (absent when no stats backend is wired). When bound, the actor
|
|
@@ -186,6 +237,16 @@ export interface ConnectionActorOptions {
|
|
|
186
237
|
* pipeline is shared unchanged across both transports.
|
|
187
238
|
*/
|
|
188
239
|
readonly transport?: Transport;
|
|
240
|
+
/**
|
|
241
|
+
* Transport-level TLS fact (PLAN §5 / docs/PlanExtensions.md "user mode
|
|
242
|
+
* `S`"). Set to `true` when the connection arrived over TLS — a `wss://`
|
|
243
|
+
* WebSocket or an `irc+tls://` raw TCP connection. The actor stamps it
|
|
244
|
+
* onto `state.secure` at construction; the registration reducer then
|
|
245
|
+
* surfaces it as read-only user mode `S` (visible in `221`/`WHOIS`,
|
|
246
|
+
* rejected by the `MODE` parser with `501`). Defaults to `false` so
|
|
247
|
+
* plain-transport tests and deployments keep the pre-TLS-mode behaviour.
|
|
248
|
+
*/
|
|
249
|
+
readonly secure?: boolean;
|
|
189
250
|
}
|
|
190
251
|
|
|
191
252
|
export class ConnectionActor {
|
|
@@ -201,6 +262,9 @@ export class ConnectionActor {
|
|
|
201
262
|
private readonly accounts: AccountStore | undefined;
|
|
202
263
|
private readonly mtlsIdentity: MtlsIdentityProvider | undefined;
|
|
203
264
|
private readonly history: NickHistoryStore | undefined;
|
|
265
|
+
private readonly readMarkers: ReadMarkerStore | undefined;
|
|
266
|
+
private readonly away: AwayStore | undefined;
|
|
267
|
+
private readonly monitor: MonitorStore | undefined;
|
|
204
268
|
private readonly stats: ServerStats | undefined;
|
|
205
269
|
private readonly reducers: RoutedReducers;
|
|
206
270
|
/**
|
|
@@ -222,6 +286,22 @@ export class ConnectionActor {
|
|
|
222
286
|
* that need a non-undefined id in test assertions.
|
|
223
287
|
*/
|
|
224
288
|
private readonly pinnedTraceId: string | undefined;
|
|
289
|
+
/**
|
|
290
|
+
* In-progress IRCv3 `draft/multiline` batch, if any. Set when the client
|
|
291
|
+
* sends `BATCH +id draft/multiline :<target>` (and the connection
|
|
292
|
+
* negotiated the cap), populated by each intervening `PRIVMSG`/`NOTICE`,
|
|
293
|
+
* and cleared when the closing `BATCH -id` is finalized. While set, the
|
|
294
|
+
* actor diverts `PRIVMSG`/`NOTICE` into the accumulator instead of
|
|
295
|
+
* running the normal channel/user reducers.
|
|
296
|
+
*/
|
|
297
|
+
private openMultilineBatch:
|
|
298
|
+
| {
|
|
299
|
+
readonly id: string;
|
|
300
|
+
readonly target: string;
|
|
301
|
+
command: 'PRIVMSG' | 'NOTICE';
|
|
302
|
+
readonly entries: MultilineEntry[];
|
|
303
|
+
}
|
|
304
|
+
| undefined;
|
|
225
305
|
|
|
226
306
|
constructor(opts: ConnectionActorOptions) {
|
|
227
307
|
this.state = opts.state;
|
|
@@ -236,6 +316,9 @@ export class ConnectionActor {
|
|
|
236
316
|
this.accounts = opts.accounts;
|
|
237
317
|
this.mtlsIdentity = opts.mtlsIdentity;
|
|
238
318
|
this.history = opts.history;
|
|
319
|
+
this.readMarkers = opts.readMarkers;
|
|
320
|
+
this.away = opts.away;
|
|
321
|
+
this.monitor = opts.monitor;
|
|
239
322
|
this.stats = opts.stats;
|
|
240
323
|
this.logger = opts.logger ?? NoopLoggerInstance;
|
|
241
324
|
this.pinnedTraceId = opts.traceId;
|
|
@@ -251,6 +334,15 @@ export class ConnectionActor {
|
|
|
251
334
|
// regardless of which authority owns the command's state.
|
|
252
335
|
this.reducers = buildRoutedReducers(opts.serverConfig);
|
|
253
336
|
this.transport = opts.transport ?? new WsTextFrameTransport();
|
|
337
|
+
// Surface the transport's TLS fact on the connection state so the
|
|
338
|
+
// registration reducer can stamp read-only user mode `S` from it.
|
|
339
|
+
// Only stamped when the caller explicitly asserts the fact; adapters
|
|
340
|
+
// that reconstruct the actor from persisted state (where `secure` was
|
|
341
|
+
// already written at construction time) omit the option so the
|
|
342
|
+
// deserialized value survives untouched.
|
|
343
|
+
if (opts.secure !== undefined) {
|
|
344
|
+
this.state.secure = opts.secure;
|
|
345
|
+
}
|
|
254
346
|
}
|
|
255
347
|
|
|
256
348
|
/**
|
|
@@ -315,12 +407,75 @@ export class ConnectionActor {
|
|
|
315
407
|
// the channel authority (e.g. CF ChannelDO). Done before `route` so the
|
|
316
408
|
// synchronous reducers observe up-to-date rosters / modes / topics.
|
|
317
409
|
await this.prefetchChannelState(msg);
|
|
318
|
-
const
|
|
410
|
+
const label = extractLabel(msg.tags);
|
|
411
|
+
const effects = await this.route(msg, label);
|
|
412
|
+
const labeled = this.applyLabel(effects, label);
|
|
413
|
+
await dispatch(labeled, this.runtime, this.dispatchOpts(log, traceId));
|
|
414
|
+
// After dispatch completes the bound MonitorStore (if any) may have
|
|
415
|
+
// enqueued `730`/`731` pushes for ReserveNick/ChangeNick/ReleaseNick
|
|
416
|
+
// transitions; turn each into one `Send` effect and dispatch them in a
|
|
417
|
+
// follow-up pass. Kept separate from the original effects array so the
|
|
418
|
+
// reducer's outputs continue to dedupe against themselves, not against
|
|
419
|
+
// fan-out sends.
|
|
420
|
+
await this.drainMonitorPushes(log, traceId);
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
/**
|
|
424
|
+
* Drains pending {@link MonitorPush}es from the bound {@link MonitorStore}
|
|
425
|
+
* and dispatches one `Send` effect per push. The watcher's nick is
|
|
426
|
+
* resolved via the live {@link ConnectionState} so the wire line carries
|
|
427
|
+
* the canonical spelling (`*` placeholder when the watcher is no longer
|
|
428
|
+
* registered — the runtime drops the send in that case anyway).
|
|
429
|
+
*/
|
|
430
|
+
private async drainMonitorPushes(log: Logger, traceId: string): Promise<void> {
|
|
431
|
+
if (this.monitor === undefined) return;
|
|
432
|
+
const pushes = this.monitor.drainPushes();
|
|
433
|
+
if (pushes.length === 0) return;
|
|
434
|
+
const effects: EffectType[] = [];
|
|
435
|
+
for (const push of pushes) {
|
|
436
|
+
const watcher = await this.runtime.getConnection(push.connId);
|
|
437
|
+
const watcherNick = watcher?.nick ?? '*';
|
|
438
|
+
const code = push.status === 'online' ? Numerics.RPL_MONONLINE : Numerics.RPL_MONOFFLINE;
|
|
439
|
+
const codeStr = code.toString().padStart(3, '0');
|
|
440
|
+
effects.push(
|
|
441
|
+
Effect.send(push.connId, [
|
|
442
|
+
{ text: `:${this.serverConfig.serverName} ${codeStr} ${watcherNick} :${push.display}` },
|
|
443
|
+
]),
|
|
444
|
+
);
|
|
445
|
+
}
|
|
446
|
+
// `effects` has one entry per push; pushes was non-empty (early
|
|
447
|
+
// return above), so the second dispatch always has work to do.
|
|
319
448
|
await dispatch(effects, this.runtime, this.dispatchOpts(log, traceId));
|
|
320
449
|
}
|
|
321
450
|
|
|
322
451
|
private dispatchOpts(log: Logger, traceId: string): DispatchOptions {
|
|
323
|
-
return {
|
|
452
|
+
return {
|
|
453
|
+
logger: log,
|
|
454
|
+
traceId,
|
|
455
|
+
connectionId: this.state.id,
|
|
456
|
+
...(this.monitor !== undefined ? { monitor: this.monitor } : {}),
|
|
457
|
+
};
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
/**
|
|
461
|
+
* IRCv3 `labeled-response` wrapping.
|
|
462
|
+
*
|
|
463
|
+
* When the inbound message carried a `+label` client tag AND the requester
|
|
464
|
+
* negotiated both `batch` and `labeled-response`, every `Send`-to-self the
|
|
465
|
+
* reducer emitted is gathered into one `BATCH +label labeled-response …
|
|
466
|
+
* -label` frame (and an empty batch is emitted when there was no reply).
|
|
467
|
+
* Otherwise the effect list is returned unchanged.
|
|
468
|
+
*
|
|
469
|
+
* The label is read back from {@link Ctx.label} (plumbed through `route`)
|
|
470
|
+
* so the correlation is attributable to the same per-invocation context the
|
|
471
|
+
* reducers observed.
|
|
472
|
+
*/
|
|
473
|
+
private applyLabel(effects: EffectType[], label: string | undefined): EffectType[] {
|
|
474
|
+
if (label === undefined) return effects;
|
|
475
|
+
if (!this.state.caps.has('batch') || !this.state.caps.has(LABELED_RESPONSE_CAP_NAME)) {
|
|
476
|
+
return effects;
|
|
477
|
+
}
|
|
478
|
+
return applyLabelToEffects(effects, { label, connId: this.state.id });
|
|
324
479
|
}
|
|
325
480
|
|
|
326
481
|
/**
|
|
@@ -336,7 +491,7 @@ export class ConnectionActor {
|
|
|
336
491
|
await Promise.all(targets.map((name) => refresh.call(this.channels, name)));
|
|
337
492
|
}
|
|
338
493
|
|
|
339
|
-
private async route(msg: IrcMessage): Promise<EffectType[]> {
|
|
494
|
+
private async route(msg: IrcMessage, label: string | undefined): Promise<EffectType[]> {
|
|
340
495
|
const ctx = buildCtx({
|
|
341
496
|
serverConfig: this.serverConfig,
|
|
342
497
|
clock: this.clock,
|
|
@@ -347,8 +502,21 @@ export class ConnectionActor {
|
|
|
347
502
|
...(this.accounts !== undefined ? { accounts: this.accounts } : {}),
|
|
348
503
|
...(this.mtlsIdentity !== undefined ? { mtlsIdentity: this.mtlsIdentity } : {}),
|
|
349
504
|
...(this.history !== undefined ? { history: this.history } : {}),
|
|
505
|
+
...(this.readMarkers !== undefined ? { readMarkers: this.readMarkers } : {}),
|
|
506
|
+
...(this.away !== undefined ? { away: this.away } : {}),
|
|
507
|
+
...(label !== undefined ? { label } : {}),
|
|
350
508
|
});
|
|
351
509
|
|
|
510
|
+
// While a `draft/multiline` batch is open, intervening PRIVMSG/NOTICE
|
|
511
|
+
// lines are accumulated into the batch instead of being delivered
|
|
512
|
+
// immediately. The closing BATCH -id finalizes and fans the batch out.
|
|
513
|
+
if (
|
|
514
|
+
this.openMultilineBatch !== undefined &&
|
|
515
|
+
(msg.command === 'PRIVMSG' || msg.command === 'NOTICE')
|
|
516
|
+
) {
|
|
517
|
+
return this.accumulateMultilineLine(msg);
|
|
518
|
+
}
|
|
519
|
+
|
|
352
520
|
switch (msg.command) {
|
|
353
521
|
// -------- Connection-authority commands --------
|
|
354
522
|
case 'PING':
|
|
@@ -373,6 +541,8 @@ export class ConnectionActor {
|
|
|
373
541
|
return this.reducers.cap(this.state, msg, ctx).effects;
|
|
374
542
|
case 'AUTHENTICATE':
|
|
375
543
|
return this.reducers.authenticate(this.state, msg, ctx).effects;
|
|
544
|
+
case 'BATCH':
|
|
545
|
+
return this.routeBatch(msg, ctx);
|
|
376
546
|
case 'AWAY':
|
|
377
547
|
return this.reducers.away(this.state, msg, ctx).effects;
|
|
378
548
|
case 'SETNAME':
|
|
@@ -391,6 +561,8 @@ export class ConnectionActor {
|
|
|
391
561
|
return this.routeUserhost(msg, ctx);
|
|
392
562
|
case 'ISON':
|
|
393
563
|
return this.routeIson(msg, ctx);
|
|
564
|
+
case 'MONITOR':
|
|
565
|
+
return this.routeMonitor(msg, ctx);
|
|
394
566
|
|
|
395
567
|
// -------- Channel-authority commands --------
|
|
396
568
|
case 'JOIN':
|
|
@@ -756,6 +928,74 @@ export class ConnectionActor {
|
|
|
756
928
|
return isonReducer(online, msg, ctx).effects;
|
|
757
929
|
}
|
|
758
930
|
|
|
931
|
+
/**
|
|
932
|
+
* Routes `MONITOR <sub> [<nicks>]` to the monitor reducer. Resolves the
|
|
933
|
+
* currently-online nicks (folded → display) via the nick registry so the
|
|
934
|
+
* reducer can emit `730`/`731` synchronously. The reducer also updates
|
|
935
|
+
* `state.monitorList`; the actor then forwards adds/removes to the bound
|
|
936
|
+
* {@link MonitorStore} (when present) so future online/offline
|
|
937
|
+
* transitions push `730`/`731` to the watcher.
|
|
938
|
+
*/
|
|
939
|
+
private async routeMonitor(
|
|
940
|
+
msg: IrcMessage,
|
|
941
|
+
ctx: ReturnType<typeof buildCtx>,
|
|
942
|
+
): Promise<EffectType[]> {
|
|
943
|
+
// Resolve every nick the reducer might consult — the union of the
|
|
944
|
+
// existing watchlist and any newly-requested targets — so the reducer
|
|
945
|
+
// can emit `730`/`731` synchronously without an O(N) lookupNick call
|
|
946
|
+
// per nick inside its own loop. Mirrors the routeIson pattern.
|
|
947
|
+
const requested =
|
|
948
|
+
msg.params[1] !== undefined
|
|
949
|
+
? msg.params[1]
|
|
950
|
+
.split(',')
|
|
951
|
+
.map((s) => s.trim())
|
|
952
|
+
.filter((s) => s.length > 0)
|
|
953
|
+
: [];
|
|
954
|
+
const candidates = new Set<string>(ctx.connection.monitorList ?? []);
|
|
955
|
+
for (const n of requested) candidates.add(n);
|
|
956
|
+
const online = await this.resolveOnlineMap(candidates);
|
|
957
|
+
|
|
958
|
+
const before = new Set(ctx.connection.monitorList ?? []);
|
|
959
|
+
const effects = monitorReducer(online, DEFAULT_MONITOR_LIMIT, msg, ctx).effects;
|
|
960
|
+
// Sync the watchlist delta (if any) into the bound MonitorStore so
|
|
961
|
+
// later ReserveNick / ReleaseNick dispatches can fire push effects.
|
|
962
|
+
if (this.monitor !== undefined) {
|
|
963
|
+
const after = ctx.connection.monitorList ?? new Set<string>();
|
|
964
|
+
this.monitor.sync(ctx.connId, before, after);
|
|
965
|
+
// Remove the connection's subscription entirely when its watchlist
|
|
966
|
+
// becomes empty — keeps the reverse index tidy.
|
|
967
|
+
if (after.size === 0) {
|
|
968
|
+
this.monitor.unsubscribe(ctx.connId);
|
|
969
|
+
}
|
|
970
|
+
}
|
|
971
|
+
return effects;
|
|
972
|
+
}
|
|
973
|
+
|
|
974
|
+
/**
|
|
975
|
+
* Resolves each candidate nick via the runtime's nick registry and
|
|
976
|
+
* returns a folded-keyed → display-spelling map of the online subset.
|
|
977
|
+
* Mirrors {@link routeIson}'s per-nick lookup loop.
|
|
978
|
+
*/
|
|
979
|
+
private async resolveOnlineMap(candidates: Iterable<string>): Promise<Map<string, string>> {
|
|
980
|
+
const online = new Map<string, string>();
|
|
981
|
+
for (const nick of candidates) {
|
|
982
|
+
const connId = await this.runtime.lookupNick(nick);
|
|
983
|
+
if (connId === null) continue;
|
|
984
|
+
const conn = await this.runtime.getConnection(connId);
|
|
985
|
+
// `conn` can be null in the race where lookupNick resolved but the
|
|
986
|
+
// connection has since torn down (the nick map lags the connection
|
|
987
|
+
// map). When the connection is alive its nick is defined (the
|
|
988
|
+
// registry only resolves nicks that have been reserved, which sets
|
|
989
|
+
// `nick` on the state), so the optional chain is purely defensive
|
|
990
|
+
// against that race.
|
|
991
|
+
const display = conn?.nick;
|
|
992
|
+
if (display !== undefined) {
|
|
993
|
+
online.set(display.toLowerCase(), display);
|
|
994
|
+
}
|
|
995
|
+
}
|
|
996
|
+
return online;
|
|
997
|
+
}
|
|
998
|
+
|
|
759
999
|
/**
|
|
760
1000
|
* Routes `CHATHISTORY <sub> <target> [<args>...]` to the chathistory
|
|
761
1001
|
* reducer. The command is query-only and must NOT create a channel as a
|
|
@@ -769,6 +1009,213 @@ export class ConnectionActor {
|
|
|
769
1009
|
* Cap-gating, param validation, and batch framing all live in the pure
|
|
770
1010
|
* reducer — this method only resolves the channel and delegates.
|
|
771
1011
|
*/
|
|
1012
|
+
/**
|
|
1013
|
+
* Routes an inbound `BATCH +id …` / `BATCH -id` frame. The only
|
|
1014
|
+
* client-initiated batch type this server honours is `draft/multiline`
|
|
1015
|
+
* (and only when the connection negotiated both `batch` and
|
|
1016
|
+
* `draft/multiline`); every other form falls through to
|
|
1017
|
+
* {@link unknownCommandEffect} (`421`), preserving the pre-batch behaviour.
|
|
1018
|
+
*
|
|
1019
|
+
* On `+id draft/multiline :<target>` the actor opens the accumulator
|
|
1020
|
+
* ({@link openMultilineBatch}); intervening PRIVMSG/NOTICE frames populate
|
|
1021
|
+
* it ({@link accumulateMultilineLine}); `BATCH -id` finalizes the batch
|
|
1022
|
+
* via the pure {@link finalizeMultilineBatch} reducer (channel target) or
|
|
1023
|
+
* the actor-side per-recipient path (nick target) and returns its fanout
|
|
1024
|
+
* effects.
|
|
1025
|
+
*/
|
|
1026
|
+
private async routeBatch(
|
|
1027
|
+
msg: IrcMessage,
|
|
1028
|
+
ctx: ReturnType<typeof buildCtx>,
|
|
1029
|
+
): Promise<EffectType[]> {
|
|
1030
|
+
const marker = msg.params[0] ?? '';
|
|
1031
|
+
// `+<id>` opens a batch; `-<id>` closes one. Anything else is malformed.
|
|
1032
|
+
if (marker.startsWith('+')) {
|
|
1033
|
+
return this.beginMultilineBatch(marker.slice(1), msg, ctx);
|
|
1034
|
+
}
|
|
1035
|
+
if (marker.startsWith('-')) {
|
|
1036
|
+
return this.closeMultilineBatch(marker.slice(1), ctx);
|
|
1037
|
+
}
|
|
1038
|
+
return [this.unknownCommandEffect(msg.command)];
|
|
1039
|
+
}
|
|
1040
|
+
|
|
1041
|
+
/**
|
|
1042
|
+
* Opens a `draft/multiline` accumulator. Requires the connection to have
|
|
1043
|
+
* negotiated both `batch` and `draft/multiline`; otherwise the open is
|
|
1044
|
+
* rejected as an unknown command (the client cannot legally send the
|
|
1045
|
+
* frame without the caps).
|
|
1046
|
+
*/
|
|
1047
|
+
private beginMultilineBatch(
|
|
1048
|
+
id: string,
|
|
1049
|
+
msg: IrcMessage,
|
|
1050
|
+
_ctx: ReturnType<typeof buildCtx>,
|
|
1051
|
+
): EffectType[] {
|
|
1052
|
+
// Cap gate: the client must have negotiated both caps to use the frame.
|
|
1053
|
+
if (!this.state.caps.has('batch') || !this.state.caps.has('draft/multiline')) {
|
|
1054
|
+
return [this.unknownCommandEffect(msg.command)];
|
|
1055
|
+
}
|
|
1056
|
+
// Only the `draft/multiline` batch type is supported client→server.
|
|
1057
|
+
const type = msg.params[1];
|
|
1058
|
+
const target = msg.params[2];
|
|
1059
|
+
if (type !== 'draft/multiline' || target === undefined || target.length === 0) {
|
|
1060
|
+
return [this.unknownCommandEffect(msg.command)];
|
|
1061
|
+
}
|
|
1062
|
+
// Reject re-open while another batch is mid-flight: the IRCv3 batch spec
|
|
1063
|
+
// forbids reusing a ref id while one is open. Surface as 421 so the
|
|
1064
|
+
// client does not silently lose the first batch's lines.
|
|
1065
|
+
if (this.openMultilineBatch !== undefined) {
|
|
1066
|
+
return [this.unknownCommandEffect(msg.command)];
|
|
1067
|
+
}
|
|
1068
|
+
this.openMultilineBatch = { id, target, command: 'PRIVMSG', entries: [] };
|
|
1069
|
+
// The open marker itself produces no server→client reply.
|
|
1070
|
+
return [];
|
|
1071
|
+
}
|
|
1072
|
+
|
|
1073
|
+
/**
|
|
1074
|
+
* Accumulates one intervening PRIVMSG/NOTICE line into the open
|
|
1075
|
+
* `draft/multiline` batch. The inner line's target must match the batch
|
|
1076
|
+
* target and (after the first line) its command must match too; a
|
|
1077
|
+
* mismatch aborts the batch with a `FAIL` and clears the accumulator.
|
|
1078
|
+
*/
|
|
1079
|
+
private accumulateMultilineLine(msg: IrcMessage): EffectType[] {
|
|
1080
|
+
const open = this.openMultilineBatch;
|
|
1081
|
+
if (open === undefined) return [];
|
|
1082
|
+
const target = msg.params[0];
|
|
1083
|
+
const text = msg.params[1] ?? '';
|
|
1084
|
+
// Target mismatch: the spec requires every inner line to address the
|
|
1085
|
+
// batch's target. Abort the batch.
|
|
1086
|
+
if (target !== open.target) {
|
|
1087
|
+
this.openMultilineBatch = undefined;
|
|
1088
|
+
return [
|
|
1089
|
+
Effect.send(this.state.id, [
|
|
1090
|
+
{
|
|
1091
|
+
text: `:${this.serverConfig.serverName} FAIL BATCH MULTILINE_INVALID_TARGET ${open.target} :Inner line target does not match the batch target`,
|
|
1092
|
+
},
|
|
1093
|
+
]),
|
|
1094
|
+
];
|
|
1095
|
+
}
|
|
1096
|
+
const command = msg.command as 'PRIVMSG' | 'NOTICE';
|
|
1097
|
+
// The first inner line pins the command; subsequent lines must match.
|
|
1098
|
+
if (open.entries.length === 0) {
|
|
1099
|
+
open.command = command;
|
|
1100
|
+
} else if (command !== open.command) {
|
|
1101
|
+
this.openMultilineBatch = undefined;
|
|
1102
|
+
return [
|
|
1103
|
+
Effect.send(this.state.id, [
|
|
1104
|
+
{
|
|
1105
|
+
text: `:${this.serverConfig.serverName} FAIL BATCH MULTILINE_MIXED_COMMANDS ${open.target} :Multi-line batch mixes PRIVMSG and NOTICE`,
|
|
1106
|
+
},
|
|
1107
|
+
]),
|
|
1108
|
+
];
|
|
1109
|
+
}
|
|
1110
|
+
open.entries.push({ text });
|
|
1111
|
+
return [];
|
|
1112
|
+
}
|
|
1113
|
+
|
|
1114
|
+
/**
|
|
1115
|
+
* Finalizes an open `draft/multiline` batch on `BATCH -id`. Channel targets
|
|
1116
|
+
* delegate to the pure {@link finalizeMultilineBatch} reducer (cap-split
|
|
1117
|
+
* Broadcast). Nick targets use the actor-side per-recipient path
|
|
1118
|
+
* ({@link closeMultilineBatchToNick}) because the recipient is a single
|
|
1119
|
+
* connection whose caps must be resolved before delivery. An unknown /
|
|
1120
|
+
* mismatched id, or a close with no batch open, falls through to `421`.
|
|
1121
|
+
*/
|
|
1122
|
+
private async closeMultilineBatch(
|
|
1123
|
+
id: string,
|
|
1124
|
+
ctx: ReturnType<typeof buildCtx>,
|
|
1125
|
+
): Promise<EffectType[]> {
|
|
1126
|
+
const open = this.openMultilineBatch;
|
|
1127
|
+
if (open === undefined || open.id !== id) {
|
|
1128
|
+
return [this.unknownCommandEffect('BATCH')];
|
|
1129
|
+
}
|
|
1130
|
+
this.openMultilineBatch = undefined;
|
|
1131
|
+
const maxBytes = this.serverConfig.multilineMaxBytes ?? DEFAULT_MULTILINE_MAX_BYTES;
|
|
1132
|
+
if (isChannelTarget(open.target)) {
|
|
1133
|
+
const result = finalizeMultilineBatch(open.target, open.command, open.entries, maxBytes, ctx);
|
|
1134
|
+
return result.effects;
|
|
1135
|
+
}
|
|
1136
|
+
return this.closeMultilineBatchToNick(open, maxBytes, ctx);
|
|
1137
|
+
}
|
|
1138
|
+
|
|
1139
|
+
/**
|
|
1140
|
+
* Finalizes a nick-target multi-line batch. Resolves the single recipient
|
|
1141
|
+
* via the nick registry, inspects their caps, and delivers the appropriate
|
|
1142
|
+
* wire form: the batched replay to `draft/multiline`+`batch` peers, the
|
|
1143
|
+
* tagged joined PRIVMSG to `message-tags` peers, or the bare joined
|
|
1144
|
+
* PRIVMSG to legacy peers. PRIVMSG surfaces `401 ERR_NOSUCHNICK` when the
|
|
1145
|
+
* recipient is offline; NOTICE is silently dropped (RFC: NOTICE MUST NOT
|
|
1146
|
+
* generate numerics).
|
|
1147
|
+
*/
|
|
1148
|
+
private async closeMultilineBatchToNick(
|
|
1149
|
+
open: {
|
|
1150
|
+
readonly id: string;
|
|
1151
|
+
readonly target: string;
|
|
1152
|
+
readonly command: 'PRIVMSG' | 'NOTICE';
|
|
1153
|
+
readonly entries: readonly MultilineEntry[];
|
|
1154
|
+
},
|
|
1155
|
+
maxBytes: number,
|
|
1156
|
+
ctx: ReturnType<typeof buildCtx>,
|
|
1157
|
+
): Promise<EffectType[]> {
|
|
1158
|
+
if (open.entries.length === 0) {
|
|
1159
|
+
return [];
|
|
1160
|
+
}
|
|
1161
|
+
|
|
1162
|
+
const bytes = multilineByteLength(open.entries);
|
|
1163
|
+
if (bytes > maxBytes) {
|
|
1164
|
+
return [
|
|
1165
|
+
Effect.send(ctx.connId, [
|
|
1166
|
+
{
|
|
1167
|
+
text: `:${ctx.serverName} FAIL BATCH MULTILINE_MAX_BYTES ${open.target} :Multi-line batch exceeds the ${maxBytes} byte limit`,
|
|
1168
|
+
},
|
|
1169
|
+
]),
|
|
1170
|
+
];
|
|
1171
|
+
}
|
|
1172
|
+
|
|
1173
|
+
const recipientConnId = await this.runtime.lookupNick(open.target);
|
|
1174
|
+
if (recipientConnId === null) {
|
|
1175
|
+
return this.nickNotFoundEffects(open, ctx);
|
|
1176
|
+
}
|
|
1177
|
+
const recipient = await this.runtime.getConnection(recipientConnId);
|
|
1178
|
+
if (recipient === null) {
|
|
1179
|
+
return this.nickNotFoundEffects(open, ctx);
|
|
1180
|
+
}
|
|
1181
|
+
|
|
1182
|
+
const hostmask = hostmaskOf(this.state) ?? '?';
|
|
1183
|
+
const batchId = ctx.ids.batchId();
|
|
1184
|
+
const fanout = buildMultilineFanout(open.target, open.command, open.entries, hostmask, batchId);
|
|
1185
|
+
const msgid = ctx.ids.nonce();
|
|
1186
|
+
recordMultilineMessage(ctx, open.target, open.command, fanout.joined, msgid);
|
|
1187
|
+
|
|
1188
|
+
let lines: RawLine[];
|
|
1189
|
+
if (recipient.caps.has('draft/multiline') && recipient.caps.has('batch')) {
|
|
1190
|
+
lines = [...fanout.batchedReplay];
|
|
1191
|
+
} else if (recipient.caps.has('message-tags')) {
|
|
1192
|
+
lines = [fanout.taggedJoined];
|
|
1193
|
+
} else {
|
|
1194
|
+
lines = [fanout.bareJoined];
|
|
1195
|
+
}
|
|
1196
|
+
|
|
1197
|
+
return [Effect.send(recipientConnId, lines)];
|
|
1198
|
+
}
|
|
1199
|
+
|
|
1200
|
+
/**
|
|
1201
|
+
* Builds the `401 ERR_NOSUCHNICK` effect for an offline nick-target batch
|
|
1202
|
+
* close. PRIVMSG surfaces the error; NOTICE returns nothing (the RFC
|
|
1203
|
+
* forbids numerics in response to NOTICE).
|
|
1204
|
+
*/
|
|
1205
|
+
private nickNotFoundEffects(
|
|
1206
|
+
open: { readonly target: string; readonly command: 'PRIVMSG' | 'NOTICE' },
|
|
1207
|
+
ctx: ReturnType<typeof buildCtx>,
|
|
1208
|
+
): EffectType[] {
|
|
1209
|
+
if (open.command === 'NOTICE') return [];
|
|
1210
|
+
const nick = ctx.connection.nick ?? '*';
|
|
1211
|
+
const code = Numerics.ERR_NOSUCHNICK.toString().padStart(3, '0');
|
|
1212
|
+
return [
|
|
1213
|
+
Effect.send(ctx.connId, [
|
|
1214
|
+
{ text: `:${ctx.serverName} ${code} ${nick} ${open.target} :No such nick/channel` },
|
|
1215
|
+
]),
|
|
1216
|
+
];
|
|
1217
|
+
}
|
|
1218
|
+
|
|
772
1219
|
private async routeChathistory(
|
|
773
1220
|
msg: IrcMessage,
|
|
774
1221
|
ctx: ReturnType<typeof buildCtx>,
|