@openclaw/irc 2026.10.10 → 2026.11.5-beta.1
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/dist/.setup/{channel-t-k-A5n4.mjs → channel-DRdPa321.mjs} +252 -294
- package/dist/.setup/channel-plugin-api-DFjxQCOi.mjs +2 -0
- package/dist/.setup/channel-runtime-D2sqTm8e.mjs +579 -0
- package/dist/.setup/{config-schema-DFqPW9AB.mjs → config-schema-CQuAQW7O.mjs} +1 -2
- package/dist/.source-module-urls/4751df78f545ae971ceb22b6e4bff6086f63313240899a1d9884b970424a305e.json +1 -1
- package/dist/api.js +1 -1
- package/dist/channel-config-api.js +1 -1
- package/dist/channel-plugin-api.js +1 -1
- package/dist/setup-entry.js +1 -1
- package/node_modules/zod/package.json +1 -1
- package/node_modules/zod/src/v4/classic/tests/array.test.ts +27 -0
- package/node_modules/zod/src/v4/classic/tests/map.test.ts +7 -0
- package/node_modules/zod/src/v4/classic/tests/set.test.ts +6 -0
- package/node_modules/zod/src/v4/core/compile.ts +9 -0
- package/node_modules/zod/src/v4/core/schemas.ts +35 -1
- package/node_modules/zod/src/v4/core/util.ts +13 -0
- package/node_modules/zod/src/v4/core/versions.ts +1 -1
- package/node_modules/zod/v4/core/compile.cjs +8 -0
- package/node_modules/zod/v4/core/compile.js +8 -0
- package/node_modules/zod/v4/core/schemas.cjs +30 -0
- package/node_modules/zod/v4/core/schemas.d.cts +3 -1
- package/node_modules/zod/v4/core/schemas.d.ts +3 -1
- package/node_modules/zod/v4/core/schemas.js +30 -0
- package/node_modules/zod/v4/core/util.cjs +15 -0
- package/node_modules/zod/v4/core/util.d.cts +2 -0
- package/node_modules/zod/v4/core/util.d.ts +2 -0
- package/node_modules/zod/v4/core/util.js +14 -0
- package/node_modules/zod/v4/core/versions.cjs +1 -1
- package/node_modules/zod/v4/core/versions.js +1 -1
- package/package.json +5 -5
- package/dist/.setup/channel-plugin-api-DRR9Fg5C.mjs +0 -2
- package/dist/.setup/channel-runtime-exb5AWhn.mjs +0 -648
|
@@ -1,648 +0,0 @@
|
|
|
1
|
-
import { a as resolveIrcGroupRequireMention, c as connectIrcClient, d as sanitizeIrcAssistantText, f as createIrcIngressSubject, h as normalizeIrcAllowEntry, i as resolveIrcGroupMatch, l as parseIrcLine, m as isChannelTarget, o as sendMessageIrc, p as ircIngressIdentity, s as buildIrcConnectOptions, u as parseIrcPrefix, y as resolveIrcAccount } from "./channel-t-k-A5n4.mjs";
|
|
2
|
-
import { t as getIrcRuntime } from "./runtime-CsdAoZex.mjs";
|
|
3
|
-
import { channelIngressRoutes } from "openclaw/plugin-sdk/channel-ingress-runtime";
|
|
4
|
-
import { computeBackoff } from "openclaw/plugin-sdk/runtime-env";
|
|
5
|
-
import { normalizeLowercaseStringOrEmpty, normalizeOptionalString, normalizeStringEntries } from "openclaw/plugin-sdk/string-coerce-runtime";
|
|
6
|
-
import { bindIngressLifecycleToReplyOptions, createChannelIngressError, createChannelIngressMonitor, resolveChannelStreamingBlockEnabled } from "openclaw/plugin-sdk/channel-outbound";
|
|
7
|
-
import { randomUUID } from "node:crypto";
|
|
8
|
-
import { createChannelInboundEnvelopeBuilderAsync, logInboundDrop } from "openclaw/plugin-sdk/channel-inbound";
|
|
9
|
-
import { resolveLoggerBackedRuntime } from "openclaw/plugin-sdk/extension-shared";
|
|
10
|
-
import { resolveAgentRoute } from "openclaw/plugin-sdk/routing";
|
|
11
|
-
import { channelReadyPatch } from "openclaw/plugin-sdk/gateway-runtime";
|
|
12
|
-
import { createChannelPairingController } from "openclaw/plugin-sdk/channel-pairing";
|
|
13
|
-
import { isDangerousNameMatchingEnabled } from "openclaw/plugin-sdk/dangerous-name-runtime";
|
|
14
|
-
import { deliverFormattedTextWithAttachments } from "openclaw/plugin-sdk/reply-payload";
|
|
15
|
-
import { GROUP_POLICY_BLOCKED_LABEL, resolveAllowlistProviderRuntimeGroupPolicy, resolveDefaultGroupPolicy, warnMissingProviderGroupPolicyFallbackOnce } from "openclaw/plugin-sdk/runtime-group-policy";
|
|
16
|
-
import { escapeRegExp } from "openclaw/plugin-sdk/text-utility-runtime";
|
|
17
|
-
// extensions/irc/src/inbound.ts
|
|
18
|
-
const CHANNEL_ID = "irc";
|
|
19
|
-
const IRC_NICK_CHARACTER = String.raw`[A-Za-z0-9_\-\[\]\\\x60^{}|~]`;
|
|
20
|
-
const IRC_RFC1459_CASE_EQUIVALENTS = /* @__PURE__ */ new Map([
|
|
21
|
-
["[", "{"],
|
|
22
|
-
["{", "["],
|
|
23
|
-
["]", "}"],
|
|
24
|
-
["}", "]"],
|
|
25
|
-
["\\", "|"],
|
|
26
|
-
["|", "\\"],
|
|
27
|
-
["^", "~"],
|
|
28
|
-
["~", "^"]
|
|
29
|
-
]);
|
|
30
|
-
function buildIrcNickMentionPattern(value) {
|
|
31
|
-
return Array.from(value, (character) => {
|
|
32
|
-
const equivalent = IRC_RFC1459_CASE_EQUIVALENTS.get(character);
|
|
33
|
-
return equivalent ? `[${escapeRegExp(character)}${escapeRegExp(equivalent)}]` : escapeRegExp(character);
|
|
34
|
-
}).join("");
|
|
35
|
-
}
|
|
36
|
-
function hasEntries(entries) {
|
|
37
|
-
return normalizeStringEntries(entries).some((entry) => normalizeIrcAllowEntry(entry));
|
|
38
|
-
}
|
|
39
|
-
async function deliverIrcReply(params) {
|
|
40
|
-
await deliverFormattedTextWithAttachments({
|
|
41
|
-
payload: {
|
|
42
|
-
...params.payload,
|
|
43
|
-
text: sanitizeIrcAssistantText(params.payload.text ?? "")
|
|
44
|
-
},
|
|
45
|
-
send: async ({ text, replyToId }) => {
|
|
46
|
-
if (params.sendReply) await params.sendReply(params.target, text, replyToId);
|
|
47
|
-
else await sendMessageIrc(params.target, text, {
|
|
48
|
-
cfg: params.cfg,
|
|
49
|
-
accountId: params.accountId,
|
|
50
|
-
replyTo: replyToId
|
|
51
|
-
});
|
|
52
|
-
params.statusSink?.({ lastOutboundAt: Date.now() });
|
|
53
|
-
}
|
|
54
|
-
});
|
|
55
|
-
}
|
|
56
|
-
async function handleIrcInbound(params) {
|
|
57
|
-
const { message, account, config, runtime, connectedNick, statusSink, turnAdoptionLifecycle } = params;
|
|
58
|
-
const core = getIrcRuntime();
|
|
59
|
-
const pairing = createChannelPairingController({
|
|
60
|
-
core,
|
|
61
|
-
channel: CHANNEL_ID,
|
|
62
|
-
accountId: account.accountId
|
|
63
|
-
});
|
|
64
|
-
const rawBody = message.text?.trim() ?? "";
|
|
65
|
-
if (!rawBody) return { kind: "completed" };
|
|
66
|
-
if (turnAdoptionLifecycle?.abortSignal.aborted) return {
|
|
67
|
-
kind: "failed-retryable",
|
|
68
|
-
error: turnAdoptionLifecycle.abortSignal.reason
|
|
69
|
-
};
|
|
70
|
-
statusSink?.({ lastInboundAt: message.timestamp });
|
|
71
|
-
const senderDisplay = message.senderHost ? `${message.senderNick}!${message.senderUser ?? "?"}@${message.senderHost}` : message.senderNick;
|
|
72
|
-
const allowNameMatching = isDangerousNameMatchingEnabled(account.config);
|
|
73
|
-
const dmPolicy = account.config.dmPolicy ?? "pairing";
|
|
74
|
-
const defaultGroupPolicy = resolveDefaultGroupPolicy(config);
|
|
75
|
-
const { groupPolicy, providerMissingFallbackApplied } = resolveAllowlistProviderRuntimeGroupPolicy({
|
|
76
|
-
providerConfigPresent: config.channels?.irc !== void 0,
|
|
77
|
-
groupPolicy: account.config.groupPolicy,
|
|
78
|
-
defaultGroupPolicy
|
|
79
|
-
});
|
|
80
|
-
warnMissingProviderGroupPolicyFallbackOnce({
|
|
81
|
-
providerMissingFallbackApplied,
|
|
82
|
-
providerKey: "irc",
|
|
83
|
-
accountId: account.accountId,
|
|
84
|
-
blockedLabel: GROUP_POLICY_BLOCKED_LABEL.channel,
|
|
85
|
-
log: (messageLocal) => runtime.log?.(messageLocal)
|
|
86
|
-
});
|
|
87
|
-
const groupMatch = resolveIrcGroupMatch({
|
|
88
|
-
groups: account.config.groups,
|
|
89
|
-
target: message.target
|
|
90
|
-
});
|
|
91
|
-
const allowTextCommands = core.channel.commands.shouldHandleTextCommands({
|
|
92
|
-
cfg: config,
|
|
93
|
-
surface: CHANNEL_ID
|
|
94
|
-
});
|
|
95
|
-
const hasControlCommand = core.channel.text.hasControlCommand(rawBody, config);
|
|
96
|
-
const mentionRegexes = core.channel.mentions.buildMentionRegexes(config);
|
|
97
|
-
const mentionNick = connectedNick?.trim() || account.nick;
|
|
98
|
-
const explicitMentionRegex = mentionNick ? new RegExp(`(?<!${IRC_NICK_CHARACTER})${buildIrcNickMentionPattern(mentionNick)}(?!${IRC_NICK_CHARACTER})[:,]?`, "i") : null;
|
|
99
|
-
const wasMentioned = core.channel.mentions.matchesMentionPatterns(rawBody, mentionRegexes) || (explicitMentionRegex ? explicitMentionRegex.test(rawBody) : false);
|
|
100
|
-
const requireMention = message.isGroup ? resolveIrcGroupRequireMention({
|
|
101
|
-
groups: account.config.groups,
|
|
102
|
-
target: message.target
|
|
103
|
-
}) : false;
|
|
104
|
-
const routeGroupAllowFrom = normalizeStringEntries(groupMatch.groupConfig?.allowFrom?.length ? groupMatch.groupConfig.allowFrom : groupMatch.wildcardConfig?.allowFrom);
|
|
105
|
-
const accessGroupPolicy = groupPolicy === "open" && (hasEntries(account.config.groupAllowFrom) || hasEntries(routeGroupAllowFrom)) ? "allowlist" : groupPolicy;
|
|
106
|
-
const channelTarget = message.target.startsWith("#") || message.target.startsWith("&") ? message.target : `#${message.target}`;
|
|
107
|
-
const peerId = message.isGroup ? channelTarget : message.senderNick;
|
|
108
|
-
const route = resolveAgentRoute({
|
|
109
|
-
cfg: config,
|
|
110
|
-
channel: CHANNEL_ID,
|
|
111
|
-
accountId: account.accountId,
|
|
112
|
-
peer: {
|
|
113
|
-
kind: message.isGroup ? "group" : "direct",
|
|
114
|
-
id: peerId
|
|
115
|
-
}
|
|
116
|
-
});
|
|
117
|
-
const access = await core.channel.inbound.ingress.createResolver({
|
|
118
|
-
channelId: CHANNEL_ID,
|
|
119
|
-
accountId: account.accountId,
|
|
120
|
-
identity: ircIngressIdentity,
|
|
121
|
-
cfg: config,
|
|
122
|
-
readStoreAllowFrom: async () => await pairing.readAllowFromStore()
|
|
123
|
-
}).message({
|
|
124
|
-
subject: createIrcIngressSubject(message),
|
|
125
|
-
conversation: {
|
|
126
|
-
kind: message.isGroup ? "group" : "direct",
|
|
127
|
-
id: message.target
|
|
128
|
-
},
|
|
129
|
-
contextBinding: {
|
|
130
|
-
agentId: route.agentId,
|
|
131
|
-
sessionKey: route.sessionKey,
|
|
132
|
-
...message.messageId ? { messageId: message.messageId } : {},
|
|
133
|
-
inboundEventKind: "user_request"
|
|
134
|
-
},
|
|
135
|
-
route: message.isGroup ? channelIngressRoutes(groupPolicy === "allowlist" && {
|
|
136
|
-
id: "irc:channel",
|
|
137
|
-
allowed: groupMatch.hasConfiguredGroups && groupMatch.allowed,
|
|
138
|
-
precedence: 0,
|
|
139
|
-
matchId: "irc-channel",
|
|
140
|
-
blockReason: "channel_not_allowlisted"
|
|
141
|
-
}, (groupMatch.groupConfig?.enabled === false || groupMatch.wildcardConfig?.enabled === false) && {
|
|
142
|
-
id: "irc:channel-enabled",
|
|
143
|
-
enabled: false,
|
|
144
|
-
precedence: 10,
|
|
145
|
-
blockReason: "channel_disabled"
|
|
146
|
-
}, hasEntries(routeGroupAllowFrom) && {
|
|
147
|
-
id: "irc:channel-sender",
|
|
148
|
-
precedence: 20,
|
|
149
|
-
senderPolicy: "replace",
|
|
150
|
-
senderAllowFrom: routeGroupAllowFrom
|
|
151
|
-
}) : [],
|
|
152
|
-
mentionFacts: message.isGroup ? {
|
|
153
|
-
canDetectMention: true,
|
|
154
|
-
wasMentioned,
|
|
155
|
-
hasAnyMention: wasMentioned
|
|
156
|
-
} : void 0,
|
|
157
|
-
dmPolicy,
|
|
158
|
-
groupPolicy: accessGroupPolicy,
|
|
159
|
-
policy: {
|
|
160
|
-
groupAllowFromFallbackToAllowFrom: false,
|
|
161
|
-
mutableIdentifierMatching: allowNameMatching ? "enabled" : "disabled",
|
|
162
|
-
activation: {
|
|
163
|
-
requireMention: message.isGroup && requireMention,
|
|
164
|
-
allowTextCommands
|
|
165
|
-
}
|
|
166
|
-
},
|
|
167
|
-
allowFrom: account.config.allowFrom,
|
|
168
|
-
groupAllowFrom: account.config.groupAllowFrom,
|
|
169
|
-
command: {
|
|
170
|
-
allowTextCommands,
|
|
171
|
-
hasControlCommand
|
|
172
|
-
}
|
|
173
|
-
});
|
|
174
|
-
const commandAuthorized = access.commandAccess.authorized;
|
|
175
|
-
if (access.ingress.admission === "pairing-required") {
|
|
176
|
-
await pairing.issueChallenge({
|
|
177
|
-
senderId: normalizeLowercaseStringOrEmpty(senderDisplay),
|
|
178
|
-
senderIdLine: `Your IRC id: ${senderDisplay}`,
|
|
179
|
-
meta: { name: message.senderNick || void 0 },
|
|
180
|
-
sendPairingReply: async (text) => {
|
|
181
|
-
await deliverIrcReply({
|
|
182
|
-
payload: { text },
|
|
183
|
-
cfg: config,
|
|
184
|
-
target: message.senderNick,
|
|
185
|
-
accountId: account.accountId,
|
|
186
|
-
sendReply: params.sendReply,
|
|
187
|
-
statusSink
|
|
188
|
-
});
|
|
189
|
-
},
|
|
190
|
-
onReplyError: (err) => {
|
|
191
|
-
runtime.error?.(`irc: pairing reply failed for ${senderDisplay}: ${String(err)}`);
|
|
192
|
-
}
|
|
193
|
-
});
|
|
194
|
-
runtime.log?.(`irc: drop DM sender ${senderDisplay} (dmPolicy=${dmPolicy})`);
|
|
195
|
-
return { kind: "completed" };
|
|
196
|
-
}
|
|
197
|
-
if (access.ingress.admission === "skip") {
|
|
198
|
-
runtime.log?.(`irc: drop channel ${message.target} (missing-mention)`);
|
|
199
|
-
return { kind: "completed" };
|
|
200
|
-
}
|
|
201
|
-
if (access.ingress.admission !== "dispatch") {
|
|
202
|
-
if (message.isGroup && access.ingress.decisiveGateId === "command" && access.commandAccess.shouldBlockControlCommand) {
|
|
203
|
-
logInboundDrop({
|
|
204
|
-
log: (line) => runtime.log?.(line),
|
|
205
|
-
channel: CHANNEL_ID,
|
|
206
|
-
reason: "control command (unauthorized)",
|
|
207
|
-
target: senderDisplay
|
|
208
|
-
});
|
|
209
|
-
return { kind: "completed" };
|
|
210
|
-
}
|
|
211
|
-
if (message.isGroup) {
|
|
212
|
-
if (access.routeAccess.reason === "channel_not_allowlisted") runtime.log?.(`irc: drop channel ${message.target} (not allowlisted)`);
|
|
213
|
-
else if (access.routeAccess.reason === "channel_disabled") runtime.log?.(`irc: drop channel ${message.target} (disabled)`);
|
|
214
|
-
else runtime.log?.(`irc: drop group sender ${senderDisplay} (policy=${groupPolicy})`);
|
|
215
|
-
} else runtime.log?.(`irc: drop DM sender ${senderDisplay} (dmPolicy=${dmPolicy})`);
|
|
216
|
-
return { kind: "completed" };
|
|
217
|
-
}
|
|
218
|
-
if (turnAdoptionLifecycle?.abortSignal.aborted) return {
|
|
219
|
-
kind: "failed-retryable",
|
|
220
|
-
error: turnAdoptionLifecycle.abortSignal.reason
|
|
221
|
-
};
|
|
222
|
-
const fromLabel = message.isGroup ? message.target : senderDisplay;
|
|
223
|
-
const body = (await createChannelInboundEnvelopeBuilderAsync({
|
|
224
|
-
cfg: config,
|
|
225
|
-
route
|
|
226
|
-
}))({
|
|
227
|
-
channel: "IRC",
|
|
228
|
-
from: fromLabel,
|
|
229
|
-
timestamp: message.timestamp,
|
|
230
|
-
body: rawBody
|
|
231
|
-
});
|
|
232
|
-
const groupSystemPrompt = normalizeOptionalString(groupMatch.groupConfig?.systemPrompt);
|
|
233
|
-
const blockStreamingEnabled = resolveChannelStreamingBlockEnabled(account.config);
|
|
234
|
-
const ctxPayload = core.channel.inbound.buildContext({
|
|
235
|
-
channelIngress: access,
|
|
236
|
-
channel: CHANNEL_ID,
|
|
237
|
-
accountId: route.accountId,
|
|
238
|
-
messageId: message.messageId,
|
|
239
|
-
timestamp: message.timestamp,
|
|
240
|
-
from: message.isGroup ? `channel:${channelTarget}` : `irc:${senderDisplay}`,
|
|
241
|
-
sender: {
|
|
242
|
-
id: senderDisplay,
|
|
243
|
-
name: message.senderNick || void 0
|
|
244
|
-
},
|
|
245
|
-
conversation: {
|
|
246
|
-
kind: message.isGroup ? "group" : "direct",
|
|
247
|
-
id: peerId,
|
|
248
|
-
label: fromLabel
|
|
249
|
-
},
|
|
250
|
-
route: {
|
|
251
|
-
agentId: route.agentId,
|
|
252
|
-
dmScope: route.dmScope,
|
|
253
|
-
accountId: route.accountId,
|
|
254
|
-
routeSessionKey: route.sessionKey
|
|
255
|
-
},
|
|
256
|
-
reply: {
|
|
257
|
-
to: message.isGroup ? `channel:${channelTarget}` : `irc:${peerId}`,
|
|
258
|
-
originatingTo: message.isGroup ? `channel:${channelTarget}` : `irc:${peerId}`
|
|
259
|
-
},
|
|
260
|
-
message: {
|
|
261
|
-
body,
|
|
262
|
-
bodyForAgent: rawBody,
|
|
263
|
-
rawBody,
|
|
264
|
-
commandBody: rawBody
|
|
265
|
-
},
|
|
266
|
-
access: {
|
|
267
|
-
commands: { authorized: commandAuthorized },
|
|
268
|
-
mentions: {
|
|
269
|
-
canDetectMention: message.isGroup,
|
|
270
|
-
wasMentioned
|
|
271
|
-
}
|
|
272
|
-
},
|
|
273
|
-
extra: {
|
|
274
|
-
GroupSubject: message.isGroup ? message.target : void 0,
|
|
275
|
-
GroupSystemPrompt: message.isGroup ? groupSystemPrompt : void 0
|
|
276
|
-
}
|
|
277
|
-
});
|
|
278
|
-
const ingressState = { handoff: "none" };
|
|
279
|
-
const trackedIngressLifecycle = turnAdoptionLifecycle ? {
|
|
280
|
-
...turnAdoptionLifecycle,
|
|
281
|
-
onAdopted: async () => {
|
|
282
|
-
ingressState.handoff = "adopted";
|
|
283
|
-
await turnAdoptionLifecycle.onAdopted();
|
|
284
|
-
},
|
|
285
|
-
onDeferred: () => {
|
|
286
|
-
ingressState.handoff = "deferred";
|
|
287
|
-
turnAdoptionLifecycle.onDeferred();
|
|
288
|
-
},
|
|
289
|
-
onAbandoned: async () => {
|
|
290
|
-
ingressState.handoff = "abandoned";
|
|
291
|
-
await turnAdoptionLifecycle.onAbandoned();
|
|
292
|
-
}
|
|
293
|
-
} : void 0;
|
|
294
|
-
await core.channel.inbound.dispatch({
|
|
295
|
-
cfg: config,
|
|
296
|
-
channel: CHANNEL_ID,
|
|
297
|
-
accountId: account.accountId,
|
|
298
|
-
route: {
|
|
299
|
-
agentId: route.agentId,
|
|
300
|
-
sessionKey: route.sessionKey
|
|
301
|
-
},
|
|
302
|
-
ctxPayload,
|
|
303
|
-
delivery: {
|
|
304
|
-
deliver: async (payload) => {
|
|
305
|
-
await deliverIrcReply({
|
|
306
|
-
payload,
|
|
307
|
-
cfg: config,
|
|
308
|
-
target: peerId,
|
|
309
|
-
accountId: account.accountId,
|
|
310
|
-
sendReply: params.sendReply,
|
|
311
|
-
statusSink
|
|
312
|
-
});
|
|
313
|
-
},
|
|
314
|
-
onError: (err, info) => {
|
|
315
|
-
runtime.error?.(`irc ${info.kind} reply failed: ${String(err)}`);
|
|
316
|
-
}
|
|
317
|
-
},
|
|
318
|
-
replyPipeline: {},
|
|
319
|
-
replyOptions: {
|
|
320
|
-
...trackedIngressLifecycle ? bindIngressLifecycleToReplyOptions(trackedIngressLifecycle) : {},
|
|
321
|
-
skillFilter: groupMatch.groupConfig?.skills,
|
|
322
|
-
disableBlockStreaming: typeof blockStreamingEnabled === "boolean" ? !blockStreamingEnabled : void 0
|
|
323
|
-
},
|
|
324
|
-
record: { onRecordError: (err) => {
|
|
325
|
-
runtime.error?.(`irc: failed updating session meta: ${String(err)}`);
|
|
326
|
-
} }
|
|
327
|
-
});
|
|
328
|
-
if (turnAdoptionLifecycle?.abortSignal.aborted && ingressState.handoff === "none") return {
|
|
329
|
-
kind: "failed-retryable",
|
|
330
|
-
error: turnAdoptionLifecycle.abortSignal.reason
|
|
331
|
-
};
|
|
332
|
-
if (turnAdoptionLifecycle && ingressState.handoff === "none") {
|
|
333
|
-
await turnAdoptionLifecycle.onAdopted();
|
|
334
|
-
ingressState.handoff = "adopted";
|
|
335
|
-
}
|
|
336
|
-
return ingressState.handoff === "deferred" || ingressState.handoff === "abandoned" ? { kind: "deferred" } : { kind: "completed" };
|
|
337
|
-
}
|
|
338
|
-
// extensions/irc/src/irc-ingress.ts
|
|
339
|
-
const IRC_INGRESS_PAYLOAD_VERSION = 1;
|
|
340
|
-
const IRC_INGRESS_POLL_INTERVAL_MS = 1e3;
|
|
341
|
-
const IrcIngressPayloadError = createChannelIngressError("IrcIngressPayloadError");
|
|
342
|
-
function inspectRawPrivmsg(rawLine) {
|
|
343
|
-
const line = parseIrcLine(rawLine);
|
|
344
|
-
if (!line || line.command !== "PRIVMSG") throw new IrcIngressPayloadError("IRC ingress row is not a PRIVMSG line.");
|
|
345
|
-
const rawTarget = line.params[0]?.trim() ?? "";
|
|
346
|
-
const text = line.trailing ?? line.params[1] ?? "";
|
|
347
|
-
const prefix = parseIrcPrefix(line.prefix);
|
|
348
|
-
const senderNick = prefix.nick?.trim() ?? "";
|
|
349
|
-
if (!rawTarget || !senderNick || !text.trim()) throw new IrcIngressPayloadError("IRC PRIVMSG line is missing target, sender, or text.");
|
|
350
|
-
const isGroup = isChannelTarget(rawTarget);
|
|
351
|
-
const target = isGroup ? rawTarget : senderNick;
|
|
352
|
-
const lanePeer = normalizeLowercaseStringOrEmpty(target);
|
|
353
|
-
return {
|
|
354
|
-
laneKey: `${isGroup ? "channel" : "direct"}:${lanePeer}`,
|
|
355
|
-
message: {
|
|
356
|
-
target,
|
|
357
|
-
rawTarget,
|
|
358
|
-
senderNick,
|
|
359
|
-
senderUser: prefix.user?.trim() || void 0,
|
|
360
|
-
senderHost: prefix.host?.trim() || void 0,
|
|
361
|
-
text,
|
|
362
|
-
isGroup
|
|
363
|
-
}
|
|
364
|
-
};
|
|
365
|
-
}
|
|
366
|
-
function decodeIrcIngressPayload(payload, claimedId) {
|
|
367
|
-
if (!payload || typeof payload !== "object" || Array.isArray(payload) || typeof payload.eventId !== "string" || !Number.isSafeInteger(payload.receivedAt) || (payload.receivedAt ?? 0) <= 0 || typeof payload.connectionEpoch !== "string" || !payload.connectionEpoch?.trim() || typeof payload.connectedNick !== "string" || !payload.connectedNick?.trim() || typeof payload.rawLine !== "string") throw new IrcIngressPayloadError(`IRC ingress row ${claimedId} has invalid metadata.`);
|
|
368
|
-
const validPayload = payload;
|
|
369
|
-
return {
|
|
370
|
-
version: validPayload.version,
|
|
371
|
-
body: {
|
|
372
|
-
eventId: validPayload.eventId,
|
|
373
|
-
receivedAt: validPayload.receivedAt,
|
|
374
|
-
connectionEpoch: validPayload.connectionEpoch,
|
|
375
|
-
connectedNick: validPayload.connectedNick,
|
|
376
|
-
rawLine: validPayload.rawLine
|
|
377
|
-
}
|
|
378
|
-
};
|
|
379
|
-
}
|
|
380
|
-
function inspectIrcIngress(raw, phase) {
|
|
381
|
-
try {
|
|
382
|
-
const projection = inspectRawPrivmsg(raw.body.rawLine);
|
|
383
|
-
if (phase === "claim") raw.claimedProjection = projection;
|
|
384
|
-
return {
|
|
385
|
-
eventId: raw.body.eventId,
|
|
386
|
-
laneKey: projection.laneKey
|
|
387
|
-
};
|
|
388
|
-
} catch (error) {
|
|
389
|
-
if (phase === "admission" && error instanceof IrcIngressPayloadError) return {
|
|
390
|
-
eventId: raw.body.eventId,
|
|
391
|
-
laneKey: `invalid:${raw.body.eventId}`
|
|
392
|
-
};
|
|
393
|
-
throw error;
|
|
394
|
-
}
|
|
395
|
-
}
|
|
396
|
-
function resolveIrcIngressNonRetryableFailure(error) {
|
|
397
|
-
return error instanceof IrcIngressPayloadError ? {
|
|
398
|
-
reason: "invalid-event",
|
|
399
|
-
message: error.message
|
|
400
|
-
} : null;
|
|
401
|
-
}
|
|
402
|
-
function createIrcIngressMonitor(options) {
|
|
403
|
-
const monitor = createChannelIngressMonitor({
|
|
404
|
-
queue: options.queue ?? (() => getIrcRuntime().state.openChannelIngressQueue({ accountId: options.accountId })),
|
|
405
|
-
inspect: (raw, context) => inspectIrcIngress(raw, context.phase),
|
|
406
|
-
payload: {
|
|
407
|
-
version: IRC_INGRESS_PAYLOAD_VERSION,
|
|
408
|
-
serialize: (raw) => raw.body,
|
|
409
|
-
deserialize: (body) => ({ body }),
|
|
410
|
-
encode: ({ body }) => ({
|
|
411
|
-
version: IRC_INGRESS_PAYLOAD_VERSION,
|
|
412
|
-
...body
|
|
413
|
-
}),
|
|
414
|
-
decode: (payload, { claim }) => decodeIrcIngressPayload(payload, claim.id),
|
|
415
|
-
createClaimError: (_kind, claim) => new IrcIngressPayloadError(`IRC ingress row ${claim.id} has invalid metadata.`)
|
|
416
|
-
},
|
|
417
|
-
deliver: (raw, lifecycle, claim) => {
|
|
418
|
-
const message = {
|
|
419
|
-
...raw.claimedProjection.message,
|
|
420
|
-
messageId: claim.id,
|
|
421
|
-
timestamp: raw.body.receivedAt
|
|
422
|
-
};
|
|
423
|
-
return options.dispatch(message, lifecycle, {
|
|
424
|
-
connectedNick: raw.body.connectedNick.trim(),
|
|
425
|
-
connectionEpoch: raw.body.connectionEpoch.trim()
|
|
426
|
-
});
|
|
427
|
-
},
|
|
428
|
-
pollIntervalMs: options.pollIntervalMs ?? IRC_INGRESS_POLL_INTERVAL_MS,
|
|
429
|
-
retention: {
|
|
430
|
-
completedMaxEntries: 1e3,
|
|
431
|
-
failedMaxEntries: 1e3
|
|
432
|
-
},
|
|
433
|
-
drain: {
|
|
434
|
-
resolveNonRetryableFailure: resolveIrcIngressNonRetryableFailure,
|
|
435
|
-
...options.adoptionStallTimeoutMs === void 0 ? {} : { adoptionStallTimeoutMs: options.adoptionStallTimeoutMs },
|
|
436
|
-
onLog: (message) => options.runtime.log?.(`irc ${message}`)
|
|
437
|
-
},
|
|
438
|
-
createStoppedError: () => /* @__PURE__ */ new Error("IRC ingress is stopped."),
|
|
439
|
-
onError: (error) => options.runtime.error?.(`irc ingress drain failed: ${String(error)}`)
|
|
440
|
-
});
|
|
441
|
-
return {
|
|
442
|
-
openConnection: (connectionEpoch = randomUUID()) => {
|
|
443
|
-
const epoch = connectionEpoch.trim();
|
|
444
|
-
if (!epoch) throw new Error("IRC ingress connection epoch is required.");
|
|
445
|
-
let sequence = 0;
|
|
446
|
-
return {
|
|
447
|
-
connectionEpoch: epoch,
|
|
448
|
-
accept: (rawLine, connectedNick) => {
|
|
449
|
-
if (monitor.isStopped()) return Promise.reject(/* @__PURE__ */ new Error("IRC ingress is stopped."));
|
|
450
|
-
sequence += 1;
|
|
451
|
-
const eventId = `local:${epoch}:${String(sequence).padStart(12, "0")}`;
|
|
452
|
-
const receivedAt = Date.now();
|
|
453
|
-
const normalizedNick = connectedNick.trim();
|
|
454
|
-
if (!normalizedNick) return Promise.reject(/* @__PURE__ */ new Error("IRC ingress connected nickname is required."));
|
|
455
|
-
return monitor.admit({ body: {
|
|
456
|
-
eventId,
|
|
457
|
-
rawLine,
|
|
458
|
-
receivedAt,
|
|
459
|
-
connectionEpoch: epoch,
|
|
460
|
-
connectedNick: normalizedNick
|
|
461
|
-
} }, { receivedAt }).then(() => void 0);
|
|
462
|
-
}
|
|
463
|
-
};
|
|
464
|
-
},
|
|
465
|
-
start: monitor.start,
|
|
466
|
-
pause: monitor.pause,
|
|
467
|
-
stop: monitor.stop,
|
|
468
|
-
waitForIdle: monitor.waitForIdle
|
|
469
|
-
};
|
|
470
|
-
}
|
|
471
|
-
// extensions/irc/src/reconnect-backoff.ts
|
|
472
|
-
const IRC_RECONNECT_BACKOFF = {
|
|
473
|
-
initialMs: 1e3,
|
|
474
|
-
maxMs: 3e4,
|
|
475
|
-
factor: 2,
|
|
476
|
-
jitter: .2
|
|
477
|
-
};
|
|
478
|
-
const IRC_RECONNECT_STABLE_MS = 6e4;
|
|
479
|
-
function createIrcReconnectBackoff(now = Date.now) {
|
|
480
|
-
let attempt = 0;
|
|
481
|
-
let connectedAt;
|
|
482
|
-
return {
|
|
483
|
-
markConnected: () => {
|
|
484
|
-
connectedAt = now();
|
|
485
|
-
},
|
|
486
|
-
nextDelayMs: () => {
|
|
487
|
-
if (connectedAt !== void 0 && now() - connectedAt >= IRC_RECONNECT_STABLE_MS) attempt = 0;
|
|
488
|
-
connectedAt = void 0;
|
|
489
|
-
attempt += 1;
|
|
490
|
-
return computeBackoff(IRC_RECONNECT_BACKOFF, attempt);
|
|
491
|
-
}
|
|
492
|
-
};
|
|
493
|
-
}
|
|
494
|
-
// extensions/irc/src/monitor.ts
|
|
495
|
-
async function monitorIrcProvider(opts) {
|
|
496
|
-
const core = getIrcRuntime();
|
|
497
|
-
const cfg = opts.config ?? core.config.current();
|
|
498
|
-
const account = resolveIrcAccount({
|
|
499
|
-
cfg,
|
|
500
|
-
accountId: opts.accountId
|
|
501
|
-
});
|
|
502
|
-
const runtime = resolveLoggerBackedRuntime(opts.runtime, core.logging.getChildLogger());
|
|
503
|
-
if (!account.configured) throw new Error(`IRC is not configured for account "${account.accountId}" (need host and nick in channels.irc).`);
|
|
504
|
-
const logger = core.logging.getChildLogger({
|
|
505
|
-
channel: "irc",
|
|
506
|
-
accountId: account.accountId
|
|
507
|
-
});
|
|
508
|
-
let client = null;
|
|
509
|
-
let activeConnectionEpoch = null;
|
|
510
|
-
let ingressPause = Promise.resolve();
|
|
511
|
-
let reconnectTimer = null;
|
|
512
|
-
const reconnectBackoff = createIrcReconnectBackoff();
|
|
513
|
-
let stopped = false;
|
|
514
|
-
const monitorAbort = new AbortController();
|
|
515
|
-
const abortSignal = opts.abortSignal ? AbortSignal.any([opts.abortSignal, monitorAbort.signal]) : monitorAbort.signal;
|
|
516
|
-
const ingress = createIrcIngressMonitor({
|
|
517
|
-
accountId: account.accountId,
|
|
518
|
-
runtime,
|
|
519
|
-
...opts.ingressQueue ? { queue: opts.ingressQueue } : {},
|
|
520
|
-
dispatch: async (message, turnAdoptionLifecycle, context) => {
|
|
521
|
-
const activeClient = client;
|
|
522
|
-
if (!activeClient || stopped || abortSignal.aborted) return {
|
|
523
|
-
kind: "failed-retryable",
|
|
524
|
-
error: /* @__PURE__ */ new Error("IRC transport disconnected before ingress dispatch.")
|
|
525
|
-
};
|
|
526
|
-
if (normalizeLowercaseStringOrEmpty(message.senderNick) === normalizeLowercaseStringOrEmpty(context.connectedNick)) return { kind: "completed" };
|
|
527
|
-
if (!message.isGroup && context.connectionEpoch !== activeConnectionEpoch) {
|
|
528
|
-
logger.warn?.(`[${account.accountId}] dropping replayed IRC DM after the connection changed`);
|
|
529
|
-
return { kind: "completed" };
|
|
530
|
-
}
|
|
531
|
-
if (opts.onMessage) {
|
|
532
|
-
await opts.onMessage(message, activeClient);
|
|
533
|
-
return { kind: "completed" };
|
|
534
|
-
}
|
|
535
|
-
return await handleIrcInbound({
|
|
536
|
-
message,
|
|
537
|
-
account,
|
|
538
|
-
config: cfg,
|
|
539
|
-
runtime,
|
|
540
|
-
connectedNick: context.connectedNick,
|
|
541
|
-
turnAdoptionLifecycle,
|
|
542
|
-
sendReply: async (target, text) => {
|
|
543
|
-
const replyClient = client;
|
|
544
|
-
if (!replyClient || !replyClient.isReady() || stopped || abortSignal.aborted) throw new Error("IRC transport disconnected before reply send.");
|
|
545
|
-
if (!message.isGroup && context.connectionEpoch !== activeConnectionEpoch) throw new Error("IRC connection changed before private reply send.");
|
|
546
|
-
await replyClient.sendPrivmsg(target, text);
|
|
547
|
-
opts.statusSink?.({ lastOutboundAt: Date.now() });
|
|
548
|
-
core.channel.activity.record({
|
|
549
|
-
channel: "irc",
|
|
550
|
-
accountId: account.accountId,
|
|
551
|
-
direction: "outbound"
|
|
552
|
-
});
|
|
553
|
-
},
|
|
554
|
-
statusSink: opts.statusSink
|
|
555
|
-
});
|
|
556
|
-
}
|
|
557
|
-
});
|
|
558
|
-
function scheduleReconnect() {
|
|
559
|
-
if (stopped || abortSignal.aborted || reconnectTimer) return;
|
|
560
|
-
opts.statusSink?.({
|
|
561
|
-
connected: false,
|
|
562
|
-
lifecycle: "recovering"
|
|
563
|
-
});
|
|
564
|
-
const delayMs = reconnectBackoff.nextDelayMs();
|
|
565
|
-
reconnectTimer = setTimeout(() => {
|
|
566
|
-
reconnectTimer = null;
|
|
567
|
-
connect().catch((error) => {
|
|
568
|
-
if (stopped || abortSignal.aborted) return;
|
|
569
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
570
|
-
logger.error(`[${account.accountId}] IRC reconnect failed: ${message}`);
|
|
571
|
-
opts.statusSink?.({ lastError: message });
|
|
572
|
-
scheduleReconnect();
|
|
573
|
-
});
|
|
574
|
-
}, delayMs);
|
|
575
|
-
return delayMs;
|
|
576
|
-
}
|
|
577
|
-
async function connect() {
|
|
578
|
-
if (stopped || abortSignal.aborted) return;
|
|
579
|
-
const ingressConnection = ingress.openConnection();
|
|
580
|
-
const nextClient = await connectIrcClient(buildIrcConnectOptions(account, {
|
|
581
|
-
channels: account.config.channels,
|
|
582
|
-
abortSignal,
|
|
583
|
-
onNotice: (text, target) => {
|
|
584
|
-
if (core.logging.shouldLogVerbose()) logger.debug?.(`[${account.accountId}] notice ${target ?? ""} len=${text.length}`);
|
|
585
|
-
},
|
|
586
|
-
onError: (error) => {
|
|
587
|
-
logger.error(`[${account.accountId}] IRC error: ${error.message}`);
|
|
588
|
-
},
|
|
589
|
-
onDisconnect: () => {
|
|
590
|
-
if (stopped || abortSignal.aborted) return;
|
|
591
|
-
ingressPause = ingress.pause();
|
|
592
|
-
if (activeConnectionEpoch === ingressConnection.connectionEpoch) activeConnectionEpoch = null;
|
|
593
|
-
client = null;
|
|
594
|
-
opts.statusSink?.({ lastDisconnect: { at: Date.now() } });
|
|
595
|
-
const delayMs = scheduleReconnect();
|
|
596
|
-
if (delayMs !== void 0) logger.warn?.(`[${account.accountId}] IRC connection closed; reconnecting in ${delayMs}ms`);
|
|
597
|
-
},
|
|
598
|
-
onPrivmsg: async (event) => {
|
|
599
|
-
await ingressConnection.accept(event.rawLine, event.connectedNick);
|
|
600
|
-
if (normalizeLowercaseStringOrEmpty(event.senderNick) === normalizeLowercaseStringOrEmpty(event.connectedNick)) return;
|
|
601
|
-
core.channel.activity.record({
|
|
602
|
-
channel: "irc",
|
|
603
|
-
accountId: account.accountId,
|
|
604
|
-
direction: "inbound",
|
|
605
|
-
at: Date.now()
|
|
606
|
-
});
|
|
607
|
-
}
|
|
608
|
-
}));
|
|
609
|
-
if (stopped || abortSignal.aborted) {
|
|
610
|
-
nextClient.quit("shutdown");
|
|
611
|
-
return;
|
|
612
|
-
}
|
|
613
|
-
client = nextClient;
|
|
614
|
-
activeConnectionEpoch = ingressConnection.connectionEpoch;
|
|
615
|
-
await ingressPause;
|
|
616
|
-
if (client !== nextClient || !nextClient.isReady()) {
|
|
617
|
-
if (activeConnectionEpoch === ingressConnection.connectionEpoch) activeConnectionEpoch = null;
|
|
618
|
-
return;
|
|
619
|
-
}
|
|
620
|
-
ingress.start();
|
|
621
|
-
reconnectBackoff.markConnected();
|
|
622
|
-
opts.statusSink?.(channelReadyPatch());
|
|
623
|
-
logger.info(`[${account.accountId}] connected to ${account.host}:${account.port}${account.tls ? " (tls)" : ""} as ${nextClient.nick}`);
|
|
624
|
-
}
|
|
625
|
-
try {
|
|
626
|
-
await connect();
|
|
627
|
-
} catch (error) {
|
|
628
|
-
await ingress.stop();
|
|
629
|
-
throw error;
|
|
630
|
-
}
|
|
631
|
-
let stopTask;
|
|
632
|
-
return { stop: () => {
|
|
633
|
-
stopTask ??= (async () => {
|
|
634
|
-
stopped = true;
|
|
635
|
-
if (!abortSignal.aborted) monitorAbort.abort();
|
|
636
|
-
if (reconnectTimer) {
|
|
637
|
-
clearTimeout(reconnectTimer);
|
|
638
|
-
reconnectTimer = null;
|
|
639
|
-
}
|
|
640
|
-
client?.quit("shutdown");
|
|
641
|
-
client = null;
|
|
642
|
-
activeConnectionEpoch = null;
|
|
643
|
-
await ingress.stop();
|
|
644
|
-
})();
|
|
645
|
-
return stopTask;
|
|
646
|
-
} };
|
|
647
|
-
}
|
|
648
|
-
export { monitorIrcProvider, sendMessageIrc };
|