serverless-ircd 0.6.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.
Files changed (247) hide show
  1. package/.github/workflows/ci.yml +6 -1
  2. package/.gitmodules +3 -0
  3. package/CHANGELOG.md +214 -0
  4. package/README.md +110 -23
  5. package/apps/aws-stack/package.json +1 -1
  6. package/apps/cf-tcp-container/package.json +1 -1
  7. package/apps/cf-worker/package.json +1 -1
  8. package/apps/cf-worker/src/origin-allowlist.ts +99 -0
  9. package/apps/cf-worker/src/worker.ts +89 -9
  10. package/apps/cf-worker/tests/fixtures/web-dist/app/index.html +18 -0
  11. package/apps/cf-worker/tests/fixtures/web-dist/index.html +18 -0
  12. package/apps/cf-worker/tests/origin-allowlist.test.ts +101 -0
  13. package/apps/cf-worker/tests/smoke.test.ts +28 -2
  14. package/apps/cf-worker/tests/ws-origin.test.ts +89 -0
  15. package/apps/cf-worker/wrangler.test.toml +23 -0
  16. package/apps/cf-worker/wrangler.toml +54 -0
  17. package/apps/local-cli/package.json +1 -1
  18. package/apps/web/landing/index.html +217 -0
  19. package/apps/web/package.json +28 -0
  20. package/apps/web/scripts/build.mjs +125 -0
  21. package/apps/web/src/build-env.ts +43 -0
  22. package/apps/web/src/config-schema.ts +138 -0
  23. package/apps/web/static/config.json +28 -0
  24. package/apps/web/static/config.prod.json +28 -0
  25. package/apps/web/static/config.staging.json +28 -0
  26. package/apps/web/tests/build-env.test.ts +63 -0
  27. package/apps/web/tests/build-smoke.test.ts +103 -0
  28. package/apps/web/tests/config-schema.test.ts +432 -0
  29. package/apps/web/tests/workspace.test.ts +12 -0
  30. package/apps/web/tsconfig.json +10 -0
  31. package/apps/web/upstream/.browserslistrc +3 -0
  32. package/apps/web/upstream/.editorconfig +9 -0
  33. package/apps/web/upstream/.eslintignore +3 -0
  34. package/apps/web/upstream/.eslintrc.js +90 -0
  35. package/apps/web/upstream/.github/FUNDING.yml +12 -0
  36. package/apps/web/upstream/.github/ISSUE_TEMPLATE/bug_report.md +38 -0
  37. package/apps/web/upstream/.github/ISSUE_TEMPLATE/feature_request.md +20 -0
  38. package/apps/web/upstream/.github/workflows/push.yml +26 -0
  39. package/apps/web/upstream/.github/workflows/release.yml +33 -0
  40. package/apps/web/upstream/.github/workflows/release_manual.yml +55 -0
  41. package/apps/web/upstream/.prettierrc.js +9 -0
  42. package/apps/web/upstream/.stylelintignore +5 -0
  43. package/apps/web/upstream/.stylelintrc.js +21 -0
  44. package/apps/web/upstream/LICENSE +201 -0
  45. package/apps/web/upstream/README.md +76 -0
  46. package/apps/web/upstream/babel.config.js +25 -0
  47. package/apps/web/upstream/crowdin.yml +3 -0
  48. package/apps/web/upstream/index.html +35 -0
  49. package/apps/web/upstream/jest.config.js +7 -0
  50. package/apps/web/upstream/jsdoc.json +22 -0
  51. package/apps/web/upstream/package.json +80 -0
  52. package/apps/web/upstream/scripts/pre-commit +43 -0
  53. package/apps/web/upstream/src/components/App.vue +516 -0
  54. package/apps/web/upstream/src/components/AppSettings.vue +587 -0
  55. package/apps/web/upstream/src/components/AutoComplete.vue +284 -0
  56. package/apps/web/upstream/src/components/Avatar.vue +105 -0
  57. package/apps/web/upstream/src/components/AwayStatusIndicator.vue +77 -0
  58. package/apps/web/upstream/src/components/BufferKey.vue +121 -0
  59. package/apps/web/upstream/src/components/BufferSettings.vue +97 -0
  60. package/apps/web/upstream/src/components/Captcha.vue +85 -0
  61. package/apps/web/upstream/src/components/ChannelBanlist.vue +96 -0
  62. package/apps/web/upstream/src/components/ChannelInfo.vue +159 -0
  63. package/apps/web/upstream/src/components/ChannelInvitelist.vue +276 -0
  64. package/apps/web/upstream/src/components/ChannelList.vue +345 -0
  65. package/apps/web/upstream/src/components/Container.vue +401 -0
  66. package/apps/web/upstream/src/components/ContainerHeader.vue +522 -0
  67. package/apps/web/upstream/src/components/ControlInput.vue +1016 -0
  68. package/apps/web/upstream/src/components/LoadingAnimation.vue +104 -0
  69. package/apps/web/upstream/src/components/MediaViewer.vue +199 -0
  70. package/apps/web/upstream/src/components/MessageInfo.vue +192 -0
  71. package/apps/web/upstream/src/components/MessageList.vue +1028 -0
  72. package/apps/web/upstream/src/components/MessageListAvatar.vue +20 -0
  73. package/apps/web/upstream/src/components/MessageListMessageCompact.vue +361 -0
  74. package/apps/web/upstream/src/components/MessageListMessageInline.vue +257 -0
  75. package/apps/web/upstream/src/components/MessageListMessageModern.vue +430 -0
  76. package/apps/web/upstream/src/components/NetworkSettings.vue +528 -0
  77. package/apps/web/upstream/src/components/Nicklist.vue +309 -0
  78. package/apps/web/upstream/src/components/NicklistUser.vue +181 -0
  79. package/apps/web/upstream/src/components/NotConnected.vue +271 -0
  80. package/apps/web/upstream/src/components/SelfUser.vue +283 -0
  81. package/apps/web/upstream/src/components/ServerSelector.vue +162 -0
  82. package/apps/web/upstream/src/components/ServerView.vue +126 -0
  83. package/apps/web/upstream/src/components/SettingsAdvanced.vue +232 -0
  84. package/apps/web/upstream/src/components/SettingsAliases.vue +121 -0
  85. package/apps/web/upstream/src/components/Sidebar.vue +429 -0
  86. package/apps/web/upstream/src/components/SidebarAboutBuffer.vue +288 -0
  87. package/apps/web/upstream/src/components/SidebarState.vue +145 -0
  88. package/apps/web/upstream/src/components/StartupError.vue +35 -0
  89. package/apps/web/upstream/src/components/StateBrowser.vue +420 -0
  90. package/apps/web/upstream/src/components/StateBrowserBuffer.vue +124 -0
  91. package/apps/web/upstream/src/components/StateBrowserNetwork.vue +823 -0
  92. package/apps/web/upstream/src/components/StateBrowserUsermenu.vue +162 -0
  93. package/apps/web/upstream/src/components/TypingStatusIndicator.vue +34 -0
  94. package/apps/web/upstream/src/components/TypingUsersList.vue +56 -0
  95. package/apps/web/upstream/src/components/UrlEmbed.vue +149 -0
  96. package/apps/web/upstream/src/components/UserBox.vue +728 -0
  97. package/apps/web/upstream/src/components/inputtools/Emoji.vue +67 -0
  98. package/apps/web/upstream/src/components/inputtools/TextStyle.vue +142 -0
  99. package/apps/web/upstream/src/components/startups/CommonLayout.vue +208 -0
  100. package/apps/web/upstream/src/components/startups/CustomServer.vue +495 -0
  101. package/apps/web/upstream/src/components/startups/KiwiBnc.vue +361 -0
  102. package/apps/web/upstream/src/components/startups/Personal.vue +317 -0
  103. package/apps/web/upstream/src/components/startups/Welcome.vue +544 -0
  104. package/apps/web/upstream/src/components/startups/ZncLogin.vue +229 -0
  105. package/apps/web/upstream/src/components/utils/InputConfirm.vue +68 -0
  106. package/apps/web/upstream/src/components/utils/InputPrompt.vue +118 -0
  107. package/apps/web/upstream/src/components/utils/InputText.vue +172 -0
  108. package/apps/web/upstream/src/components/utils/IrcInput.vue +579 -0
  109. package/apps/web/upstream/src/components/utils/PluginWrapper.vue +26 -0
  110. package/apps/web/upstream/src/components/utils/TabbedView.vue +149 -0
  111. package/apps/web/upstream/src/components/utils/TransitionExpand.vue +97 -0
  112. package/apps/web/upstream/src/helpers/Colours.js +128 -0
  113. package/apps/web/upstream/src/helpers/IrcdDiffs.js +26 -0
  114. package/apps/web/upstream/src/helpers/Md5.js +193 -0
  115. package/apps/web/upstream/src/helpers/Misc.js +401 -0
  116. package/apps/web/upstream/src/helpers/TextFormatting.js +237 -0
  117. package/apps/web/upstream/src/libs/AliasRewriter.js +157 -0
  118. package/apps/web/upstream/src/libs/AudioManager.js +60 -0
  119. package/apps/web/upstream/src/libs/BouncerMiddleware.js +247 -0
  120. package/apps/web/upstream/src/libs/BouncerProvider.js +551 -0
  121. package/apps/web/upstream/src/libs/ChathistoryMiddleware.js +153 -0
  122. package/apps/web/upstream/src/libs/ConfigLoader.js +94 -0
  123. package/apps/web/upstream/src/libs/EmojiProvider.js +46 -0
  124. package/apps/web/upstream/src/libs/GlobalApi.js +346 -0
  125. package/apps/web/upstream/src/libs/IPC.js +51 -0
  126. package/apps/web/upstream/src/libs/InputHandler.js +921 -0
  127. package/apps/web/upstream/src/libs/IrcClient.js +1506 -0
  128. package/apps/web/upstream/src/libs/Logger.js +71 -0
  129. package/apps/web/upstream/src/libs/Message.js +169 -0
  130. package/apps/web/upstream/src/libs/MessageFormatter.js +364 -0
  131. package/apps/web/upstream/src/libs/MessageParser.js +249 -0
  132. package/apps/web/upstream/src/libs/Notifications.js +78 -0
  133. package/apps/web/upstream/src/libs/ServerConnection.js +270 -0
  134. package/apps/web/upstream/src/libs/ServerSession.js +102 -0
  135. package/apps/web/upstream/src/libs/SoundBleep.js +20 -0
  136. package/apps/web/upstream/src/libs/StatePersistence.js +90 -0
  137. package/apps/web/upstream/src/libs/ThemeManager.js +128 -0
  138. package/apps/web/upstream/src/libs/TypingMiddleware.js +105 -0
  139. package/apps/web/upstream/src/libs/WindowTitle.js +63 -0
  140. package/apps/web/upstream/src/libs/batchedAdd.js +74 -0
  141. package/apps/web/upstream/src/libs/bufferTools.js +177 -0
  142. package/apps/web/upstream/src/libs/polyfill/Element.closest.js +18 -0
  143. package/apps/web/upstream/src/libs/renderers/Html.js +113 -0
  144. package/apps/web/upstream/src/libs/settingTools.js +31 -0
  145. package/apps/web/upstream/src/libs/state/BufferState.js +703 -0
  146. package/apps/web/upstream/src/libs/state/NetworkState.js +158 -0
  147. package/apps/web/upstream/src/libs/state/UserState.js +89 -0
  148. package/apps/web/upstream/src/libs/state/common.js +26 -0
  149. package/apps/web/upstream/src/libs/state.js +961 -0
  150. package/apps/web/upstream/src/libs/storage/Local.js +51 -0
  151. package/apps/web/upstream/src/main.js +566 -0
  152. package/apps/web/upstream/src/res/autocompleteCommands.js +31 -0
  153. package/apps/web/upstream/src/res/configTemplates.js +373 -0
  154. package/apps/web/upstream/src/res/globalStyle.css +277 -0
  155. package/apps/web/upstream/src/res/kiwiLoadingLogo.png +0 -0
  156. package/apps/web/upstream/src/res/locales/app.af-ZA.po +1145 -0
  157. package/apps/web/upstream/src/res/locales/app.ar-SA.po +1145 -0
  158. package/apps/web/upstream/src/res/locales/app.bg-BG.po +1145 -0
  159. package/apps/web/upstream/src/res/locales/app.bs-BA.po +1145 -0
  160. package/apps/web/upstream/src/res/locales/app.ca-ES.po +1145 -0
  161. package/apps/web/upstream/src/res/locales/app.cs-CZ.po +1145 -0
  162. package/apps/web/upstream/src/res/locales/app.da-DK.po +1145 -0
  163. package/apps/web/upstream/src/res/locales/app.de-DE.po +1145 -0
  164. package/apps/web/upstream/src/res/locales/app.dev.po +1246 -0
  165. package/apps/web/upstream/src/res/locales/app.el-GR.po +1145 -0
  166. package/apps/web/upstream/src/res/locales/app.en-US.po +1145 -0
  167. package/apps/web/upstream/src/res/locales/app.es-419.po +1145 -0
  168. package/apps/web/upstream/src/res/locales/app.es-AR.po +1145 -0
  169. package/apps/web/upstream/src/res/locales/app.es-EM.po +536 -0
  170. package/apps/web/upstream/src/res/locales/app.es-ES.po +1145 -0
  171. package/apps/web/upstream/src/res/locales/app.es-US.po +1145 -0
  172. package/apps/web/upstream/src/res/locales/app.eu-ES.po +1145 -0
  173. package/apps/web/upstream/src/res/locales/app.fi-FI.po +1145 -0
  174. package/apps/web/upstream/src/res/locales/app.fr-FR.po +1145 -0
  175. package/apps/web/upstream/src/res/locales/app.gl-ES.po +1145 -0
  176. package/apps/web/upstream/src/res/locales/app.he-IL.po +1145 -0
  177. package/apps/web/upstream/src/res/locales/app.hi-IN.po +1145 -0
  178. package/apps/web/upstream/src/res/locales/app.hu-HU.po +1145 -0
  179. package/apps/web/upstream/src/res/locales/app.id-ID.po +1145 -0
  180. package/apps/web/upstream/src/res/locales/app.it-IT.po +1145 -0
  181. package/apps/web/upstream/src/res/locales/app.ja-JP.po +1145 -0
  182. package/apps/web/upstream/src/res/locales/app.ko-KR.po +1145 -0
  183. package/apps/web/upstream/src/res/locales/app.nl-NL.po +1145 -0
  184. package/apps/web/upstream/src/res/locales/app.no-NO.po +1145 -0
  185. package/apps/web/upstream/src/res/locales/app.pl-PL.po +1145 -0
  186. package/apps/web/upstream/src/res/locales/app.pt-BR.po +1145 -0
  187. package/apps/web/upstream/src/res/locales/app.pt-PT.po +1145 -0
  188. package/apps/web/upstream/src/res/locales/app.ro-RO.po +1145 -0
  189. package/apps/web/upstream/src/res/locales/app.ru-RU.po +1145 -0
  190. package/apps/web/upstream/src/res/locales/app.sl-SI.po +1145 -0
  191. package/apps/web/upstream/src/res/locales/app.sq-AL.po +1145 -0
  192. package/apps/web/upstream/src/res/locales/app.sr-SP.po +1145 -0
  193. package/apps/web/upstream/src/res/locales/app.sv-SE.po +1145 -0
  194. package/apps/web/upstream/src/res/locales/app.tr-TR.po +1145 -0
  195. package/apps/web/upstream/src/res/locales/app.uk-UA.po +1145 -0
  196. package/apps/web/upstream/src/res/locales/app.vi-VN.po +1145 -0
  197. package/apps/web/upstream/src/res/locales/app.zh-CN.po +1145 -0
  198. package/apps/web/upstream/src/res/locales/app.zh-TW.po +1145 -0
  199. package/apps/web/upstream/src/res/localesList.json +27 -0
  200. package/apps/web/upstream/src/res/logo.png +0 -0
  201. package/apps/web/upstream/src/thirdparty/about.html +43 -0
  202. package/apps/web/upstream/src/thirdparty/index.js +5 -0
  203. package/apps/web/upstream/src/thirdparty/kiwiirccom.vue +185 -0
  204. package/apps/web/upstream/static/config.json +28 -0
  205. package/apps/web/upstream/static/emoticons/smile.png +0 -0
  206. package/apps/web/upstream/static/favicon.png +0 -0
  207. package/apps/web/upstream/static/highlight.mp3 +0 -0
  208. package/apps/web/upstream/static/highlight.ogg +0 -0
  209. package/apps/web/upstream/static/locales/.gitignore +2 -0
  210. package/apps/web/upstream/static/plugins/customise.html.example +10 -0
  211. package/apps/web/upstream/static/themes/coffee/theme.css +145 -0
  212. package/apps/web/upstream/static/themes/common/base.css +1064 -0
  213. package/apps/web/upstream/static/themes/dark/theme.css +196 -0
  214. package/apps/web/upstream/static/themes/default/theme.css +92 -0
  215. package/apps/web/upstream/static/themes/elite/theme.css +248 -0
  216. package/apps/web/upstream/static/themes/grayfox/theme.css +143 -0
  217. package/apps/web/upstream/static/themes/nightswatch/theme.css +367 -0
  218. package/apps/web/upstream/static/themes/osprey/theme.css +115 -0
  219. package/apps/web/upstream/static/themes/radioactive/theme.css +1211 -0
  220. package/apps/web/upstream/static/themes/sky/theme.css +107 -0
  221. package/apps/web/upstream/tests/unit/BatchAdd.spec.js +174 -0
  222. package/apps/web/upstream/tests/unit/MessageParser.spec.js +125 -0
  223. package/apps/web/upstream/tests/unit/Misc.spec.js +24 -0
  224. package/apps/web/upstream/tests/unit/NetworkState.spec.js +58 -0
  225. package/apps/web/upstream/tests/unit/StartupError.spec.js +20 -0
  226. package/apps/web/upstream/vue.config.js +150 -0
  227. package/apps/web/upstream/yarn.lock +9767 -0
  228. package/apps/web/vitest.config.ts +13 -0
  229. package/biome.json +1 -0
  230. package/docs/Home.md +1 -0
  231. package/docs/WebClientGuide.md +535 -0
  232. package/package.json +2 -2
  233. package/packages/aws-adapter/package.json +1 -1
  234. package/packages/cf-adapter/package.json +1 -1
  235. package/packages/cf-adapter/tests/cf-harness.ts +12 -1
  236. package/packages/in-memory-runtime/package.json +1 -1
  237. package/packages/irc-core/package.json +1 -1
  238. package/packages/irc-core/reports/mutation/mutation.html +342 -0
  239. package/packages/irc-core/tests/batch.test.ts +15 -0
  240. package/packages/irc-core/tests/numerics.test.ts +13 -0
  241. package/packages/irc-core/tests/parser.test.ts +109 -0
  242. package/packages/irc-server/package.json +1 -1
  243. package/packages/irc-test-support/package.json +1 -1
  244. package/packages/irc-test-support/src/scenarios.ts +24 -2
  245. package/tools/ci-hardening/package.json +1 -1
  246. package/tools/load-test/package.json +1 -1
  247. package/tools/tcp-ws-forwarder/package.json +1 -1
