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
|
@@ -0,0 +1,327 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure reducer for the IRCv3 `MONITOR` command.
|
|
3
|
+
*
|
|
4
|
+
* IRCv3 MONITOR spec <https://ircv3.net/specs/extensions/monitor-3.2.html>:
|
|
5
|
+
*
|
|
6
|
+
* - `MONITOR + <nick>{,<nick>}` — add targets to the watchlist.
|
|
7
|
+
* - `MONITOR - <nick>{,<nick>}` — remove targets from the watchlist.
|
|
8
|
+
* - `MONITOR C` — clear the watchlist.
|
|
9
|
+
* - `MONITOR L` — list the watchlist (`732` per line +
|
|
10
|
+
* `733` terminator).
|
|
11
|
+
* - `MONITOR S` — status: `730 RPL_MONONLINE` for the
|
|
12
|
+
* online subset, `731 RPL_MONOFFLINE`
|
|
13
|
+
* for the rest.
|
|
14
|
+
*
|
|
15
|
+
* Numerics:
|
|
16
|
+
* - `730 RPL_MONONLINE` `:<server> 730 <nick> :<online>{,<online>}`
|
|
17
|
+
* - `731 RPL_MONOFFLINE` `:<server> 731 <nick> :<offline>{,<offline>}`
|
|
18
|
+
* - `732 RPL_MONLIST` `:<server> 732 <nick> :<entry>`
|
|
19
|
+
* - `733 RPL_ENDOFMONLIST` `:<server> 733 <nick> :End of MONITOR list`
|
|
20
|
+
* - `734 ERR_MONLISTFULL` `:<server> 734 <nick> <limit> :<rejected>{,<rejected>}`
|
|
21
|
+
*
|
|
22
|
+
* The watchlist lives on {@link ConnectionState.monitorList} (folded nicks,
|
|
23
|
+
* case-insensitive membership). The reducer resolves each requested nick
|
|
24
|
+
* against the supplied `online` map (folded-keyed → display spelling); the
|
|
25
|
+
* actor builds that map from the nick registry before invoking the reducer,
|
|
26
|
+
* mirroring the {@link isonReducer} pattern.
|
|
27
|
+
*
|
|
28
|
+
* Asynchronous `730`/`731` pushes on online/offline transitions are emitted
|
|
29
|
+
* by the actor layer (it watches `ReserveNick` / `ReleaseNick` effects
|
|
30
|
+
* against the watchlists); the pure reducer only handles the request/reply
|
|
31
|
+
* subcommands above.
|
|
32
|
+
*/
|
|
33
|
+
|
|
34
|
+
import { Effect } from '../effects.js';
|
|
35
|
+
import type { Effect as EffectType, RawLine } from '../effects.js';
|
|
36
|
+
import { Numerics } from '../protocol/numerics.js';
|
|
37
|
+
import type { ConnectionState } from '../state/connection.js';
|
|
38
|
+
import type { Ctx, ReducerResult } from '../types.js';
|
|
39
|
+
|
|
40
|
+
/** Wire name of the IRCv3 MONITOR capability. */
|
|
41
|
+
export const MONITOR_CAP = 'monitor';
|
|
42
|
+
|
|
43
|
+
/** Default monitor cap (max watchlist size) advertised via `MONITOR=<n>`. */
|
|
44
|
+
export const DEFAULT_MONITOR_LIMIT = 30;
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Handles the IRCv3 `MONITOR` subcommands.
|
|
48
|
+
*
|
|
49
|
+
* Signature is bespoke (not {@link Reducer}) because the actor must resolve
|
|
50
|
+
* the requested nicks asynchronously before formatting — mirrors
|
|
51
|
+
* {@link isonReducer} / {@link whoisReducer}.
|
|
52
|
+
*
|
|
53
|
+
* @param online Folded-keyed → display-spelling map of currently
|
|
54
|
+
* online nicks (built by the actor from the nick
|
|
55
|
+
* registry).
|
|
56
|
+
* @param maxMonitor Server-wide cap on the per-connection watchlist size
|
|
57
|
+
* (advertised via `MONITOR=<n>` in `005`).
|
|
58
|
+
* @param msg Parsed `MONITOR <sub> [<args>]` message.
|
|
59
|
+
* @param ctx Per-invocation context.
|
|
60
|
+
*/
|
|
61
|
+
export function monitorReducer(
|
|
62
|
+
online: ReadonlyMap<string, string>,
|
|
63
|
+
maxMonitor: number,
|
|
64
|
+
msg: { params: readonly string[] },
|
|
65
|
+
ctx: Ctx,
|
|
66
|
+
): ReducerResult<ConnectionState> {
|
|
67
|
+
ctx.connection.lastSeen = ctx.clock.now();
|
|
68
|
+
|
|
69
|
+
const state = ctx.connection;
|
|
70
|
+
const nick = state.nick ?? '*';
|
|
71
|
+
const serverName = ctx.serverName;
|
|
72
|
+
|
|
73
|
+
const sub = (msg.params[0] ?? '').toUpperCase();
|
|
74
|
+
|
|
75
|
+
// No subcommand at all → 461 NEEDMOREPARAMS (the parser hands us an
|
|
76
|
+
// empty string when the sender wrote bare `MONITOR`). A non-empty but
|
|
77
|
+
// unknown subcommand (e.g. `MONITOR Z`) is also 461 with a different
|
|
78
|
+
// trailing string per the IRCv3 MONITOR dispatch table.
|
|
79
|
+
if (sub.length === 0) {
|
|
80
|
+
return {
|
|
81
|
+
state,
|
|
82
|
+
effects: [
|
|
83
|
+
Effect.send(ctx.connId, [
|
|
84
|
+
numericLine(
|
|
85
|
+
serverName,
|
|
86
|
+
Numerics.ERR_NEEDMOREPARAMS,
|
|
87
|
+
nick,
|
|
88
|
+
'MONITOR',
|
|
89
|
+
'Not enough parameters',
|
|
90
|
+
),
|
|
91
|
+
]),
|
|
92
|
+
],
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
switch (sub) {
|
|
97
|
+
case '+':
|
|
98
|
+
return handleAdd(state, online, maxMonitor, msg.params[1] ?? '', nick, serverName);
|
|
99
|
+
case '-':
|
|
100
|
+
return handleRemove(state, msg.params[1] ?? '');
|
|
101
|
+
case 'C':
|
|
102
|
+
return handleClear(state);
|
|
103
|
+
case 'L':
|
|
104
|
+
return handleList(state, nick, serverName);
|
|
105
|
+
case 'S':
|
|
106
|
+
return handleStatus(state, online, nick, serverName);
|
|
107
|
+
default:
|
|
108
|
+
return {
|
|
109
|
+
state,
|
|
110
|
+
effects: [
|
|
111
|
+
Effect.send(ctx.connId, [
|
|
112
|
+
numericLine(
|
|
113
|
+
serverName,
|
|
114
|
+
Numerics.ERR_NEEDMOREPARAMS,
|
|
115
|
+
nick,
|
|
116
|
+
'MONITOR',
|
|
117
|
+
'Unknown command',
|
|
118
|
+
),
|
|
119
|
+
]),
|
|
120
|
+
],
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// ---------------------------------------------------------------------------
|
|
126
|
+
// Subcommand handlers
|
|
127
|
+
// ---------------------------------------------------------------------------
|
|
128
|
+
|
|
129
|
+
function handleAdd(
|
|
130
|
+
state: ConnectionState,
|
|
131
|
+
online: ReadonlyMap<string, string>,
|
|
132
|
+
maxMonitor: number,
|
|
133
|
+
targetList: string,
|
|
134
|
+
nick: string,
|
|
135
|
+
serverName: string,
|
|
136
|
+
): ReducerResult<ConnectionState> {
|
|
137
|
+
if (targetList.length === 0) {
|
|
138
|
+
return {
|
|
139
|
+
state,
|
|
140
|
+
effects: [
|
|
141
|
+
Effect.send(state.id, [
|
|
142
|
+
numericLine(
|
|
143
|
+
serverName,
|
|
144
|
+
Numerics.ERR_NEEDMOREPARAMS,
|
|
145
|
+
nick,
|
|
146
|
+
'MONITOR',
|
|
147
|
+
'Not enough parameters',
|
|
148
|
+
),
|
|
149
|
+
]),
|
|
150
|
+
],
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const watchlist = ensureWatchlist(state);
|
|
155
|
+
|
|
156
|
+
const added: { folded: string; display: string }[] = [];
|
|
157
|
+
const rejected: string[] = [];
|
|
158
|
+
for (const requested of splitNicks(targetList)) {
|
|
159
|
+
const folded = requested.toLowerCase();
|
|
160
|
+
if (watchlist.has(folded)) continue; // already monitored; no output
|
|
161
|
+
if (watchlist.size >= maxMonitor) {
|
|
162
|
+
rejected.push(requested);
|
|
163
|
+
continue;
|
|
164
|
+
}
|
|
165
|
+
watchlist.add(folded);
|
|
166
|
+
// For the display spelling prefer the online map (canonical) and fall
|
|
167
|
+
// back to the requester spelling when the nick is offline.
|
|
168
|
+
const display = online.get(folded) ?? requested;
|
|
169
|
+
added.push({ folded, display });
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const effects: EffectType[] = [];
|
|
173
|
+
|
|
174
|
+
const onlineDisplays = added.filter((a) => online.has(a.folded)).map((a) => a.display);
|
|
175
|
+
const offlineDisplays = added.filter((a) => !online.has(a.folded)).map((a) => a.display);
|
|
176
|
+
|
|
177
|
+
if (onlineDisplays.length > 0) {
|
|
178
|
+
effects.push(
|
|
179
|
+
Effect.send(state.id, [
|
|
180
|
+
monLine(Numerics.RPL_MONONLINE, serverName, nick, onlineDisplays.join(',')),
|
|
181
|
+
]),
|
|
182
|
+
);
|
|
183
|
+
}
|
|
184
|
+
if (offlineDisplays.length > 0) {
|
|
185
|
+
effects.push(
|
|
186
|
+
Effect.send(state.id, [
|
|
187
|
+
monLine(Numerics.RPL_MONOFFLINE, serverName, nick, offlineDisplays.join(',')),
|
|
188
|
+
]),
|
|
189
|
+
);
|
|
190
|
+
}
|
|
191
|
+
if (rejected.length > 0) {
|
|
192
|
+
effects.push(
|
|
193
|
+
Effect.send(state.id, [monListFullLine(serverName, nick, maxMonitor, rejected.join(','))]),
|
|
194
|
+
);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
return { state, effects };
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function handleRemove(state: ConnectionState, targetList: string): ReducerResult<ConnectionState> {
|
|
201
|
+
if (state.monitorList === undefined) return { state, effects: [] };
|
|
202
|
+
if (targetList.length === 0) return { state, effects: [] };
|
|
203
|
+
for (const requested of splitNicks(targetList)) {
|
|
204
|
+
state.monitorList.delete(requested.toLowerCase());
|
|
205
|
+
}
|
|
206
|
+
return { state, effects: [] };
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function handleClear(state: ConnectionState): ReducerResult<ConnectionState> {
|
|
210
|
+
// Allocate the set on first clear so callers observe a stable post-state
|
|
211
|
+
// (size 0) regardless of whether the watchlist previously existed.
|
|
212
|
+
if (state.monitorList === undefined) {
|
|
213
|
+
state.monitorList = new Set<string>();
|
|
214
|
+
} else {
|
|
215
|
+
state.monitorList.clear();
|
|
216
|
+
}
|
|
217
|
+
return { state, effects: [] };
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function handleList(
|
|
221
|
+
state: ConnectionState,
|
|
222
|
+
nick: string,
|
|
223
|
+
serverName: string,
|
|
224
|
+
): ReducerResult<ConnectionState> {
|
|
225
|
+
const effects: EffectType[] = [];
|
|
226
|
+
if (state.monitorList !== undefined) {
|
|
227
|
+
for (const entry of state.monitorList) {
|
|
228
|
+
effects.push(Effect.send(state.id, [monLine(Numerics.RPL_MONLIST, serverName, nick, entry)]));
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
effects.push(
|
|
232
|
+
Effect.send(state.id, [
|
|
233
|
+
monLine(Numerics.RPL_ENDOFMONLIST, serverName, nick, 'End of MONITOR list'),
|
|
234
|
+
]),
|
|
235
|
+
);
|
|
236
|
+
return { state, effects };
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function handleStatus(
|
|
240
|
+
state: ConnectionState,
|
|
241
|
+
online: ReadonlyMap<string, string>,
|
|
242
|
+
nick: string,
|
|
243
|
+
serverName: string,
|
|
244
|
+
): ReducerResult<ConnectionState> {
|
|
245
|
+
if (state.monitorList === undefined || state.monitorList.size === 0) {
|
|
246
|
+
return { state, effects: [] };
|
|
247
|
+
}
|
|
248
|
+
const onlineDisplays: string[] = [];
|
|
249
|
+
const offlineDisplays: string[] = [];
|
|
250
|
+
for (const folded of state.monitorList) {
|
|
251
|
+
const display = online.get(folded);
|
|
252
|
+
if (display !== undefined) {
|
|
253
|
+
onlineDisplays.push(display);
|
|
254
|
+
} else {
|
|
255
|
+
offlineDisplays.push(folded);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
const effects: EffectType[] = [];
|
|
259
|
+
if (onlineDisplays.length > 0) {
|
|
260
|
+
effects.push(
|
|
261
|
+
Effect.send(state.id, [
|
|
262
|
+
monLine(Numerics.RPL_MONONLINE, serverName, nick, onlineDisplays.join(',')),
|
|
263
|
+
]),
|
|
264
|
+
);
|
|
265
|
+
}
|
|
266
|
+
if (offlineDisplays.length > 0) {
|
|
267
|
+
effects.push(
|
|
268
|
+
Effect.send(state.id, [
|
|
269
|
+
monLine(Numerics.RPL_MONOFFLINE, serverName, nick, offlineDisplays.join(',')),
|
|
270
|
+
]),
|
|
271
|
+
);
|
|
272
|
+
}
|
|
273
|
+
return { state, effects };
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// ---------------------------------------------------------------------------
|
|
277
|
+
// Helpers
|
|
278
|
+
// ---------------------------------------------------------------------------
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* Returns the connection's watchlist, lazily allocating it on first use.
|
|
282
|
+
* Existing persisted state migrates without a schema bump (the field stays
|
|
283
|
+
* optional; reducers that pre-date MONITOR never touch it).
|
|
284
|
+
*/
|
|
285
|
+
function ensureWatchlist(state: ConnectionState): Set<string> {
|
|
286
|
+
if (state.monitorList === undefined) {
|
|
287
|
+
state.monitorList = new Set<string>();
|
|
288
|
+
}
|
|
289
|
+
return state.monitorList;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/** Splits a comma-separated nick list, dropping empty entries. */
|
|
293
|
+
function splitNicks(csv: string): string[] {
|
|
294
|
+
return csv
|
|
295
|
+
.split(',')
|
|
296
|
+
.map((s) => s.trim())
|
|
297
|
+
.filter((s) => s.length > 0);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/** `:<server> <code> <nick> :<trailing>` — used by 730/731/732/733. */
|
|
301
|
+
function monLine(code: number, serverName: string, nick: string, trailing: string): RawLine {
|
|
302
|
+
const codeStr = code.toString().padStart(3, '0');
|
|
303
|
+
return { text: `:${serverName} ${codeStr} ${nick} :${trailing}` };
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/** `:<server> 734 <nick> <limit> :<rejected>` (ERR_MONLISTFULL). */
|
|
307
|
+
function monListFullLine(
|
|
308
|
+
serverName: string,
|
|
309
|
+
nick: string,
|
|
310
|
+
limit: number,
|
|
311
|
+
rejected: string,
|
|
312
|
+
): RawLine {
|
|
313
|
+
const codeStr = Numerics.ERR_MONLISTFULL.toString().padStart(3, '0');
|
|
314
|
+
return { text: `:${serverName} ${codeStr} ${nick} ${limit} :${rejected}` };
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/** `:<server> <code> <nick> <cmd> :<trailing>` — used by 461-style errors. */
|
|
318
|
+
function numericLine(
|
|
319
|
+
serverName: string,
|
|
320
|
+
code: number,
|
|
321
|
+
nick: string,
|
|
322
|
+
cmd: string,
|
|
323
|
+
trailing: string,
|
|
324
|
+
): RawLine {
|
|
325
|
+
const codeStr = code.toString().padStart(3, '0');
|
|
326
|
+
return { text: `:${serverName} ${codeStr} ${nick} ${cmd} :${trailing}` };
|
|
327
|
+
}
|
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure reducer for IRCv3 `draft/multiline` (multi-line `BATCH`).
|
|
3
|
+
*
|
|
4
|
+
* Spec: https://ircv3.net/specs/extensions/multiline
|
|
5
|
+
*
|
|
6
|
+
* A cap-enabled client opens a multi-line batch with
|
|
7
|
+
* `BATCH +id draft/multiline :<target>`
|
|
8
|
+
* followed by N `PRIVMSG`/`NOTICE` lines addressed to the same target, then
|
|
9
|
+
* closes it with `BATCH -id`. The server concatenates the per-line trailing
|
|
10
|
+
* parameters with `\n`, enforces the advertised byte budget, and fans the
|
|
11
|
+
* result out:
|
|
12
|
+
*
|
|
13
|
+
* - to peers that also negotiated `draft/multiline`: a replayed
|
|
14
|
+
* `BATCH +newid draft/multiline :<target>` containing the original
|
|
15
|
+
* per-line `PRIVMSG`/`NOTICE` messages;
|
|
16
|
+
* - to everyone else: a single joined `PRIVMSG`/`NOTICE` carrying the
|
|
17
|
+
* `+draft/multiline` client tag so message-tags-aware (but not
|
|
18
|
+
* multiline-aware) clients can recognise the joined form.
|
|
19
|
+
*
|
|
20
|
+
* This module owns the pure finalization step (concatenation, byte-budget
|
|
21
|
+
* enforcement, fanout construction). The stateful accumulation of the inner
|
|
22
|
+
* lines — buffering `PRIVMSG`/`NOTICE` between the `BATCH +` and `BATCH -`
|
|
23
|
+
* markers — lives in the actor layer, which calls
|
|
24
|
+
* {@link finalizeMultilineBatch} once the batch closes.
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
import {
|
|
28
|
+
DEFAULT_MULTILINE_MAX_BYTES,
|
|
29
|
+
MULTILINE_CAP_NAME,
|
|
30
|
+
multilineCapValue,
|
|
31
|
+
} from '../caps/capabilities.js';
|
|
32
|
+
import { Effect } from '../effects.js';
|
|
33
|
+
import type { Effect as EffectType, RawLine } from '../effects.js';
|
|
34
|
+
import { hostmaskOf } from '../state/connection.js';
|
|
35
|
+
import type { Ctx } from '../types.js';
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Wire name of the IRCv3 `draft/multiline` capability. Aliased from
|
|
39
|
+
* {@link MULTILINE_CAP_NAME} (the canonical home in the cap data table) for
|
|
40
|
+
* ergonomic use inside the reducer.
|
|
41
|
+
*/
|
|
42
|
+
export const MULTILINE_CAP = MULTILINE_CAP_NAME;
|
|
43
|
+
|
|
44
|
+
export { DEFAULT_MULTILINE_MAX_BYTES, multilineCapValue };
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* One accumulated inner line of a multi-line batch. The actor collects these
|
|
48
|
+
* as the client streams `PRIVMSG`/`NOTICE` lines between the batch markers,
|
|
49
|
+
* then hands the list to {@link finalizeMultilineBatch}. Only the trailing
|
|
50
|
+
* parameter (message text) is stored per line: the spec requires every inner
|
|
51
|
+
* line of a batch to share both the command and the target, so those live on
|
|
52
|
+
* the batch as a whole and are passed to the reducer separately.
|
|
53
|
+
*/
|
|
54
|
+
export interface MultilineEntry {
|
|
55
|
+
/** The trailing parameter (message text) of the inner line. */
|
|
56
|
+
readonly text: string;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Result of {@link finalizeMultilineBatch}: either accepted or rejected. */
|
|
60
|
+
export type FinalizeMultilineResult =
|
|
61
|
+
| { readonly ok: true; readonly effects: EffectType[] }
|
|
62
|
+
| { readonly ok: false; readonly effects: EffectType[] };
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Concatenates the per-line texts of a multi-line batch with `\n`, per the
|
|
66
|
+
* IRCv3 spec. The joined value is what legacy (non-multiline) peers receive
|
|
67
|
+
* as the trailing parameter of the single relayed PRIVMSG/NOTICE.
|
|
68
|
+
*/
|
|
69
|
+
export function joinMultiline(entries: readonly MultilineEntry[]): string {
|
|
70
|
+
return entries.map((e) => e.text).join('\n');
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Returns the byte length of the joined multi-line content as the byte budget
|
|
75
|
+
* sees it: the sum of every entry's UTF-8 byte length plus one byte per `\n`
|
|
76
|
+
* separator. This is the quantity compared against `draft/multiline=<max>`.
|
|
77
|
+
*/
|
|
78
|
+
export function multilineByteLength(entries: readonly MultilineEntry[]): number {
|
|
79
|
+
if (entries.length === 0) return 0;
|
|
80
|
+
let total = 0;
|
|
81
|
+
for (const entry of entries) {
|
|
82
|
+
total += utf8ByteLength(entry.text);
|
|
83
|
+
}
|
|
84
|
+
// `(n - 1)` separators join `n` entries.
|
|
85
|
+
total += entries.length - 1;
|
|
86
|
+
return total;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** Shared `TextEncoder` for UTF-8 byte-length accounting (Web standard). */
|
|
90
|
+
const TEXT_ENCODER = new TextEncoder();
|
|
91
|
+
|
|
92
|
+
/** Returns the UTF-8 byte length of `s` (the wire encoding for IRC content). */
|
|
93
|
+
function utf8ByteLength(s: string): number {
|
|
94
|
+
return TEXT_ENCODER.encode(s).length;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** Resolves the sender's hostmask, falling back to nick then `?`. */
|
|
98
|
+
function senderHostmask(conn: Ctx['connection']): string {
|
|
99
|
+
return hostmaskOf(conn) ?? conn.nick ?? '?';
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Pre-built wire lines for a finalized multi-line batch, independent of the
|
|
104
|
+
* delivery mechanism (channel `Broadcast` vs. nick-target `Send`). The caller
|
|
105
|
+
* picks the appropriate variant per recipient based on the recipient's caps:
|
|
106
|
+
*
|
|
107
|
+
* - `draft/multiline` + `batch` → `batchedReplay` (replayed BATCH frame).
|
|
108
|
+
* - `message-tags` only → `taggedJoined` (joined PRIVMSG with
|
|
109
|
+
* `+draft/multiline` client tag).
|
|
110
|
+
* - neither → `bareJoined` (joined PRIVMSG, no tag).
|
|
111
|
+
*/
|
|
112
|
+
export interface MultilineFanout {
|
|
113
|
+
/** Replayed `BATCH +id draft/multiline <target>` … `BATCH -id`. */
|
|
114
|
+
readonly batchedReplay: RawLine[];
|
|
115
|
+
/** Joined PRIVMSG/NOTICE carrying the `+draft/multiline` client tag. */
|
|
116
|
+
readonly taggedJoined: RawLine;
|
|
117
|
+
/** Joined PRIVMSG/NOTICE with no tag prefix (for legacy peers). */
|
|
118
|
+
readonly bareJoined: RawLine;
|
|
119
|
+
/** The `\n`-joined trailing parameter (for chathistory recording). */
|
|
120
|
+
readonly joined: string;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Builds the wire-line variants for a finalized multi-line batch. Pure: no
|
|
125
|
+
* context, no effects — just string construction. Shared by the channel
|
|
126
|
+
* broadcast path ({@link finalizeMultilineBatch}) and the actor-side nick
|
|
127
|
+
* target path so both emit identical wire forms.
|
|
128
|
+
*
|
|
129
|
+
* @param target The batch target (channel name or nick).
|
|
130
|
+
* @param command The inner-line command (`PRIVMSG` or `NOTICE`).
|
|
131
|
+
* @param entries The accumulated inner lines in arrival order.
|
|
132
|
+
* @param hostmask The sender's `nick!user@host` (or fallback).
|
|
133
|
+
* @param batchId A fresh batch reference id for the replayed BATCH frame.
|
|
134
|
+
*/
|
|
135
|
+
export function buildMultilineFanout(
|
|
136
|
+
target: string,
|
|
137
|
+
command: 'PRIVMSG' | 'NOTICE',
|
|
138
|
+
entries: readonly MultilineEntry[],
|
|
139
|
+
hostmask: string,
|
|
140
|
+
batchId: string,
|
|
141
|
+
): MultilineFanout {
|
|
142
|
+
const joined = joinMultiline(entries);
|
|
143
|
+
|
|
144
|
+
const taggedJoined: RawLine = {
|
|
145
|
+
text: `@+draft/multiline :${hostmask} ${command} ${target} :${joined}`,
|
|
146
|
+
};
|
|
147
|
+
const bareJoined: RawLine = {
|
|
148
|
+
text: `:${hostmask} ${command} ${target} :${joined}`,
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
const inner: RawLine[] = entries.map(
|
|
152
|
+
(e) => ({ text: `:${hostmask} ${command} ${target} :${e.text}` }) as RawLine,
|
|
153
|
+
);
|
|
154
|
+
const batchedReplay: RawLine[] = [
|
|
155
|
+
{ text: `BATCH +${batchId} draft/multiline ${target}` },
|
|
156
|
+
...inner,
|
|
157
|
+
{ text: `BATCH -${batchId}` },
|
|
158
|
+
];
|
|
159
|
+
|
|
160
|
+
return { batchedReplay, taggedJoined, bareJoined, joined };
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Finalizes a closed multi-line batch: enforces the byte budget and builds
|
|
165
|
+
* the fanout effects for both multiline-capable and legacy peers.
|
|
166
|
+
*
|
|
167
|
+
* On success the joined body is also recorded into the bound
|
|
168
|
+
* {@link MessageStore} (if any) as a single entry carrying the
|
|
169
|
+
* `+draft/multiline` client tag, so `CHATHISTORY` playback returns exactly
|
|
170
|
+
* one entry per batch.
|
|
171
|
+
*
|
|
172
|
+
* @param target The batch's relay target (channel name). The actor
|
|
173
|
+
* validates that every inner line addressed this same target
|
|
174
|
+
* before calling.
|
|
175
|
+
* @param command The inner-line command (`PRIVMSG` or `NOTICE`); the actor
|
|
176
|
+
* guarantees every inner line used the same one.
|
|
177
|
+
* @param entries The accumulated inner lines in arrival order.
|
|
178
|
+
* @param maxBytes The advertised byte budget (`draft/multiline=<n>`).
|
|
179
|
+
* @param ctx Per-invocation context.
|
|
180
|
+
* @returns `{ ok: true, effects }` on success; `{ ok: false, effects }`
|
|
181
|
+
* when the joined content exceeds `maxBytes` (the effects
|
|
182
|
+
* carry the `FAIL` rejection back to the sender).
|
|
183
|
+
*/
|
|
184
|
+
export function finalizeMultilineBatch(
|
|
185
|
+
target: string,
|
|
186
|
+
command: 'PRIVMSG' | 'NOTICE',
|
|
187
|
+
entries: readonly MultilineEntry[],
|
|
188
|
+
maxBytes: number,
|
|
189
|
+
ctx: Ctx,
|
|
190
|
+
): FinalizeMultilineResult {
|
|
191
|
+
if (entries.length === 0) {
|
|
192
|
+
return { ok: true, effects: [] };
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const bytes = multilineByteLength(entries);
|
|
196
|
+
if (bytes > maxBytes) {
|
|
197
|
+
return {
|
|
198
|
+
ok: false,
|
|
199
|
+
effects: [
|
|
200
|
+
Effect.send(ctx.connId, [
|
|
201
|
+
{
|
|
202
|
+
text: `:${ctx.serverName} FAIL BATCH MULTILINE_MAX_BYTES ${target} :Multi-line batch exceeds the ${maxBytes} byte limit`,
|
|
203
|
+
},
|
|
204
|
+
]),
|
|
205
|
+
],
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const hostmask = senderHostmask(ctx.connection);
|
|
210
|
+
const batchId = ctx.ids.batchId();
|
|
211
|
+
const fanout = buildMultilineFanout(target, command, entries, hostmask, batchId);
|
|
212
|
+
const msgid = ctx.ids.nonce();
|
|
213
|
+
recordMultilineMessage(ctx, target, command, fanout.joined, msgid);
|
|
214
|
+
|
|
215
|
+
const effects: EffectType[] = [
|
|
216
|
+
Effect.broadcast(
|
|
217
|
+
target,
|
|
218
|
+
[fanout.taggedJoined],
|
|
219
|
+
ctx.connId,
|
|
220
|
+
MULTILINE_CAP,
|
|
221
|
+
fanout.batchedReplay,
|
|
222
|
+
),
|
|
223
|
+
];
|
|
224
|
+
|
|
225
|
+
return { ok: true, effects };
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Records one finalized multi-line batch into the bound {@link MessageStore}
|
|
230
|
+
* (when present) as a single chathistory entry. The joined body is stored
|
|
231
|
+
* verbatim, the assigned `msgid` is the chathistory pivot, and the
|
|
232
|
+
* `+draft/multiline` client tag is attached so the chathistory replay can
|
|
233
|
+
* mark the line as originating from a multi-line batch. Shared by the
|
|
234
|
+
* channel broadcast path ({@link finalizeMultilineBatch}) and the actor-side
|
|
235
|
+
* nick-target path.
|
|
236
|
+
*/
|
|
237
|
+
export function recordMultilineMessage(
|
|
238
|
+
ctx: Ctx,
|
|
239
|
+
target: string,
|
|
240
|
+
command: 'PRIVMSG' | 'NOTICE',
|
|
241
|
+
joined: string,
|
|
242
|
+
msgid: string,
|
|
243
|
+
): void {
|
|
244
|
+
if (ctx.messages === undefined) return;
|
|
245
|
+
ctx.messages.record({
|
|
246
|
+
msgid,
|
|
247
|
+
time: ctx.clock.now(),
|
|
248
|
+
chan: target.toLowerCase(),
|
|
249
|
+
command,
|
|
250
|
+
nick: (ctx.connection.nick as string) ?? '?',
|
|
251
|
+
user: ctx.connection.user as string,
|
|
252
|
+
host: ctx.connection.host as string,
|
|
253
|
+
text: joined,
|
|
254
|
+
clientTags: ['+draft/multiline'],
|
|
255
|
+
});
|
|
256
|
+
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure helpers for IRCv3 `draft/pre-away` (persistent away status).
|
|
3
|
+
*
|
|
4
|
+
* Spec (work-in-progress): https://github.com/ircv3/ircv3-specifications/pull/415
|
|
5
|
+
*
|
|
6
|
+
* Flow:
|
|
7
|
+
* - The {@link awayReducer} sets/clears {@link ConnectionState.away} as
|
|
8
|
+
* usual. When an {@link AwayStore} is bound AND the connection is
|
|
9
|
+
* identified (has an `account`), the change is mirrored into the
|
|
10
|
+
* per-`account` record via {@link persistAway} (on set) or
|
|
11
|
+
* {@link clearPersistedAway} (on unset).
|
|
12
|
+
* - On SASL identify, the actor calls {@link replayPersistedAway} so a
|
|
13
|
+
* fresh connection's `away` is restored from the persisted reason and
|
|
14
|
+
* `306 RPL_NOWAWAY` is emitted as if the client had sent
|
|
15
|
+
* `AWAY :reason` themselves.
|
|
16
|
+
*
|
|
17
|
+
* Mirrors the {@link ReadMarkerStore} plumbing from the read-marker cap.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import { PRE_AWAY_CAP_NAME } from '../caps/capabilities.js';
|
|
21
|
+
import type { AwayStore } from '../ports.js';
|
|
22
|
+
import { Numerics } from '../protocol/numerics.js';
|
|
23
|
+
import type { ConnectionState } from '../state/connection.js';
|
|
24
|
+
import type { Ctx } from '../types.js';
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Wire name of the IRCv3 `draft/pre-away` capability. Aliased from
|
|
28
|
+
* {@link PRE_AWAY_CAP_NAME} (the canonical home in the cap data table)
|
|
29
|
+
* for ergonomic use inside the reducer.
|
|
30
|
+
*/
|
|
31
|
+
export const PRE_AWAY_CAP = PRE_AWAY_CAP_NAME;
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Records the away reason for the connection's account and stamps the
|
|
35
|
+
* in-memory {@link ConnectionState.away} so the current session tracks
|
|
36
|
+
* the new reason.
|
|
37
|
+
*
|
|
38
|
+
* Mirrors {@link persistReadMarker}: the store write is gated on both an
|
|
39
|
+
* {@link AwayStore} being bound AND the connection being identified.
|
|
40
|
+
* Unidentified connections still get the in-memory write so the session's
|
|
41
|
+
* own view (numerics, WHO/WHOIS) stays consistent.
|
|
42
|
+
*/
|
|
43
|
+
export function persistAway(ctx: Ctx, reason: string): void {
|
|
44
|
+
ctx.connection.away = reason;
|
|
45
|
+
const store = ctx.away;
|
|
46
|
+
const account = ctx.connection.account;
|
|
47
|
+
if (store !== undefined && account !== undefined) {
|
|
48
|
+
store.set(account, reason);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Clears the persisted away reason for the connection's account and the
|
|
54
|
+
* in-memory {@link ConnectionState.away}.
|
|
55
|
+
*
|
|
56
|
+
* Mirrors {@link persistAway}'s gating: a store write (delete) is gated
|
|
57
|
+
* on both a store being bound AND the connection being identified. The
|
|
58
|
+
* in-memory clear is unconditional so the session's own view stays
|
|
59
|
+
* consistent regardless of identification.
|
|
60
|
+
*/
|
|
61
|
+
export function clearPersistedAway(ctx: Ctx): void {
|
|
62
|
+
// biome-ignore lint/performance/noDelete: exactOptionalPropertyTypes forbids `= undefined`.
|
|
63
|
+
delete ctx.connection.away;
|
|
64
|
+
const store = ctx.away;
|
|
65
|
+
const account = ctx.connection.account;
|
|
66
|
+
if (store !== undefined && account !== undefined) {
|
|
67
|
+
store.clear(account);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Restores a fresh connection's {@link ConnectionState.away} from the
|
|
73
|
+
* persisted reason for `account`. Called at SASL identify so a reconnect
|
|
74
|
+
* resumes the user's away state.
|
|
75
|
+
*
|
|
76
|
+
* Existing in-memory `away` is preserved: a session may have already set
|
|
77
|
+
* a different reason, and the session's own value wins. Returns the
|
|
78
|
+
* persisted reason (or `undefined`) so the caller can emit the
|
|
79
|
+
* `306 RPL_NOWAWAY` numeric only when the replay actually applied a value.
|
|
80
|
+
*/
|
|
81
|
+
export function replayPersistedAway(
|
|
82
|
+
conn: ConnectionState,
|
|
83
|
+
store: AwayStore,
|
|
84
|
+
account: string,
|
|
85
|
+
): string | undefined {
|
|
86
|
+
if (conn.away !== undefined) return undefined;
|
|
87
|
+
const reason = store.get(account);
|
|
88
|
+
if (reason === undefined) return undefined;
|
|
89
|
+
conn.away = reason;
|
|
90
|
+
return reason;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Builds the `306 RPL_NOWAWAY` line emitted after a successful away
|
|
95
|
+
* replay at identify time. Mirrors the line the {@link awayReducer}
|
|
96
|
+
* emits when a client sets `AWAY :reason` themselves, so the
|
|
97
|
+
* `draft/pre-away` replay is wire-identical to a direct AWAY set as far
|
|
98
|
+
* as the identified user is concerned.
|
|
99
|
+
*
|
|
100
|
+
* The companion `away-notify` broadcast (`:<hostmask> AWAY :<reason>` to
|
|
101
|
+
* cap-enabled peers in shared channels) is intentionally NOT emitted
|
|
102
|
+
* here: at SASL identify time the connection has not yet joined any
|
|
103
|
+
* channels, so the broadcast would have no audience. Peers learn the
|
|
104
|
+
* away state through the normal `AWAY` / `WHO` / `WHOIS` queries once
|
|
105
|
+
* the user joins.
|
|
106
|
+
*/
|
|
107
|
+
export function nowAwayLine(conn: ConnectionState, serverName: string): { text: string } {
|
|
108
|
+
const nick = conn.nick ?? '*';
|
|
109
|
+
return {
|
|
110
|
+
text: `:${serverName} ${Numerics.RPL_NOWAWAY.toString().padStart(3, '0')} ${nick} :You have been marked as being away`,
|
|
111
|
+
};
|
|
112
|
+
}
|