@actagent/irc 2026.6.2
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/actagent.plugin.json +26 -0
- package/api.ts +11 -0
- package/channel-config-api.ts +2 -0
- package/channel-plugin-api.ts +3 -0
- package/configured-state.ts +9 -0
- package/contract-api.ts +5 -0
- package/index.test.ts +14 -0
- package/index.ts +21 -0
- package/package.json +44 -0
- package/runtime-api.test.ts +24 -0
- package/runtime-api.ts +3 -0
- package/secret-contract-api.ts +6 -0
- package/setup-entry.ts +14 -0
- package/src/accounts.test.ts +224 -0
- package/src/accounts.ts +240 -0
- package/src/channel-api.ts +7 -0
- package/src/channel-runtime.ts +4 -0
- package/src/channel.test.ts +17 -0
- package/src/channel.ts +367 -0
- package/src/client.test.ts +44 -0
- package/src/client.ts +443 -0
- package/src/config-schema.test.ts +117 -0
- package/src/config-schema.ts +97 -0
- package/src/config-ui-hints.ts +41 -0
- package/src/connect-options.test.ts +48 -0
- package/src/connect-options.ts +31 -0
- package/src/control-chars.test.ts +18 -0
- package/src/control-chars.ts +23 -0
- package/src/doctor.ts +55 -0
- package/src/gateway.ts +54 -0
- package/src/inbound.behavior.test.ts +247 -0
- package/src/inbound.ts +440 -0
- package/src/message-adapter.ts +29 -0
- package/src/monitor.test.ts +44 -0
- package/src/monitor.ts +150 -0
- package/src/normalize.test.ts +56 -0
- package/src/normalize.ts +111 -0
- package/src/outbound-base.ts +11 -0
- package/src/policy.test.ts +56 -0
- package/src/policy.ts +79 -0
- package/src/probe.test.ts +111 -0
- package/src/probe.ts +54 -0
- package/src/protocol.test.ts +49 -0
- package/src/protocol.ts +170 -0
- package/src/runtime-api.ts +42 -0
- package/src/runtime.ts +16 -0
- package/src/secret-contract.ts +104 -0
- package/src/send.test.ts +327 -0
- package/src/send.ts +122 -0
- package/src/setup-core.ts +152 -0
- package/src/setup-surface.ts +451 -0
- package/src/setup.test.ts +487 -0
- package/src/types.ts +101 -0
- package/tsconfig.json +16 -0
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
// Irc tests cover channel plugin behavior.
|
|
2
|
+
import { afterEach, describe, expect, it } from "vitest";
|
|
3
|
+
import { ircOutboundBaseAdapter } from "./outbound-base.js";
|
|
4
|
+
import { clearIrcRuntime } from "./runtime.js";
|
|
5
|
+
|
|
6
|
+
describe("irc outbound chunking", () => {
|
|
7
|
+
afterEach(() => {
|
|
8
|
+
clearIrcRuntime();
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
it("chunks outbound text without requiring IRC runtime initialization", () => {
|
|
12
|
+
expect(ircOutboundBaseAdapter.chunker("alpha beta", 5)).toEqual(["alpha", "beta"]);
|
|
13
|
+
expect(ircOutboundBaseAdapter.deliveryMode).toBe("direct");
|
|
14
|
+
expect(ircOutboundBaseAdapter.chunkerMode).toBe("markdown");
|
|
15
|
+
expect(ircOutboundBaseAdapter.textChunkLimit).toBe(350);
|
|
16
|
+
});
|
|
17
|
+
});
|
package/src/channel.ts
ADDED
|
@@ -0,0 +1,367 @@
|
|
|
1
|
+
// Irc plugin module implements channel behavior.
|
|
2
|
+
import { describeAccountSnapshot } from "actagent/plugin-sdk/account-helpers";
|
|
3
|
+
import { formatNormalizedAllowFromEntries } from "actagent/plugin-sdk/allow-from";
|
|
4
|
+
import {
|
|
5
|
+
adaptScopedAccountAccessor,
|
|
6
|
+
createScopedChannelConfigAdapter,
|
|
7
|
+
createScopedDmSecurityResolver,
|
|
8
|
+
} from "actagent/plugin-sdk/channel-config-helpers";
|
|
9
|
+
import { createChatChannelPlugin } from "actagent/plugin-sdk/channel-core";
|
|
10
|
+
import {
|
|
11
|
+
composeAccountWarningCollectors,
|
|
12
|
+
createAllowlistProviderOpenWarningCollector,
|
|
13
|
+
} from "actagent/plugin-sdk/channel-policy";
|
|
14
|
+
import {
|
|
15
|
+
createChannelDirectoryAdapter,
|
|
16
|
+
createResolvedDirectoryEntriesLister,
|
|
17
|
+
} from "actagent/plugin-sdk/directory-runtime";
|
|
18
|
+
import {
|
|
19
|
+
createComputedAccountStatusAdapter,
|
|
20
|
+
createDefaultChannelRuntimeState,
|
|
21
|
+
} from "actagent/plugin-sdk/status-helpers";
|
|
22
|
+
import {
|
|
23
|
+
listIrcAccountIds,
|
|
24
|
+
resolveDefaultIrcAccountId,
|
|
25
|
+
resolveIrcAccount,
|
|
26
|
+
type ResolvedIrcAccount,
|
|
27
|
+
} from "./accounts.js";
|
|
28
|
+
import {
|
|
29
|
+
buildBaseChannelStatusSummary,
|
|
30
|
+
DEFAULT_ACCOUNT_ID,
|
|
31
|
+
PAIRING_APPROVED_MESSAGE,
|
|
32
|
+
type ChannelPlugin,
|
|
33
|
+
} from "./channel-api.js";
|
|
34
|
+
import { IrcChannelConfigSchema } from "./config-schema.js";
|
|
35
|
+
import { collectIrcMutableAllowlistWarnings } from "./doctor.js";
|
|
36
|
+
import { startIrcGatewayAccount } from "./gateway.js";
|
|
37
|
+
import { ircMessageAdapter } from "./message-adapter.js";
|
|
38
|
+
import {
|
|
39
|
+
isChannelTarget,
|
|
40
|
+
looksLikeIrcTargetId,
|
|
41
|
+
normalizeIrcAllowEntry,
|
|
42
|
+
normalizeIrcMessagingTarget,
|
|
43
|
+
} from "./normalize.js";
|
|
44
|
+
import { ircOutboundBaseAdapter } from "./outbound-base.js";
|
|
45
|
+
import { resolveIrcGroupMatch, resolveIrcRequireMention } from "./policy.js";
|
|
46
|
+
import { probeIrc } from "./probe.js";
|
|
47
|
+
import { collectRuntimeConfigAssignments, secretTargetRegistryEntries } from "./secret-contract.js";
|
|
48
|
+
import { ircSetupAdapter } from "./setup-core.js";
|
|
49
|
+
import { ircSetupWizard } from "./setup-surface.js";
|
|
50
|
+
import type { CoreConfig, IrcProbe } from "./types.js";
|
|
51
|
+
|
|
52
|
+
const meta = {
|
|
53
|
+
id: "irc",
|
|
54
|
+
label: "IRC",
|
|
55
|
+
selectionLabel: "IRC (Server + Nick)",
|
|
56
|
+
docsPath: "/channels/irc",
|
|
57
|
+
docsLabel: "irc",
|
|
58
|
+
blurb: "classic IRC networks; host, nick, channels.",
|
|
59
|
+
order: 80,
|
|
60
|
+
detailLabel: "IRC",
|
|
61
|
+
systemImage: "number",
|
|
62
|
+
markdownCapable: true,
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
type IrcChannelRuntimeModule = typeof import("./channel-runtime.js");
|
|
66
|
+
|
|
67
|
+
let ircChannelRuntimePromise: Promise<IrcChannelRuntimeModule> | undefined;
|
|
68
|
+
|
|
69
|
+
async function loadIrcChannelRuntime(): Promise<IrcChannelRuntimeModule> {
|
|
70
|
+
ircChannelRuntimePromise ??= import("./channel-runtime.js");
|
|
71
|
+
return await ircChannelRuntimePromise;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function normalizePairingTarget(raw: string): string {
|
|
75
|
+
const normalized = normalizeIrcAllowEntry(raw);
|
|
76
|
+
if (!normalized) {
|
|
77
|
+
return "";
|
|
78
|
+
}
|
|
79
|
+
return normalized.split(/[!@]/, 1)[0]?.trim() ?? "";
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const listIrcDirectoryPeersFromConfig = createResolvedDirectoryEntriesLister<ResolvedIrcAccount>({
|
|
83
|
+
kind: "user",
|
|
84
|
+
resolveAccount: adaptScopedAccountAccessor(resolveIrcAccount),
|
|
85
|
+
resolveSources: (account) => [
|
|
86
|
+
account.config.allowFrom ?? [],
|
|
87
|
+
account.config.groupAllowFrom ?? [],
|
|
88
|
+
...Object.values(account.config.groups ?? {}).map((group) => group.allowFrom ?? []),
|
|
89
|
+
],
|
|
90
|
+
normalizeId: (entry) => normalizePairingTarget(entry) || null,
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
const listIrcDirectoryGroupsFromConfig = createResolvedDirectoryEntriesLister<ResolvedIrcAccount>({
|
|
94
|
+
kind: "group",
|
|
95
|
+
resolveAccount: adaptScopedAccountAccessor(resolveIrcAccount),
|
|
96
|
+
resolveSources: (account) => [
|
|
97
|
+
account.config.channels ?? [],
|
|
98
|
+
Object.keys(account.config.groups ?? {}),
|
|
99
|
+
],
|
|
100
|
+
normalizeId: (entry) => {
|
|
101
|
+
const normalized = normalizeIrcMessagingTarget(entry);
|
|
102
|
+
return normalized && isChannelTarget(normalized) ? normalized : null;
|
|
103
|
+
},
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
const ircConfigAdapter = createScopedChannelConfigAdapter<ResolvedIrcAccount, ResolvedIrcAccount>({
|
|
107
|
+
sectionKey: "irc",
|
|
108
|
+
listAccountIds: listIrcAccountIds,
|
|
109
|
+
resolveAccount: adaptScopedAccountAccessor(resolveIrcAccount),
|
|
110
|
+
defaultAccountId: resolveDefaultIrcAccountId,
|
|
111
|
+
clearBaseFields: [
|
|
112
|
+
"name",
|
|
113
|
+
"host",
|
|
114
|
+
"port",
|
|
115
|
+
"tls",
|
|
116
|
+
"nick",
|
|
117
|
+
"username",
|
|
118
|
+
"realname",
|
|
119
|
+
"password",
|
|
120
|
+
"passwordFile",
|
|
121
|
+
"channels",
|
|
122
|
+
],
|
|
123
|
+
resolveAllowFrom: (account: ResolvedIrcAccount) => account.config.allowFrom,
|
|
124
|
+
formatAllowFrom: (allowFrom) =>
|
|
125
|
+
formatNormalizedAllowFromEntries({
|
|
126
|
+
allowFrom,
|
|
127
|
+
normalizeEntry: normalizeIrcAllowEntry,
|
|
128
|
+
}),
|
|
129
|
+
resolveDefaultTo: (account: ResolvedIrcAccount) => account.config.defaultTo,
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
const resolveIrcDmPolicy = createScopedDmSecurityResolver<ResolvedIrcAccount>({
|
|
133
|
+
channelKey: "irc",
|
|
134
|
+
resolvePolicy: (account) => account.config.dmPolicy,
|
|
135
|
+
resolveAllowFrom: (account) => account.config.allowFrom,
|
|
136
|
+
policyPathSuffix: "dmPolicy",
|
|
137
|
+
normalizeEntry: (raw) => normalizeIrcAllowEntry(raw),
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
const collectIrcGroupPolicyWarnings =
|
|
141
|
+
createAllowlistProviderOpenWarningCollector<ResolvedIrcAccount>({
|
|
142
|
+
providerConfigPresent: (cfg) => cfg.channels?.irc !== undefined,
|
|
143
|
+
resolveGroupPolicy: (account) => account.config.groupPolicy,
|
|
144
|
+
buildOpenWarning: {
|
|
145
|
+
surface: "IRC channels",
|
|
146
|
+
openBehavior: "allows all channels and senders (mention-gated)",
|
|
147
|
+
remediation: 'Prefer channels.irc.groupPolicy="allowlist" with channels.irc.groups',
|
|
148
|
+
},
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
const collectIrcSecurityWarnings = composeAccountWarningCollectors<
|
|
152
|
+
ResolvedIrcAccount,
|
|
153
|
+
{
|
|
154
|
+
account: ResolvedIrcAccount;
|
|
155
|
+
cfg: CoreConfig;
|
|
156
|
+
}
|
|
157
|
+
>(
|
|
158
|
+
collectIrcGroupPolicyWarnings,
|
|
159
|
+
(account) =>
|
|
160
|
+
!account.config.tls &&
|
|
161
|
+
"- IRC TLS is disabled (channels.irc.tls=false); traffic and credentials are plaintext.",
|
|
162
|
+
(account) =>
|
|
163
|
+
account.config.nickserv?.register &&
|
|
164
|
+
'- IRC NickServ registration is enabled (channels.irc.nickserv.register=true); this sends "REGISTER" on every connect. Disable after first successful registration.',
|
|
165
|
+
(account) =>
|
|
166
|
+
account.config.nickserv?.register &&
|
|
167
|
+
!account.config.nickserv.password?.trim() &&
|
|
168
|
+
"- IRC NickServ registration is enabled but no NickServ password is resolved; set channels.irc.nickserv.password, channels.irc.nickserv.passwordFile, or IRC_NICKSERV_PASSWORD.",
|
|
169
|
+
);
|
|
170
|
+
|
|
171
|
+
export const ircPlugin: ChannelPlugin<ResolvedIrcAccount, IrcProbe> = createChatChannelPlugin({
|
|
172
|
+
base: {
|
|
173
|
+
id: "irc",
|
|
174
|
+
meta: {
|
|
175
|
+
...meta,
|
|
176
|
+
quickstartAllowFrom: true,
|
|
177
|
+
},
|
|
178
|
+
setup: ircSetupAdapter,
|
|
179
|
+
setupWizard: ircSetupWizard,
|
|
180
|
+
capabilities: {
|
|
181
|
+
chatTypes: ["direct", "group"],
|
|
182
|
+
media: true,
|
|
183
|
+
blockStreaming: true,
|
|
184
|
+
},
|
|
185
|
+
reload: { configPrefixes: ["channels.irc"] },
|
|
186
|
+
configSchema: IrcChannelConfigSchema,
|
|
187
|
+
config: {
|
|
188
|
+
...ircConfigAdapter,
|
|
189
|
+
hasConfiguredState: ({ env }) =>
|
|
190
|
+
typeof env?.IRC_HOST === "string" &&
|
|
191
|
+
env.IRC_HOST.trim().length > 0 &&
|
|
192
|
+
typeof env?.IRC_NICK === "string" &&
|
|
193
|
+
env.IRC_NICK.trim().length > 0,
|
|
194
|
+
isConfigured: (account) => account.configured,
|
|
195
|
+
describeAccount: (account) =>
|
|
196
|
+
describeAccountSnapshot({
|
|
197
|
+
account,
|
|
198
|
+
configured: account.configured,
|
|
199
|
+
extra: {
|
|
200
|
+
host: account.host,
|
|
201
|
+
port: account.port,
|
|
202
|
+
tls: account.tls,
|
|
203
|
+
nick: account.nick,
|
|
204
|
+
passwordSource: account.passwordSource,
|
|
205
|
+
},
|
|
206
|
+
}),
|
|
207
|
+
},
|
|
208
|
+
secrets: {
|
|
209
|
+
secretTargetRegistryEntries,
|
|
210
|
+
collectRuntimeConfigAssignments,
|
|
211
|
+
},
|
|
212
|
+
doctor: {
|
|
213
|
+
groupAllowFromFallbackToAllowFrom: false,
|
|
214
|
+
collectMutableAllowlistWarnings: collectIrcMutableAllowlistWarnings,
|
|
215
|
+
},
|
|
216
|
+
groups: {
|
|
217
|
+
resolveRequireMention: ({ cfg, accountId, groupId }) => {
|
|
218
|
+
const account = resolveIrcAccount({ cfg: cfg as CoreConfig, accountId });
|
|
219
|
+
if (!groupId) {
|
|
220
|
+
return true;
|
|
221
|
+
}
|
|
222
|
+
const match = resolveIrcGroupMatch({ groups: account.config.groups, target: groupId });
|
|
223
|
+
return resolveIrcRequireMention({
|
|
224
|
+
groupConfig: match.groupConfig,
|
|
225
|
+
wildcardConfig: match.wildcardConfig,
|
|
226
|
+
});
|
|
227
|
+
},
|
|
228
|
+
resolveToolPolicy: ({ cfg, accountId, groupId }) => {
|
|
229
|
+
const account = resolveIrcAccount({ cfg: cfg as CoreConfig, accountId });
|
|
230
|
+
if (!groupId) {
|
|
231
|
+
return undefined;
|
|
232
|
+
}
|
|
233
|
+
const match = resolveIrcGroupMatch({ groups: account.config.groups, target: groupId });
|
|
234
|
+
return match.groupConfig?.tools ?? match.wildcardConfig?.tools;
|
|
235
|
+
},
|
|
236
|
+
},
|
|
237
|
+
messaging: {
|
|
238
|
+
targetPrefixes: ["irc"],
|
|
239
|
+
normalizeTarget: normalizeIrcMessagingTarget,
|
|
240
|
+
targetResolver: {
|
|
241
|
+
looksLikeId: looksLikeIrcTargetId,
|
|
242
|
+
hint: "<#channel|nick>",
|
|
243
|
+
},
|
|
244
|
+
},
|
|
245
|
+
message: ircMessageAdapter,
|
|
246
|
+
resolver: {
|
|
247
|
+
resolveTargets: async ({ inputs, kind }) => {
|
|
248
|
+
return inputs.map((input) => {
|
|
249
|
+
const normalized = normalizeIrcMessagingTarget(input);
|
|
250
|
+
if (!normalized) {
|
|
251
|
+
return {
|
|
252
|
+
input,
|
|
253
|
+
resolved: false,
|
|
254
|
+
note: "invalid IRC target",
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
if (kind === "group") {
|
|
258
|
+
const groupId = isChannelTarget(normalized) ? normalized : `#${normalized}`;
|
|
259
|
+
return {
|
|
260
|
+
input,
|
|
261
|
+
resolved: true,
|
|
262
|
+
id: groupId,
|
|
263
|
+
name: groupId,
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
if (isChannelTarget(normalized)) {
|
|
267
|
+
return {
|
|
268
|
+
input,
|
|
269
|
+
resolved: false,
|
|
270
|
+
note: "expected user target",
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
return {
|
|
274
|
+
input,
|
|
275
|
+
resolved: true,
|
|
276
|
+
id: normalized,
|
|
277
|
+
name: normalized,
|
|
278
|
+
};
|
|
279
|
+
});
|
|
280
|
+
},
|
|
281
|
+
},
|
|
282
|
+
directory: createChannelDirectoryAdapter({
|
|
283
|
+
listPeers: async (params) => listIrcDirectoryPeersFromConfig(params),
|
|
284
|
+
listGroups: async (params) => {
|
|
285
|
+
const entries = await listIrcDirectoryGroupsFromConfig(params);
|
|
286
|
+
return entries.map((entry) => Object.assign({}, entry, { name: entry.id }));
|
|
287
|
+
},
|
|
288
|
+
}),
|
|
289
|
+
status: createComputedAccountStatusAdapter<ResolvedIrcAccount, IrcProbe>({
|
|
290
|
+
defaultRuntime: createDefaultChannelRuntimeState(DEFAULT_ACCOUNT_ID),
|
|
291
|
+
buildChannelSummary: ({ account, snapshot }) => ({
|
|
292
|
+
...buildBaseChannelStatusSummary(snapshot),
|
|
293
|
+
host: account.host,
|
|
294
|
+
port: snapshot.port,
|
|
295
|
+
tls: account.tls,
|
|
296
|
+
nick: account.nick,
|
|
297
|
+
probe: snapshot.probe,
|
|
298
|
+
lastProbeAt: snapshot.lastProbeAt ?? null,
|
|
299
|
+
}),
|
|
300
|
+
probeAccount: async ({ cfg, account, timeoutMs }) =>
|
|
301
|
+
probeIrc(cfg as CoreConfig, { accountId: account.accountId, timeoutMs }),
|
|
302
|
+
resolveAccountSnapshot: ({ account }) => ({
|
|
303
|
+
accountId: account.accountId,
|
|
304
|
+
name: account.name,
|
|
305
|
+
enabled: account.enabled,
|
|
306
|
+
configured: account.configured,
|
|
307
|
+
extra: {
|
|
308
|
+
host: account.host,
|
|
309
|
+
port: account.port,
|
|
310
|
+
tls: account.tls,
|
|
311
|
+
nick: account.nick,
|
|
312
|
+
passwordSource: account.passwordSource,
|
|
313
|
+
},
|
|
314
|
+
}),
|
|
315
|
+
}),
|
|
316
|
+
gateway: {
|
|
317
|
+
startAccount: async (ctx) =>
|
|
318
|
+
await startIrcGatewayAccount({
|
|
319
|
+
...ctx,
|
|
320
|
+
cfg: ctx.cfg as CoreConfig,
|
|
321
|
+
}),
|
|
322
|
+
},
|
|
323
|
+
},
|
|
324
|
+
pairing: {
|
|
325
|
+
text: {
|
|
326
|
+
idLabel: "ircUser",
|
|
327
|
+
message: PAIRING_APPROVED_MESSAGE,
|
|
328
|
+
normalizeAllowEntry: (entry) => normalizeIrcAllowEntry(entry),
|
|
329
|
+
notify: async ({ cfg, id, message }) => {
|
|
330
|
+
const target = normalizePairingTarget(id);
|
|
331
|
+
if (!target) {
|
|
332
|
+
throw new Error(`invalid IRC pairing id: ${id}`);
|
|
333
|
+
}
|
|
334
|
+
const { sendMessageIrc } = await loadIrcChannelRuntime();
|
|
335
|
+
await sendMessageIrc(target, message, {
|
|
336
|
+
cfg: cfg as CoreConfig,
|
|
337
|
+
});
|
|
338
|
+
},
|
|
339
|
+
},
|
|
340
|
+
},
|
|
341
|
+
security: {
|
|
342
|
+
resolveDmPolicy: resolveIrcDmPolicy,
|
|
343
|
+
collectWarnings: collectIrcSecurityWarnings,
|
|
344
|
+
},
|
|
345
|
+
outbound: {
|
|
346
|
+
base: ircOutboundBaseAdapter,
|
|
347
|
+
attachedResults: {
|
|
348
|
+
channel: "irc",
|
|
349
|
+
sendText: async ({ cfg, to, text, accountId, replyToId }) => {
|
|
350
|
+
const { sendMessageIrc } = await loadIrcChannelRuntime();
|
|
351
|
+
return await sendMessageIrc(to, text, {
|
|
352
|
+
cfg: cfg as CoreConfig,
|
|
353
|
+
accountId: accountId ?? undefined,
|
|
354
|
+
replyTo: replyToId ?? undefined,
|
|
355
|
+
});
|
|
356
|
+
},
|
|
357
|
+
sendMedia: async ({ cfg, to, text, mediaUrl, accountId, replyToId }) => {
|
|
358
|
+
const { sendMessageIrc } = await loadIrcChannelRuntime();
|
|
359
|
+
return await sendMessageIrc(to, mediaUrl ? `${text}\n\nAttachment: ${mediaUrl}` : text, {
|
|
360
|
+
cfg: cfg as CoreConfig,
|
|
361
|
+
accountId: accountId ?? undefined,
|
|
362
|
+
replyTo: replyToId ?? undefined,
|
|
363
|
+
});
|
|
364
|
+
},
|
|
365
|
+
},
|
|
366
|
+
},
|
|
367
|
+
});
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
// Irc tests cover client plugin behavior.
|
|
2
|
+
import { describe, expect, it } from "vitest";
|
|
3
|
+
import { buildIrcNickServCommands } from "./client.js";
|
|
4
|
+
|
|
5
|
+
describe("irc client nickserv", () => {
|
|
6
|
+
it("builds IDENTIFY command when password is set", () => {
|
|
7
|
+
expect(
|
|
8
|
+
buildIrcNickServCommands({
|
|
9
|
+
password: "secret",
|
|
10
|
+
}),
|
|
11
|
+
).toEqual(["PRIVMSG NickServ :IDENTIFY secret"]);
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
it("builds REGISTER command when enabled with email", () => {
|
|
15
|
+
expect(
|
|
16
|
+
buildIrcNickServCommands({
|
|
17
|
+
password: "secret",
|
|
18
|
+
register: true,
|
|
19
|
+
registerEmail: "bot@example.com",
|
|
20
|
+
}),
|
|
21
|
+
).toEqual([
|
|
22
|
+
"PRIVMSG NickServ :IDENTIFY secret",
|
|
23
|
+
"PRIVMSG NickServ :REGISTER secret bot@example.com",
|
|
24
|
+
]);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it("rejects register without registerEmail", () => {
|
|
28
|
+
expect(() =>
|
|
29
|
+
buildIrcNickServCommands({
|
|
30
|
+
password: "secret",
|
|
31
|
+
register: true,
|
|
32
|
+
}),
|
|
33
|
+
).toThrow(/registerEmail/);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it("sanitizes outbound NickServ payloads", () => {
|
|
37
|
+
expect(
|
|
38
|
+
buildIrcNickServCommands({
|
|
39
|
+
service: "NickServ\n",
|
|
40
|
+
password: "secret\r\nJOIN #bad",
|
|
41
|
+
}),
|
|
42
|
+
).toEqual(["PRIVMSG NickServ :IDENTIFY secret JOIN #bad"]);
|
|
43
|
+
});
|
|
44
|
+
});
|