@@ -0,0 +1,1506 @@
1
+ 'kiwi public';
2
+
3
+ import _ from 'lodash';
4
+ import strftime from 'strftime';
5
+ import Irc from 'irc-framework';
6
+ import * as TextFormatting from '@/helpers/TextFormatting';
7
+ import typingMiddleware from './TypingMiddleware';
8
+ import chathistoryMiddleware from './ChathistoryMiddleware';
9
+ import * as ServerConnection from './ServerConnection';
10
+
11
+ export function create(state, network) {
12
+ let networkid = network.id;
13
+
14
+ let ircClient = new Irc.Client({
15
+ // Most options are set under the overloaded .connect()
16
+ version: null,
17
+ enable_chghost: true,
18
+ enable_setname: true,
19
+ message_max_length: 350,
20
+ });
21
+ ircClient.requestCap('znc.in/self-message');
22
+ ircClient.use(chathistoryMiddleware());
23
+ ircClient.use(clientMiddleware(state, network));
24
+ ircClient.use(typingMiddleware());
25
+
26
+ // Overload the connect() function to make sure we are connecting with the
27
+ // most recent connection details from the network state
28
+ let originalIrcClientConnect = ircClient.connect;
29
+ ircClient.connect = function connect(...args) {
30
+ // Set some defaults if we don't have eveything
31
+ if (!network.connection.nick) {
32
+ network.connection.nick = 'Guest' + Math.floor(Math.random() * 100);
33
+ }
34
+
35
+ ircClient.options.host = network.connection.server;
36
+ ircClient.options.port = network.connection.port;
37
+ ircClient.options.tls = network.connection.tls;
38
+ ircClient.options.path = network.connection.path;
39
+ ircClient.options.password = network.connection.password;
40
+ if (network.password) {
41
+ ircClient.options.account = {
42
+ account: network.connection.nick,
43
+ password: network.password,
44
+ };
45
+ } else {
46
+ // No password so give an empty account config. This forces irc-framework to keep
47
+ // the server password (options.password) separate from SASL
48
+ ircClient.options.account = { };
49
+ }
50
+ ircClient.options.nick = network.connection.nick;
51
+ ircClient.options.username = network.username || network.connection.nick;
52
+ ircClient.options.gecos = network.gecos || 'https://kiwiirc.com/';
53
+ ircClient.options.encoding = network.connection.encoding;
54
+ ircClient.options.auto_reconnect = !!state.setting('autoReconnect');
55
+ ircClient.options.sasl_disconnect_on_fail = !!state.setting('disconnectOnSaslFail');
56
+
57
+ // Apply any irc-fw options specified in kiwiirc config
58
+ let configOptions = state.setting('ircFramework');
59
+ if (configOptions) {
60
+ Object.assign(ircClient.options, configOptions);
61
+ }
62
+
63
+ let eventObj = { network, transport: null };
64
+ state.$emit('network.connecting', eventObj);
65
+
66
+ if (eventObj.transport) {
67
+ // A plugin might use its own transport of some kind
68
+ ircClient.options.transport = eventObj.transport;
69
+ } else if (!network.connection.direct) {
70
+ // A direct connection uses a websocket to connect (note: some browsers limit
71
+ // the number of connections to the same host!).
72
+ // A non-direct connection will connect via the configured kiwi server using
73
+ // with our own irc-framework compatible transport.
74
+ ircClient.options.transport = ServerConnection.createChannelConstructor(
75
+ state.settings.kiwiServer,
76
+ (window.location.hash || '').substr(1),
77
+ networkid
78
+ );
79
+ } else {
80
+ // Use the irc-framework default transport
81
+ ircClient.options.transport = undefined;
82
+ }
83
+
84
+ originalIrcClientConnect.apply(ircClient, args);
85
+ };
86
+
87
+ // Overload the raw() function so that we can emit outgoing IRC messages to plugins
88
+ let originalIrcClientRaw = ircClient.raw;
89
+ ircClient.raw = function raw(...args) {
90
+ let message = null;
91
+
92
+ if (args[0] instanceof Irc.Message) {
93
+ message = args[0];
94
+ } else {
95
+ let rawString = ircClient.rawString(...args);
96
+ message = Irc.ircLineParser(rawString);
97
+ }
98
+
99
+ let eventObj = { network, message, handled: false };
100
+ state.$emit('ircout', eventObj);
101
+ if (!eventObj.handled) {
102
+ originalIrcClientRaw.apply(ircClient, [message]);
103
+ }
104
+ };
105
+
106
+ ircClient.on('raw', (event) => {
107
+ if (!network.setting('show_raw') && !state.setting('showRaw')) {
108
+ return;
109
+ }
110
+
111
+ let buffer = state.getOrAddBufferByName(networkid, '*raw');
112
+ state.addMessage(buffer, {
113
+ time: Date.now(),
114
+ nick: '',
115
+ message: (event.from_server ? '[S] ' : '[C] ') + event.line,
116
+ });
117
+ });
118
+
119
+ ircClient.on('typing', (event) => {
120
+ let user = state.getUser(network.id, event.nick);
121
+ if (user) {
122
+ user.typingStatus(event.target, event.status);
123
+ }
124
+ });
125
+
126
+ return ircClient;
127
+ }
128
+
129
+ function clientMiddleware(state, network) {
130
+ let networkid = network.id;
131
+ // eslint-disable-next-line
132
+ let numConnects = 0;
133
+ let isRegistered = false;
134
+
135
+ return function middlewareFn(client, rawEvents, parsedEvents) {
136
+ parsedEvents.use(parsedEventsHandler);
137
+ rawEvents.use(rawEventsHandler);
138
+
139
+ client.on('connecting', () => {
140
+ network.state_error = '';
141
+ network.state = 'connecting';
142
+ network.last_error = '';
143
+ network.last_error_numeric = 0;
144
+ });
145
+
146
+ client.on('connected', () => {
147
+ network.state_error = '';
148
+ network.state = 'connected';
149
+ });
150
+
151
+ client.on('socket close', (err) => {
152
+ isRegistered = false;
153
+ network.state = 'disconnected';
154
+ network.state_error = err || '';
155
+
156
+ network.buffers.forEach((buffer) => {
157
+ if (!buffer) {
158
+ return;
159
+ }
160
+
161
+ buffer.joined = false;
162
+ buffer.clearUsers();
163
+ });
164
+ });
165
+ };
166
+
167
+ function rawEventsHandler(command, event, rawLine, client, next) {
168
+ // Allow plugins to override raw IRC events
169
+ let eventObj = { ...event, raw: rawLine, handled: false };
170
+ state.$emit('irc.raw', command, eventObj, network);
171
+ if (eventObj.handled) {
172
+ return;
173
+ }
174
+
175
+ state.$emit('irc.raw.' + command, command, eventObj, network);
176
+ if (eventObj.handled) {
177
+ return;
178
+ }
179
+
180
+ if (command === '002') {
181
+ // Your host is server.example.net, running version InspIRCd-2.0
182
+ let param = event.params[1] || '';
183
+ let m = param.match(/running version (.*)$/);
184
+ network.ircd = m ?
185
+ m[1] :
186
+ '';
187
+ }
188
+
189
+ if (command === 'CAP' && network.setting('show_raw_caps')) {
190
+ let params = [...event.params];
191
+ if (params[params.length - 1].indexOf(' ') > -1) {
192
+ params[params.length - 1] = ':' + params[params.length - 1];
193
+ }
194
+
195
+ let buffer = network.serverBuffer();
196
+ state.addMessage(buffer, {
197
+ time: Date.now(),
198
+ nick: '',
199
+ message: event.command + ' ' + params.join(' '),
200
+ });
201
+ }
202
+
203
+ next();
204
+ }
205
+
206
+ function parsedEventsHandler(command, event, client, next) {
207
+ // Trigger this event through the state object first. If it's been handled
208
+ // somewhere else then we ignore it.
209
+ let ircEventObj = { handled: false };
210
+ state.$emit('irc.' + command, event, network, ircEventObj);
211
+ if (ircEventObj.handled) {
212
+ next();
213
+ return;
214
+ }
215
+
216
+ // Ignore any of the control messages. They're transport related to kiwi internals
217
+ if (event && event.command === 'CONTROL') {
218
+ next();
219
+ return;
220
+ }
221
+
222
+ // If there is a time difference between this client and the server, convert it
223
+ // to match our local time so it makes sense to the user
224
+ let eventTime = (event && event.time) ?
225
+ network.ircClient.network.timeToLocal(event.time) :
226
+ Date.now();
227
+ let serverTime = (event && event.time) || 0;
228
+
229
+ if (command === 'channel_redirect') {
230
+ let b = network.bufferByName(event.from);
231
+ if (b) {
232
+ b.flags.redirect_to = event.to;
233
+ }
234
+ }
235
+
236
+ if (command === 'registered') {
237
+ isRegistered = true;
238
+ network.nick = event.nick;
239
+ state.addUser(networkid, { nick: event.nick, username: client.user.username });
240
+
241
+ let serverBuffer = network.serverBuffer();
242
+ state.addMessage(serverBuffer, {
243
+ time: eventTime,
244
+ server_time: serverTime,
245
+ nick: '',
246
+ message: TextFormatting.t('connected_to', { network: client.network.name }),
247
+ });
248
+
249
+ // Get some extra info about ourselves
250
+ client.raw('WHO ' + event.nick);
251
+
252
+ if (network.auto_commands) {
253
+ network.auto_commands.split('\n').forEach((line) => {
254
+ state.$emit('input.raw', line[0] === '/' ? line : `/${line}`, {
255
+ network: network,
256
+ buffer: serverBuffer,
257
+ });
258
+ });
259
+ }
260
+
261
+ // Join our channels
262
+ // If under bouncer mode, the bouncer will send the channels were joined to instead.
263
+ if (!network.connection.bncnetid) {
264
+ network.buffers.forEach((buffer) => {
265
+ if (buffer.isChannel() && buffer.enabled) {
266
+ client.join(buffer.name, buffer.key);
267
+ }
268
+ });
269
+ }
270
+
271
+ numConnects++;
272
+ }
273
+
274
+ if (command === 'server options') {
275
+ // If the network name has changed from the irc-framework default, update ours
276
+ // Also if it isn't a BNC network as the name is then derived from the BNC info instead
277
+ if (client.network.name !== 'Network' && !network.connection.bncnetid) {
278
+ network.name = client.network.name;
279
+ }
280
+ }
281
+
282
+ // Show unhandled data from the server in the servers tab
283
+ if (command === 'unknown command') {
284
+ let buffer = network.serverBuffer();
285
+ let containsNick = event.params[0] === network.ircClient.user.nick;
286
+ let isChannelMessage = network.isChannelName(event.params[1]);
287
+
288
+ let message = '';
289
+ if (['486', '477'].includes(event.command)) {
290
+ // Messages from these events don't need the nick from params[0]
291
+ message = event.params[2];
292
+ } else if (containsNick && isChannelMessage) {
293
+ // This error is aimed at us and has a target channel
294
+ // replace the target buffer and remove unneeded params
295
+ let channelBuffer = network.bufferByName(event.params[1]);
296
+ if (channelBuffer) {
297
+ buffer = channelBuffer;
298
+ }
299
+ message = event.params.slice(2).join(', ');
300
+ } else if (containsNick) {
301
+ // Strip out our nick if it's the first params (many commands include this)
302
+ message = event.params.slice(1).join(', ');
303
+ } else {
304
+ message = event.params.join(', ');
305
+ }
306
+
307
+ // Numerics for restrictions on sending messages to channels/users
308
+ const restrictedMessages = [
309
+ '486', // ERR_NONONREG old numeric used by unreal42
310
+ '477', // ERR_NEEDREGGEDNICK
311
+ '716', // RPL_TARGUMODEG
312
+ '717', // RPL_TARGNOTIFY
313
+ ];
314
+ if (restrictedMessages.includes(event.command)) {
315
+ // You must log in with services to message this user/channel
316
+ // or CallerID +g
317
+ buffer = state.getOrAddBufferByName(network.id, event.params[1]);
318
+
319
+ // Only add this message if it does not match the previous message
320
+ // Typing status messages can cause a spam of this error type
321
+ state.addMessageNoRepeat(buffer, {
322
+ time: eventTime,
323
+ server_time: serverTime,
324
+ nick: '*',
325
+ message: message,
326
+ type: 'error',
327
+ });
328
+
329
+ if (
330
+ event.command === '477' &&
331
+ buffer.isChannel() &&
332
+ buffer.enabled &&
333
+ !buffer.joined
334
+ ) {
335
+ // The buffer we tried to join requires a registered nick,
336
+ // disable it until the user manually tries to rejoin
337
+ buffer.enabled = false;
338
+ }
339
+ } else {
340
+ // Only show non-numeric commands
341
+ if (!event.command.match(/^\d+$/)) {
342
+ message += event.command + ' ';
343
+ }
344
+
345
+ state.addMessage(buffer, {
346
+ time: eventTime,
347
+ server_time: serverTime,
348
+ nick: '',
349
+ message: message,
350
+ });
351
+ }
352
+ }
353
+
354
+ if (command.toLowerCase() === 'batch start chathistory' && client.chathistory) {
355
+ // We have a new batch of messages. To prevent duplicate messages being shown, we remove
356
+ // all messages we have locally in the range of these new messages so that the new block
357
+ // of messages we recieved are displayed accurately. Each message in the block will
358
+ // trigger a 'message' event after this.
359
+ let startTime = 0;
360
+ let endTime = 0;
361
+ event.commands.forEach((message) => {
362
+ if (message.time && message.time > endTime) {
363
+ endTime = message.time;
364
+ }
365
+
366
+ if (message.time && message.time < startTime) {
367
+ startTime = message.time;
368
+ }
369
+ });
370
+
371
+ if (!startTime || !endTime) {
372
+ return;
373
+ }
374
+
375
+ let buffer = state.getBufferByName(networkid, event.params[0]);
376
+ if (buffer) {
377
+ buffer.clearMessageRange(startTime, endTime);
378
+ }
379
+ }
380
+
381
+ if (command === 'message') {
382
+ let isPrivateMessage = false;
383
+ let bufferName = event.from_server ? '*' : event.target;
384
+
385
+ // If the message came from a batch then params[0] is the bufferName
386
+ if (event.batch && event.batch.type === 'chathistory' && event.batch.params[0]) {
387
+ bufferName = event.batch.params[0];
388
+ isPrivateMessage = !network.isChannelName(bufferName);
389
+ } else if (!event.from_server && event.target === client.user.nick) {
390
+ // PMs should go to a buffer with the name of the other user
391
+ isPrivateMessage = true;
392
+ bufferName = event.nick;
393
+ }
394
+
395
+ // Chanserv sometimes PMs messages about a channel on join in the format of
396
+ // [#channel] welcome!
397
+ // Redirect these to #channel
398
+ if (
399
+ event.nick.toLowerCase() === 'chanserv' &&
400
+ isPrivateMessage &&
401
+ event.message[0] === '['
402
+ ) {
403
+ bufferName = event.message.substr(1, event.message.indexOf(']') - 1);
404
+ }
405
+
406
+ // Notices from somewhere when we don't have an existing buffer for them should go into
407
+ // the server tab. ie. notices from servers
408
+ if (event.type === 'notice') {
409
+ let existingBuffer = state.getBufferByName(networkid, bufferName);
410
+ let noticeActiveBuffer = state.setting('noticeActiveBuffer');
411
+ let activeBuffer = state.getActiveBuffer();
412
+ let hasActiveBuffer = activeBuffer && activeBuffer.networkid === networkid;
413
+
414
+ // If we don't have a buffer for this notice sender, either show it in our active
415
+ // buffer or the server buffer
416
+ if (!existingBuffer) {
417
+ if (noticeActiveBuffer && hasActiveBuffer && !activeBuffer.isSpecial()) {
418
+ bufferName = activeBuffer.name;
419
+ } else {
420
+ bufferName = '*';
421
+ }
422
+ }
423
+ }
424
+
425
+ const PM_BLOCK_BLOCKED = false;
426
+ // const PM_BLOCK_NOT_BLOCKED = true;
427
+ const PM_BLOCK_REQUIRES_CHECK = null;
428
+
429
+ let pmBlock = network.isNickExemptFromPmBlocks(event.nick);
430
+ let blockNewPms = state.setting('buffers.block_pms');
431
+ let buffer = state.getBufferByName(networkid, bufferName);
432
+
433
+ let textFormatType = 'privmsg';
434
+ if (event.type === 'action') {
435
+ textFormatType = 'action';
436
+ } else if (event.type === 'notice') {
437
+ textFormatType = 'notice';
438
+ }
439
+
440
+ let messageBody = TextFormatting.formatText(textFormatType, {
441
+ nick: event.nick,
442
+ username: event.ident,
443
+ host: event.hostname,
444
+ text: event.message,
445
+ });
446
+
447
+ let message = {
448
+ time: eventTime,
449
+ server_time: serverTime,
450
+ nick: event.nick,
451
+ message: messageBody,
452
+ type: event.type,
453
+ tags: event.tags || {},
454
+ };
455
+
456
+ // If this is a new PM and the sending user is not exempt from blocks, ignore it
457
+ if (blockNewPms && isPrivateMessage && !buffer && pmBlock === PM_BLOCK_BLOCKED) {
458
+ return;
459
+ }
460
+
461
+ // If we need to manually check if this user is blocked..
462
+ // PM_BLOCK_REQUIRES_CHECK means we should whois the user to get their oper status. We
463
+ // allways allow messages from opers.
464
+ if (blockNewPms && isPrivateMessage && !buffer && pmBlock === PM_BLOCK_REQUIRES_CHECK) {
465
+ // if the nick is in pendingPms it has already issued a whois request
466
+ let awaitingWhois = !!_.find(network.pendingPms, { nick: event.nick });
467
+ network.pendingPms.push({ bufferName, message });
468
+
469
+ // Don't send another whois if we are already awaiting another
470
+ if (awaitingWhois) {
471
+ return;
472
+ }
473
+
474
+ network.ircClient.whois(event.nick, event.nick, (whoisData) => {
475
+ network.pendingPms.forEach((pm, idx, obj) => {
476
+ let nickLower = pm.message.nick.toLowerCase();
477
+ if (nickLower === whoisData.nick.toLowerCase()) {
478
+ if (whoisData.operator) {
479
+ buffer = state.getOrAddBufferByName(network.id, pm.bufferName);
480
+ state.addMessage(buffer, pm.message);
481
+ }
482
+ obj.splice(idx, 1);
483
+ }
484
+ });
485
+ });
486
+
487
+ return;
488
+ }
489
+
490
+ // Make sure we have a buffer for our message
491
+ if (!buffer) {
492
+ buffer = state.getOrAddBufferByName(networkid, bufferName);
493
+ }
494
+ state.addMessage(buffer, message);
495
+ }
496
+
497
+ if (command === 'wallops') {
498
+ let buffer = state.getOrAddBufferByName(networkid, '*');
499
+ let messageBody = TextFormatting.formatText('wallops', {
500
+ text: event.message,
501
+ });
502
+
503
+ state.addMessage(buffer, {
504
+ time: eventTime,
505
+ server_time: serverTime,
506
+ nick: event.nick,
507
+ message: messageBody,
508
+ type: 'wallops',
509
+ });
510
+ }
511
+
512
+ if (command === 'join') {
513
+ // If we have any buffers marked as being redirected to this new channel, update
514
+ // that buffer instead of creating a new one
515
+ if (event.nick === client.user.nick) {
516
+ network.buffers.forEach((b) => {
517
+ if ((b.flags.redirect_to || '').toLowerCase() === event.channel.toLowerCase()) {
518
+ state.$delete(b.flags, 'redirect_to');
519
+ b.rename(event.channel);
520
+ }
521
+ });
522
+ }
523
+
524
+ let buffer = state.getOrAddBufferByName(networkid, event.channel);
525
+
526
+ // The case does not match, update buffer.name to the casing sent by the server
527
+ if (buffer.name !== event.channel) {
528
+ buffer.rename(event.channel);
529
+ }
530
+
531
+ state.addUserToBuffer(buffer, {
532
+ nick: event.nick,
533
+ username: event.ident,
534
+ host: event.hostname,
535
+ realname: event.gecos,
536
+ account: event.account || '',
537
+ });
538
+
539
+ if (event.nick === client.user.nick) {
540
+ buffer.enabled = true;
541
+ buffer.joined = true;
542
+ buffer.flags.channel_badkey = false;
543
+ network.ircClient.raw('MODE', event.channel);
544
+ network.ircClient.who(event.channel);
545
+ }
546
+
547
+ let ignoreEvent = state.setting('skipHiddenMessages') && !buffer.setting('show_joinparts');
548
+ if (!ignoreEvent || event.nick === client.user.nick) {
549
+ let nick = buffer.setting('show_hostnames') ?
550
+ TextFormatting.formatUserFull(event) :
551
+ TextFormatting.formatUser(event);
552
+
553
+ let messageBody = TextFormatting.formatAndT(
554
+ 'channel_join',
555
+ null,
556
+ 'has_joined',
557
+ { nick: nick }
558
+ );
559
+
560
+ let typeExtra = (event.nick === client.user.nick) ? 'join_self' : 'join';
561
+
562
+ state.addMessage(buffer, {
563
+ time: eventTime,
564
+ server_time: serverTime,
565
+ nick: event.nick,
566
+ message: messageBody,
567
+ type: 'traffic',
568
+ type_extra: typeExtra,
569
+ });
570
+ }
571
+ }
572
+ if (command === 'kick') {
573
+ let buffer = state.getOrAddBufferByName(networkid, event.channel);
574
+ state.removeUserFromBuffer(buffer, event.kicked);
575
+
576
+ if (event.kicked === client.user.nick) {
577
+ buffer.joined = false;
578
+ buffer.enabled = false;
579
+ buffer.clearUsers();
580
+ }
581
+
582
+ let messageBody = '';
583
+ let ignoreEvent = state.setting('skipHiddenMessages') && !buffer.setting('show_joinparts');
584
+ let isUserInvolved = [event.kicked, event.nick].includes(client.user.nick);
585
+ if (!ignoreEvent || isUserInvolved) {
586
+ if (event.kicked === client.user.nick) {
587
+ messageBody = TextFormatting.formatAndT(
588
+ 'channel_selfkick',
589
+ { reason: event.message },
590
+ 'kicked_you_from',
591
+ {
592
+ nick: TextFormatting.formatUser(event),
593
+ channel: event.channel,
594
+ }
595
+ );
596
+ } else {
597
+ messageBody = TextFormatting.formatAndT(
598
+ 'channel_kicked',
599
+ { reason: event.message },
600
+ 'was_kicked_from',
601
+ {
602
+ nick: event.kicked,
603
+ channel: event.channel,
604
+ chanop: TextFormatting.formatUser(event.nick),
605
+ }
606
+ );
607
+ }
608
+
609
+ let typeExtra = (isUserInvolved) ? 'kick_self' : 'kick';
610
+
611
+ state.addMessage(buffer, {
612
+ time: eventTime,
613
+ server_time: serverTime,
614
+ nick: event.nick,
615
+ message: messageBody,
616
+ type: 'traffic',
617
+ type_extra: typeExtra,
618
+ });
619
+ }
620
+ }
621
+ if (command === 'part') {
622
+ let buffer = state.getBufferByName(networkid, event.channel);
623
+ if (!buffer) {
624
+ return;
625
+ }
626
+
627
+ state.removeUserFromBuffer(buffer, event.nick);
628
+ if (event.nick === client.user.nick) {
629
+ buffer.joined = false;
630
+ buffer.enabled = false;
631
+ buffer.clearUsers();
632
+ }
633
+
634
+ // Remove the user from network state if no remaining common channels
635
+ let remainingBuffers = state.getBuffersWithUser(networkid, event.nick);
636
+ if (remainingBuffers.length === 0) {
637
+ state.removeUser(networkid, {
638
+ nick: event.nick,
639
+ });
640
+ }
641
+
642
+ let ignoreEvent = state.setting('skipHiddenMessages') && !buffer.setting('show_joinparts');
643
+ if (!ignoreEvent || event.nick === client.user.nick) {
644
+ let nick = buffer.setting('show_hostnames') ?
645
+ TextFormatting.formatUserFull(event) :
646
+ TextFormatting.formatUser(event);
647
+
648
+ let messageBody = TextFormatting.formatAndT(
649
+ 'channel_part',
650
+ { reason: event.message },
651
+ 'has_left',
652
+ { nick: nick },
653
+ );
654
+
655
+ let typeExtra = (event.nick === client.user.nick) ? 'part_self' : 'part';
656
+
657
+ state.addMessage(buffer, {
658
+ time: eventTime,
659
+ server_time: serverTime,
660
+ nick: event.nick,
661
+ message: messageBody,
662
+ type: 'traffic',
663
+ type_extra: typeExtra,
664
+ });
665
+ }
666
+ }
667
+ if (command === 'quit') {
668
+ let buffers = state.getBuffersWithUser(networkid, event.nick);
669
+
670
+ buffers.forEach((buffer) => {
671
+ if (!buffer) {
672
+ return;
673
+ }
674
+
675
+ if (event.nick === client.user.nick) {
676
+ buffer.joined = false;
677
+ buffer.clearUsers();
678
+ }
679
+
680
+ let ignoreEvent = state.setting('skipHiddenMessages') && !buffer.setting('show_joinparts');
681
+ if (ignoreEvent && event.nick !== client.user.nick) {
682
+ return;
683
+ }
684
+
685
+ let nick = buffer.setting('show_hostnames') ?
686
+ TextFormatting.formatUserFull(event) :
687
+ TextFormatting.formatUser(event);
688
+
689
+ let messageBody = TextFormatting.formatAndT(
690
+ 'channel_quit',
691
+ { reason: event.message },
692
+ 'has_left',
693
+ { nick: nick }
694
+ );
695
+
696
+ let typeExtra = (event.nick === client.user.nick) ? 'quit_self' : 'quit';
697
+
698
+ state.addMessage(buffer, {
699
+ time: eventTime,
700
+ server_time: serverTime,
701
+ nick: event.nick,
702
+ message: messageBody,
703
+ type: 'traffic',
704
+ type_extra: typeExtra,
705
+ });
706
+ });
707
+
708
+ // Set the user as away before removing so away status indicators are updated
709
+ let user = state.getUser(networkid, event.nick);
710
+ if (user) {
711
+ user.away = 'offline';
712
+ }
713
+
714
+ state.removeUser(networkid, {
715
+ nick: event.nick,
716
+ });
717
+ }
718
+
719
+ if (command === 'invite') {
720
+ let buffer = network.serverBuffer();
721
+ let activeNetwork = state.getActiveNetwork();
722
+ let activeBuffer = state.getActiveBuffer();
723
+ if (network === activeNetwork && !activeBuffer.isSpecial()) {
724
+ buffer = activeBuffer;
725
+ }
726
+
727
+ let translationKey = 'invited_other';
728
+ if (event.invited === network.currentUser().nick) {
729
+ translationKey = 'invited_you';
730
+ } else if (event.nick === network.currentUser().nick) {
731
+ translationKey = 'invited_inviter';
732
+ }
733
+
734
+ state.addMessage(buffer, {
735
+ nick: '',
736
+ time: eventTime,
737
+ server_time: serverTime,
738
+ type: 'invite',
739
+ message: TextFormatting.t(translationKey, {
740
+ nick: event.nick,
741
+ invited: event.invited,
742
+ channel: event.channel,
743
+ }),
744
+ });
745
+ }
746
+
747
+ if (command === 'account') {
748
+ state.addUser(networkid, { nick: event.nick, account: event.account || '' });
749
+ }
750
+
751
+ if (command === 'whois') {
752
+ let obj = {
753
+ nick: event.nick,
754
+ host: event.hostname,
755
+ username: event.ident,
756
+ away: event.away || '',
757
+ realname: event.real_name,
758
+ hasWhois: true,
759
+ };
760
+
761
+ // Some other optional bits of info
762
+ [
763
+ 'actual_host',
764
+ 'helpop',
765
+ 'bot',
766
+ 'server',
767
+ 'server_info',
768
+ 'operator',
769
+ 'channels',
770
+ 'modes',
771
+ 'idle',
772
+ 'logon',
773
+ 'registered_nick',
774
+ 'account',
775
+ 'secure',
776
+ 'certfp',
777
+ 'special',
778
+ ].forEach((prop) => {
779
+ if (typeof event[prop] !== 'undefined') {
780
+ obj[prop] = event[prop];
781
+ }
782
+ });
783
+
784
+ state.addUser(networkid, obj);
785
+ }
786
+
787
+ if (command === 'away') {
788
+ state.addUser(networkid, {
789
+ nick: event.nick,
790
+ away: event.message || '',
791
+ });
792
+ let buffer = state.getActiveBuffer();
793
+ if (buffer && event.nick === client.user.nick) {
794
+ network.away = 'away';
795
+ state.addMessage(buffer, {
796
+ time: eventTime,
797
+ server_time: serverTime,
798
+ nick: '*',
799
+ type: 'presence',
800
+ message: event.message,
801
+ });
802
+ }
803
+ }
804
+
805
+ if (command === 'back') {
806
+ state.addUser(networkid, {
807
+ nick: event.nick,
808
+ away: '',
809
+ });
810
+ let buffer = state.getActiveBuffer();
811
+ if (buffer && event.nick === client.user.nick) {
812
+ network.away = '';
813
+ state.addMessage(buffer, {
814
+ time: eventTime,
815
+ server_time: serverTime,
816
+ nick: '*',
817
+ type: 'presence',
818
+ message: event.message,
819
+ });
820
+ }
821
+ }
822
+
823
+ if (command === 'wholist') {
824
+ state.usersTransaction(networkid, (users) => {
825
+ event.users.forEach((eventUser) => {
826
+ let userObj = {
827
+ nick: eventUser.nick,
828
+ host: eventUser.hostname || undefined,
829
+ username: eventUser.ident || undefined,
830
+ away: eventUser.away ? 'Away' : '',
831
+ realname: eventUser.real_name,
832
+ account: eventUser.account || undefined,
833
+ };
834
+ let user = state.addUser(networkid, userObj, users);
835
+ if (!user) {
836
+ // Should never happen as this network should always exist
837
+ return;
838
+ }
839
+
840
+ let buffer = network.bufferByName(eventUser.channel);
841
+ if (!buffer || !user.buffers[buffer.id]) {
842
+ return;
843
+ }
844
+
845
+ // Add all the user channel modes
846
+ let modes = user.buffers[buffer.id].modes;
847
+ eventUser.channel_modes.forEach((mode) => {
848
+ if (modes.indexOf(mode) === -1) {
849
+ modes.push(mode);
850
+ }
851
+ });
852
+ });
853
+ });
854
+ }
855
+
856
+ if (command === 'channel list start') {
857
+ network.channel_list_cache = [];
858
+ network.channel_list_state = 'updating';
859
+ }
860
+ if (command === 'channel list') {
861
+ network.channel_list_state = 'updating';
862
+ // Filter private channels from the channel list
863
+ let filteredEvent = _.filter(event, (o) => o.channel !== '*');
864
+ // Store the channels in channel_list_cache before moving it all to
865
+ // channel_list at the end. This gives a huge performance boost since
866
+ // it doesn't need to be all reactive for every update
867
+ network.channel_list_cache = (network.channel_list_cache || []).concat(filteredEvent);
868
+ }
869
+ if (command === 'channel list end') {
870
+ network.channel_list = network.channel_list_cache || [];
871
+ network.channel_list_state = 'updated';
872
+ delete network.channel_list_cache;
873
+ }
874
+
875
+ if (command === 'motd') {
876
+ let buffer = network.serverBuffer();
877
+ let messageBody = TextFormatting.formatText('motd', {
878
+ text: event.motd,
879
+ });
880
+ state.addMessage(buffer, {
881
+ time: eventTime,
882
+ server_time: serverTime,
883
+ nick: '',
884
+ message: messageBody,
885
+ type: 'motd',
886
+ });
887
+ }
888
+
889
+ if (command === 'nick in use') {
890
+ let shouldChangeNick = !client.connection.registered && state.setting('changeNickOnCollision');
891
+ let newNick = client.user.nick.replace(/\d+$/, '') + rand(1, 99);
892
+
893
+ let translationKey = shouldChangeNick ? 'nick_in_use_retrying' : 'error_nick_in_use';
894
+ let translationVars = { nick: client.user.nick };
895
+ if (shouldChangeNick) {
896
+ translationVars.newnick = newNick;
897
+ }
898
+
899
+ let messageBody = TextFormatting.formatAndT(
900
+ 'nickname_alreadyinuse',
901
+ null,
902
+ translationKey,
903
+ translationVars,
904
+ );
905
+
906
+ let message = {
907
+ time: eventTime,
908
+ server_time: serverTime,
909
+ nick: '',
910
+ message: messageBody,
911
+ type: 'error',
912
+ };
913
+
914
+ let activeBuffer = state.getActiveBuffer();
915
+ let buffer = network.serverBuffer();
916
+
917
+ if (activeBuffer && activeBuffer.networkid === network.id) {
918
+ buffer = activeBuffer;
919
+ }
920
+ if (buffer) {
921
+ state.addMessage(buffer, message);
922
+ }
923
+
924
+ if (shouldChangeNick) {
925
+ client.changeNick(newNick);
926
+ } else if (!client.connection.registered) {
927
+ network.last_error = messageBody;
928
+ network.ircClient.connection.end();
929
+ }
930
+ }
931
+
932
+ if (command === 'nick') {
933
+ if (event.nick === client.user.nick) {
934
+ network.nick = event.new_nick;
935
+ }
936
+
937
+ state.changeUserNick(networkid, event.nick, event.new_nick);
938
+
939
+ let messageBody = TextFormatting.formatAndT(
940
+ 'nick_changed',
941
+ null,
942
+ 'now_known_as',
943
+ { nick: event.nick, newnick: event.new_nick },
944
+ );
945
+
946
+ let typeExtra = (event.nick === client.user.nick) ? 'nick_self' : '';
947
+
948
+ let buffers = state.getBuffersWithUser(networkid, event.new_nick);
949
+ buffers.forEach((buffer) => {
950
+ let ignoreEvent = state.setting('skipHiddenMessages') && !buffer.setting('show_nick_changes');
951
+ if (ignoreEvent && event.nick !== client.user.nick) {
952
+ return;
953
+ }
954
+
955
+ state.addMessage(buffer, {
956
+ time: eventTime,
957
+ server_time: serverTime,
958
+ nick: '',
959
+ message: messageBody,
960
+ type: 'nick',
961
+ type_extra: typeExtra,
962
+ });
963
+ });
964
+ }
965
+
966
+ if (command === 'userlist') {
967
+ let buffer = state.getOrAddBufferByName(networkid, event.channel);
968
+ let ucNick = client.user.nick.toUpperCase();
969
+ let hadExistingUsers = Object.keys(buffer.users)
970
+ .filter((u) => u !== ucNick)
971
+ .length > 0;
972
+ let users = [];
973
+ event.users.forEach((user) => {
974
+ users.push({
975
+ user: {
976
+ nick: user.nick,
977
+ username: user.ident,
978
+ hostname: user.hostname,
979
+ },
980
+ modes: user.modes,
981
+ });
982
+ });
983
+ state.addMultipleUsersToBuffer(buffer, users);
984
+
985
+ if (!hadExistingUsers && network.ircClient.chathistory.isSupported()) {
986
+ // TODO: If this is a reconnect (numConnects > 1) then paginate backwards
987
+ // until we reach our last message.
988
+ // OR
989
+ // Add a marker at the gap between this new chathistory block starts and when
990
+ // the existing messages end so that we can add a "load missing messages"
991
+ // button there or have it auto request them when it scrolls into view
992
+ if (
993
+ buffer.isChannel()
994
+ && ['all', 'channels'].includes(buffer.setting('auto_request_history'))
995
+ ) {
996
+ buffer.requestLatestScrollback();
997
+ }
998
+ }
999
+ }
1000
+
1001
+ if (command === 'user updated') {
1002
+ const user = network.userByName(event.nick);
1003
+ if (user) {
1004
+ Object.entries(event).forEach(([key, val]) => {
1005
+ if (key.indexOf('new_') !== 0) {
1006
+ return;
1007
+ }
1008
+
1009
+ const paramName = key.substr(4);
1010
+ switch (paramName) {
1011
+ case 'gecos':
1012
+ user.realname = val;
1013
+ break;
1014
+ case 'ident':
1015
+ user.username = val;
1016
+ break;
1017
+ case 'hostname':
1018
+ user.host = val;
1019
+ break;
1020
+ default:
1021
+ }
1022
+ });
1023
+ }
1024
+ }
1025
+
1026
+ if (command === 'channel info') {
1027
+ let buffer = network.bufferByName(event.channel);
1028
+ if (!buffer) {
1029
+ return;
1030
+ }
1031
+
1032
+ if (event.modes) {
1033
+ let modeStrs = [];
1034
+
1035
+ event.modes.forEach((mode) => {
1036
+ let adding = mode.mode[0] === '+';
1037
+ let modeChar = mode.mode.substr(1);
1038
+
1039
+ if (adding) {
1040
+ state.$set(buffer.modes, modeChar, mode.param);
1041
+ } else if (!adding) {
1042
+ state.$delete(buffer.modes, modeChar);
1043
+ }
1044
+
1045
+ modeStrs.push(mode.mode + (mode.param ? ' ' + mode.param : ''));
1046
+ });
1047
+
1048
+ if (buffer.flags.requested_modes) {
1049
+ state.addMessage(buffer, {
1050
+ time: eventTime,
1051
+ server_time: serverTime,
1052
+ nick: '*',
1053
+ message: buffer.name + ' ' + modeStrs.join(', '),
1054
+ });
1055
+ }
1056
+ }
1057
+
1058
+ if (event.created_at) {
1059
+ buffer.created_at = new Date(event.created_at * 1000);
1060
+ }
1061
+
1062
+ if (event.created_at && buffer.flags.requested_modes) {
1063
+ let tFormat = buffer.setting('timestamp_full_format');
1064
+ let timeCreated = tFormat ?
1065
+ strftime(tFormat, new Date(event.created_at * 1000)) :
1066
+ (new Date(event.created_at * 1000)).toLocaleString();
1067
+
1068
+ state.addMessage(buffer, {
1069
+ time: eventTime,
1070
+ server_time: serverTime,
1071
+ nick: '*',
1072
+ message: buffer.name + ' ' + timeCreated,
1073
+ });
1074
+ }
1075
+ }
1076
+
1077
+ if (command === 'mode') {
1078
+ let buffer = network.bufferByName(event.target);
1079
+ let modeStrs = {};
1080
+ if (buffer) {
1081
+ // Join all the same mode changes together so they can be shown on one
1082
+ // line such as "prawnsalad sets +b on nick1, nick2"
1083
+ event.modes.forEach((mode) => {
1084
+ modeStrs[mode.mode] = modeStrs[mode.mode] || [];
1085
+
1086
+ // If this mode has a user prefix then we need to update the user object
1087
+ let prefix = _.find(network.ircClient.network.options.PREFIX, {
1088
+ mode: mode.mode[1],
1089
+ });
1090
+
1091
+ if (prefix) {
1092
+ let user = state.getUser(network.id, mode.param);
1093
+ if (user) {
1094
+ let adding = mode.mode[0] === '+';
1095
+ let modes = user.buffers[buffer.id].modes;
1096
+ let modeIdx = modes.indexOf(prefix.mode);
1097
+
1098
+ // Add or remove the mode from the users mode list
1099
+ if (adding && modeIdx === -1) {
1100
+ modes.push(prefix.mode);
1101
+ } else if (!adding && modeIdx > -1) {
1102
+ modes.splice(modeIdx, 1);
1103
+ }
1104
+ }
1105
+
1106
+ modeStrs[mode.mode].push({ target: mode.param });
1107
+ } else {
1108
+ // Not a user prefix, add it as a channel mode
1109
+ // TODO: Why are these not appearing as the 'channel info' command?
1110
+ let adding = mode.mode[0] === '+';
1111
+ let modeChar = mode.mode.substr(1);
1112
+
1113
+ if (adding) {
1114
+ state.$set(buffer.modes, modeChar, mode.param);
1115
+ } else if (!adding) {
1116
+ state.$delete(buffer.modes, modeChar);
1117
+ }
1118
+
1119
+ modeStrs[mode.mode].push({ target: buffer.name, param: mode.param });
1120
+ }
1121
+ });
1122
+
1123
+ // Mode -> locale ID mappings
1124
+ // If a mode isn't found here, the local ID modes_other is used
1125
+ let modeLocaleIds = {
1126
+ '+o': 'modes_give_ops',
1127
+ '-o': 'modes_take_ops',
1128
+ '+h': 'modes_give_halfops',
1129
+ '-h': 'modes_take_halfops',
1130
+ '+v': 'modes_give_voice',
1131
+ '-v': 'modes_take_voice',
1132
+ '+a': 'modes_give_admin',
1133
+ '-a': 'modes_take_admin',
1134
+ '+q': 'modes_give_owner',
1135
+ '-q': 'modes_take_owner',
1136
+ '+b': 'modes_gives_ban',
1137
+ '-b': 'modes_takes_ban',
1138
+ };
1139
+
1140
+ let prefixes = network.ircClient.network.options.PREFIX;
1141
+ Object.keys(modeLocaleIds).forEach((mode) => {
1142
+ let supported = mode[1] === 'b' || prefixes.find((p) => mode[1] === p.mode);
1143
+ if (!supported) {
1144
+ delete modeLocaleIds[mode];
1145
+ }
1146
+ });
1147
+
1148
+ // Some modes have specific data for its locale data while most
1149
+ // use a default. The returned objects are passed to the translation
1150
+ // functions to build the translation
1151
+ let modeLocaleDataBuilders = {
1152
+ default(targets, mode) {
1153
+ return {
1154
+ mode: mode + (targets[0].param ? ' ' + targets[0].param : ''),
1155
+ target: targets.map((t) => t.target).join(', '),
1156
+ nick: event.nick,
1157
+ };
1158
+ },
1159
+ b(targets, mode) {
1160
+ return {
1161
+ mode: mode,
1162
+ target: targets[0].param ? targets[0].param : '',
1163
+ nick: event.nick,
1164
+ };
1165
+ },
1166
+ };
1167
+
1168
+ // Show one line per mode, listing each effecting user
1169
+ _.each(modeStrs, (targets, mode) => {
1170
+ let targetNicks = targets.map((t) => t.target);
1171
+ let ignoreEvent = state.setting('skipHiddenMessages') && !buffer.setting('show_mode_changes');
1172
+ let isUserInvolved = [event.nick, ...targetNicks].includes(client.user.nick);
1173
+ if (ignoreEvent && !isUserInvolved) {
1174
+ return;
1175
+ }
1176
+
1177
+ // Find a locale data builder for this mode
1178
+ let builders = modeLocaleDataBuilders;
1179
+ let localeDataFn = builders[mode[1]] || builders.default;
1180
+ let localeData = localeDataFn(targets, mode);
1181
+
1182
+ // Translate using the built locale data
1183
+ let localeKey = modeLocaleIds[mode] || 'modes_other';
1184
+ let text = TextFormatting.t(localeKey, localeData);
1185
+
1186
+ let messageBody = TextFormatting.formatText('mode', {
1187
+ nick: event.nick,
1188
+ username: event.ident,
1189
+ host: event.hostname,
1190
+ target: targetNicks.join(', '),
1191
+ text,
1192
+ });
1193
+
1194
+ let typeExtra = (isUserInvolved) ?
1195
+ 'mode_self' :
1196
+ '';
1197
+
1198
+ state.addMessage(buffer, {
1199
+ time: eventTime,
1200
+ server_time: serverTime,
1201
+ nick: '',
1202
+ message: messageBody,
1203
+ type: 'mode',
1204
+ type_extra: typeExtra,
1205
+ });
1206
+ });
1207
+ } else {
1208
+ // target is not a channel buffer (user mode ?)
1209
+ // if mode had param, show in a new line
1210
+ let modeslines = {};
1211
+
1212
+ // Group each - or + modes to each of their own message lines
1213
+ event.modes.forEach((mode) => {
1214
+ if (mode.param) {
1215
+ modeslines[mode.mode] = ' ' + mode.param;
1216
+ } else if (mode.mode[0] === '-') {
1217
+ if (!modeslines['-']) {
1218
+ modeslines['-'] = '';
1219
+ }
1220
+ modeslines['-'] += mode.mode.slice(1);
1221
+ } else {
1222
+ if (!modeslines['+']) {
1223
+ modeslines['+'] = '';
1224
+ }
1225
+ if (mode.mode[0] === '+') {
1226
+ modeslines['+'] += mode.mode.slice(1);
1227
+ } else {
1228
+ modeslines['+'] += mode.mode;
1229
+ }
1230
+ }
1231
+ });
1232
+
1233
+ let serverBuffer = network.serverBuffer();
1234
+ _.each(modeslines, (mode, value) => {
1235
+ let text = TextFormatting.t('modes_other', {
1236
+ nick: event.nick,
1237
+ target: event.target,
1238
+ mode: value + mode,
1239
+ });
1240
+ let messageBody = TextFormatting.formatText('mode', {
1241
+ nick: event.nick,
1242
+ username: event.ident,
1243
+ host: event.hostname,
1244
+ target: event.target,
1245
+ text,
1246
+ });
1247
+ state.addMessage(serverBuffer, {
1248
+ time: eventTime,
1249
+ server_time: serverTime,
1250
+ nick: '',
1251
+ message: messageBody,
1252
+ type: 'mode',
1253
+ type_extra: 'mode_self',
1254
+ });
1255
+ });
1256
+ }
1257
+ }
1258
+
1259
+ if (command === 'banlist') {
1260
+ let buffer = state.getBufferByName(networkid, event.channel);
1261
+ if (buffer && buffer.flags.requested_banlist) {
1262
+ if (!event.bans || event.bans.length === 0) {
1263
+ state.addMessage(buffer, {
1264
+ time: eventTime,
1265
+ server_time: serverTime,
1266
+ nick: '',
1267
+ message: TextFormatting.t('bans_nobody'),
1268
+ type: 'banlist',
1269
+ });
1270
+ } else {
1271
+ let banText = '';
1272
+ _.each(event.bans, (ban) => {
1273
+ let dateStr = (new Date(ban.banned_at * 1000)).toDateString();
1274
+ banText += `+b ${ban.banned} [by ${ban.banned_by}, ${dateStr}]\n`;
1275
+ });
1276
+
1277
+ state.addMessage(buffer, {
1278
+ time: eventTime,
1279
+ server_time: serverTime,
1280
+ nick: '*',
1281
+ message: banText,
1282
+ type: 'banlist',
1283
+ });
1284
+ }
1285
+ buffer.flags.requested_banlist = false;
1286
+ }
1287
+ }
1288
+
1289
+ if (command === 'topic') {
1290
+ let buffer = state.getOrAddBufferByName(networkid, event.channel);
1291
+ buffer.topic = event.topic || '';
1292
+
1293
+ let typeExtra = '';
1294
+ let messageBody = '';
1295
+
1296
+ if (event.nick) {
1297
+ typeExtra = 'topic_change';
1298
+ messageBody = TextFormatting.formatAndT(
1299
+ 'channel_topic',
1300
+ null,
1301
+ 'changed_topic_to',
1302
+ { nick: event.nick, topic: buffer.topic },
1303
+ );
1304
+ } else if (buffer.topic.trim()) {
1305
+ typeExtra = 'topic_join';
1306
+ messageBody = TextFormatting.formatText('channel_topic', buffer.topic);
1307
+ }
1308
+
1309
+ if (messageBody) {
1310
+ state.addMessage(buffer, {
1311
+ time: eventTime,
1312
+ server_time: serverTime,
1313
+ nick: '',
1314
+ message: messageBody,
1315
+ type: 'topic',
1316
+ type_extra: typeExtra,
1317
+ });
1318
+ }
1319
+ }
1320
+
1321
+ if (command === 'help') {
1322
+ let buffer = state.getOrAddBufferByName(networkid, '*help');
1323
+ state.addMessage(buffer, {
1324
+ time: eventTime,
1325
+ server_time: serverTime,
1326
+ nick: '',
1327
+ message: event.help,
1328
+ type: 'help',
1329
+ tags: event.tags || {},
1330
+ });
1331
+ }
1332
+
1333
+ if (command === 'ctcp response' || command === 'ctcp request') {
1334
+ let buffer = network.bufferByName(event.target) || network.serverBuffer();
1335
+ let textFormatId = command === 'ctcp response' ?
1336
+ 'ctcp_response' :
1337
+ 'ctcp_request';
1338
+ let messageBody = TextFormatting.formatText(textFormatId, {
1339
+ nick: event.nick,
1340
+ message: event.message,
1341
+ type: event.type,
1342
+ });
1343
+
1344
+ state.addMessage(buffer, {
1345
+ time: eventTime,
1346
+ server_time: serverTime,
1347
+ nick: '',
1348
+ message: messageBody,
1349
+ type: 'error',
1350
+ });
1351
+
1352
+ if (command === 'ctcp request' && event.type === 'VERSION') {
1353
+ let target = event.from_server ?
1354
+ event.hostname :
1355
+ event.nick;
1356
+ client.ctcpResponse(target, 'VERSION', 'Kiwi IRC');
1357
+ }
1358
+ }
1359
+
1360
+ if (command === 'nick invalid') {
1361
+ let messageBody = TextFormatting.formatText('general_error', {
1362
+ text: event.reason,
1363
+ });
1364
+ let buffer = state.getActiveBuffer();
1365
+ state.addMessage(buffer, {
1366
+ time: eventTime,
1367
+ server_time: serverTime,
1368
+ nick: '',
1369
+ message: messageBody,
1370
+ type: 'error',
1371
+ });
1372
+
1373
+ if (!isRegistered) {
1374
+ network.last_error_numeric = 432;
1375
+ network.last_error = event.reason;
1376
+ network.ircClient.quit();
1377
+ }
1378
+ }
1379
+
1380
+ if (command === 'loggedin' || command === 'loggedout') {
1381
+ let translationKey = command === 'loggedin' ? 'logged_in' : 'logged_out';
1382
+
1383
+ let buffers = [network.serverBuffer()];
1384
+ let activeBuffer = state.getActiveBuffer();
1385
+ if (activeBuffer?.networkid === network.id && activeBuffer !== buffers[0]) {
1386
+ buffers.push(activeBuffer);
1387
+ }
1388
+
1389
+ let messageBody = TextFormatting.formatAndT(
1390
+ 'notice',
1391
+ null,
1392
+ translationKey,
1393
+ { account: event.account }
1394
+ );
1395
+
1396
+ buffers.forEach((buffer) => {
1397
+ state.addMessage(buffer, {
1398
+ time: Date.now(),
1399
+ nick: '',
1400
+ message: messageBody,
1401
+ type: 'notice',
1402
+ });
1403
+ });
1404
+ }
1405
+
1406
+ if (command === 'sasl failed') {
1407
+ let failMessage = TextFormatting.formatAndT(
1408
+ 'general_error',
1409
+ null,
1410
+ 'login_failed'
1411
+ );
1412
+
1413
+ let buffers = [network.serverBuffer()];
1414
+ let activeBuffer = state.getActiveBuffer();
1415
+ if (activeBuffer?.networkid === network.id && activeBuffer !== buffers[0]) {
1416
+ buffers.push(activeBuffer);
1417
+ }
1418
+
1419
+ let disconnectOnSaslFail = network.ircClient.connection.options.sasl_disconnect_on_fail;
1420
+
1421
+ if (disconnectOnSaslFail && network.state !== 'connected') {
1422
+ // The client is going to disconnect set last_error so it is show to the user
1423
+ network.last_error = failMessage;
1424
+ }
1425
+
1426
+ if (!disconnectOnSaslFail) {
1427
+ // Only add messages if the client is not going to disconnect on failure
1428
+ buffers.forEach((buffer) => {
1429
+ state.addMessage(buffer, {
1430
+ time: Date.now(),
1431
+ nick: '',
1432
+ message: failMessage,
1433
+ type: 'error',
1434
+ });
1435
+ });
1436
+ }
1437
+ }
1438
+
1439
+ if (command === 'irc error') {
1440
+ let buffer;
1441
+ if (event.channel || event.nick) {
1442
+ buffer = state.getOrAddBufferByName(network.id, event.channel || event.nick);
1443
+ }
1444
+ if (!buffer) {
1445
+ buffer = network.serverBuffer();
1446
+ }
1447
+
1448
+ if (!buffer) {
1449
+ // we could not find a buffer, this is likely because the network was removed
1450
+ return;
1451
+ }
1452
+
1453
+ // TODO: Some of these errors contain a .error property which we can match against,
1454
+ // ie. password_mismatch.
1455
+
1456
+ if (event.error === 'bad_channel_key') {
1457
+ buffer.flags.channel_badkey = true;
1458
+ }
1459
+
1460
+ // ignore error 432 (erroneous nickname) as it is handled above
1461
+ if (event.reason && network.last_error_numeric !== 432) {
1462
+ if (!isRegistered) {
1463
+ network.last_error = event.reason;
1464
+ }
1465
+
1466
+ let messageRaw = event.reason || event.error;
1467
+ if (event.error === 'unknown_command') {
1468
+ messageRaw = `${messageRaw} "${event.command}"`;
1469
+ }
1470
+
1471
+ let messageBody = TextFormatting.formatText('general_error', {
1472
+ text: messageRaw,
1473
+ });
1474
+
1475
+ let message = {
1476
+ time: eventTime,
1477
+ server_time: serverTime,
1478
+ nick: '',
1479
+ message: messageBody,
1480
+ type: 'error',
1481
+ };
1482
+
1483
+ if (event.error === 'cannot_send_to_channel') {
1484
+ // Only add this message if it does not match the previous message
1485
+ // Typing status messages can cause a spam of this error type
1486
+ state.addMessageNoRepeat(buffer, message);
1487
+ return;
1488
+ }
1489
+
1490
+ state.addMessage(buffer, message);
1491
+ }
1492
+
1493
+ // Getting an error about a channel while we are not joined means that we couldn't join
1494
+ // or do some action on it. Disable it until we manually reattempt to join.
1495
+ if (buffer.isChannel() && !buffer.joined) {
1496
+ buffer.enabled = false;
1497
+ }
1498
+ }
1499
+
1500
+ next();
1501
+ }
1502
+ }
1503
+
1504
+ function rand(min, max) {
1505
+ return Math.floor(Math.random() * (max - min + 1) + min);
1506
+ }