openclaw-groupme 0.0.4 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +373 -70
- package/package.json +36 -11
- package/src/accounts.ts +51 -23
- package/src/channel.ts +163 -16
- package/src/config-schema.ts +73 -3
- package/src/groupme-api.ts +98 -0
- package/src/history.ts +54 -0
- package/src/inbound.ts +129 -23
- package/src/monitor.ts +285 -33
- package/src/normalize.ts +1 -9
- package/src/onboarding.ts +413 -38
- package/src/parse.ts +32 -33
- package/src/policy.ts +5 -2
- package/src/rate-limit.ts +128 -0
- package/src/replay-cache.ts +71 -0
- package/src/security.ts +457 -0
- package/src/send.ts +237 -51
- package/src/types.ts +98 -1
- package/.github/workflows/publish-npm.yml +0 -30
- package/openclaw.plugin.json +0 -9
- package/src/monitor.test.ts +0 -186
- package/src/normalize.test.ts +0 -43
- package/src/parse.test.ts +0 -162
- package/src/policy.test.ts +0 -23
- package/src/send.test.ts +0 -153
package/src/onboarding.ts
CHANGED
|
@@ -1,8 +1,12 @@
|
|
|
1
|
-
import
|
|
2
|
-
import type {
|
|
1
|
+
import { randomBytes } from "node:crypto";
|
|
2
|
+
import type {
|
|
3
|
+
ChannelOnboardingAdapter,
|
|
4
|
+
OpenClawConfig,
|
|
5
|
+
} from "openclaw/plugin-sdk";
|
|
3
6
|
import { DEFAULT_ACCOUNT_ID } from "openclaw/plugin-sdk";
|
|
4
|
-
import type { CoreConfig, GroupMeConfig } from "./types.js";
|
|
5
7
|
import { resolveGroupMeAccount } from "./accounts.js";
|
|
8
|
+
import { createBot, fetchGroups } from "./groupme-api.js";
|
|
9
|
+
import type { CoreConfig, GroupMeConfig } from "./types.js";
|
|
6
10
|
|
|
7
11
|
function applyGroupMeConfig(params: {
|
|
8
12
|
cfg: OpenClawConfig;
|
|
@@ -46,6 +50,34 @@ function applyGroupMeConfig(params: {
|
|
|
46
50
|
};
|
|
47
51
|
}
|
|
48
52
|
|
|
53
|
+
function parsePublicDomain(raw: string): string {
|
|
54
|
+
const trimmed = raw.trim();
|
|
55
|
+
try {
|
|
56
|
+
if (/^https?:\/\//i.test(trimmed)) {
|
|
57
|
+
const url = new URL(trimmed);
|
|
58
|
+
return url.port ? `${url.hostname}:${url.port}` : url.hostname;
|
|
59
|
+
}
|
|
60
|
+
const withoutLeadingSlashes = trimmed.replace(/^\/+/, "");
|
|
61
|
+
return withoutLeadingSlashes.split(/[\/?#]/, 1)[0];
|
|
62
|
+
} catch {
|
|
63
|
+
const noScheme = trimmed.replace(/^https?:\/\//i, "");
|
|
64
|
+
return noScheme.split(/[\/?#]/, 1)[0];
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function generateCallbackUrl(): string {
|
|
69
|
+
const pathSegment = randomBytes(8).toString("hex");
|
|
70
|
+
const callbackToken = randomBytes(32).toString("hex");
|
|
71
|
+
return `/groupme/${pathSegment}?k=${callbackToken}`;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function redactMiddle(value: string): string {
|
|
75
|
+
if (value.length <= 10) {
|
|
76
|
+
return value;
|
|
77
|
+
}
|
|
78
|
+
return `${value.slice(0, 6)}...${value.slice(-3)}`;
|
|
79
|
+
}
|
|
80
|
+
|
|
49
81
|
export const groupmeOnboardingAdapter: ChannelOnboardingAdapter = {
|
|
50
82
|
channel: "groupme",
|
|
51
83
|
getStatus: async ({ cfg, accountOverrides }) => {
|
|
@@ -56,72 +88,154 @@ export const groupmeOnboardingAdapter: ChannelOnboardingAdapter = {
|
|
|
56
88
|
});
|
|
57
89
|
|
|
58
90
|
const configured = account.configured;
|
|
91
|
+
const callbackUrlConfigured = Boolean(account.config.callbackUrl?.trim());
|
|
92
|
+
const groupIdConfigured = Boolean(account.config.groupId?.trim());
|
|
93
|
+
const publicDomainConfigured = Boolean(account.config.publicDomain?.trim());
|
|
94
|
+
|
|
59
95
|
return {
|
|
60
96
|
channel: "groupme",
|
|
61
97
|
configured,
|
|
62
98
|
statusLines: [
|
|
63
|
-
`GroupMe (${accountId}): ${configured ? "configured" : "needs
|
|
99
|
+
`GroupMe (${accountId}): ${configured ? "configured" : "needs access token"}`,
|
|
64
100
|
account.config.accessToken?.trim()
|
|
65
101
|
? "Access token configured"
|
|
66
|
-
: "Access token missing
|
|
102
|
+
: "Access token missing",
|
|
103
|
+
callbackUrlConfigured
|
|
104
|
+
? "Webhook callback URL configured"
|
|
105
|
+
: "Webhook callback URL missing",
|
|
106
|
+
publicDomainConfigured
|
|
107
|
+
? "Public domain configured"
|
|
108
|
+
: "Public domain missing",
|
|
109
|
+
groupIdConfigured ? "Group ID configured" : "Group ID missing",
|
|
67
110
|
],
|
|
68
|
-
selectionHint: configured ? "configured" : "needs
|
|
111
|
+
selectionHint: configured ? "configured" : "needs access token",
|
|
69
112
|
quickstartScore: configured ? 1 : 0,
|
|
70
113
|
};
|
|
71
114
|
},
|
|
72
115
|
configure: async ({ cfg, prompter, accountOverrides }) => {
|
|
73
116
|
const accountId = accountOverrides.groupme ?? DEFAULT_ACCOUNT_ID;
|
|
74
117
|
|
|
75
|
-
|
|
76
|
-
[
|
|
77
|
-
"GroupMe bots are bound to a single group.",
|
|
78
|
-
"Create a bot at https://dev.groupme.com/bots and copy bot_id + access token.",
|
|
79
|
-
].join("\n"),
|
|
80
|
-
"GroupMe setup",
|
|
81
|
-
);
|
|
82
|
-
|
|
83
|
-
const botId = (
|
|
118
|
+
const botNameInput = (
|
|
84
119
|
await prompter.text({
|
|
85
|
-
message: "Bot
|
|
86
|
-
|
|
120
|
+
message: "Bot name",
|
|
121
|
+
initialValue: "openclaw",
|
|
87
122
|
})
|
|
88
123
|
).trim();
|
|
124
|
+
const botName = botNameInput || "openclaw";
|
|
89
125
|
|
|
90
126
|
const accessToken = (
|
|
91
127
|
await prompter.text({
|
|
92
|
-
message: "
|
|
93
|
-
validate: (value) =>
|
|
128
|
+
message: "GroupMe access token",
|
|
129
|
+
validate: (value) =>
|
|
130
|
+
value.trim() ? undefined : "Access token is required",
|
|
94
131
|
})
|
|
95
132
|
).trim();
|
|
96
133
|
|
|
97
|
-
const
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
134
|
+
const groupsSpin = prompter.progress("Fetching your GroupMe groups...");
|
|
135
|
+
let groups: Awaited<ReturnType<typeof fetchGroups>>;
|
|
136
|
+
try {
|
|
137
|
+
groups = await fetchGroups(accessToken);
|
|
138
|
+
} catch {
|
|
139
|
+
groupsSpin.stop("Failed");
|
|
140
|
+
await prompter.note(
|
|
141
|
+
"Could not fetch groups. Check your access token and try again.",
|
|
142
|
+
"GroupMe setup failed",
|
|
143
|
+
);
|
|
144
|
+
throw new Error("Could not fetch groups");
|
|
145
|
+
}
|
|
103
146
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
147
|
+
if (groups.length === 0) {
|
|
148
|
+
groupsSpin.stop("No groups found");
|
|
149
|
+
await prompter.note(
|
|
150
|
+
"No groups found. Create or join a GroupMe group first.",
|
|
151
|
+
"GroupMe setup failed",
|
|
152
|
+
);
|
|
153
|
+
throw new Error("No GroupMe groups found");
|
|
154
|
+
}
|
|
155
|
+
groupsSpin.stop(`Found ${groups.length} groups`);
|
|
111
156
|
|
|
157
|
+
const groupId = await prompter.select<string>({
|
|
158
|
+
message: "Select a GroupMe group",
|
|
159
|
+
options: groups.map((group) => ({
|
|
160
|
+
value: group.id,
|
|
161
|
+
label: group.name || group.id,
|
|
162
|
+
hint: group.id,
|
|
163
|
+
})),
|
|
164
|
+
});
|
|
165
|
+
const selectedGroup = groups.find((group) => group.id === groupId);
|
|
112
166
|
const requireMention = await prompter.confirm({
|
|
113
167
|
message: "Require mention to respond?",
|
|
114
168
|
initialValue: true,
|
|
115
169
|
});
|
|
170
|
+
const publicDomainRaw = (
|
|
171
|
+
await prompter.text({
|
|
172
|
+
message: "Public domain (must be reachable — GroupMe will ping it)",
|
|
173
|
+
validate: (value) => {
|
|
174
|
+
const trimmed = value.trim();
|
|
175
|
+
if (!trimmed) {
|
|
176
|
+
return "Public domain is required";
|
|
177
|
+
}
|
|
178
|
+
const normalized = trimmed
|
|
179
|
+
.replace(/^https?:\/\//, "")
|
|
180
|
+
.replace(/\/+$/, "");
|
|
181
|
+
if (!normalized) {
|
|
182
|
+
return "Public domain is required";
|
|
183
|
+
}
|
|
184
|
+
const parsed = parsePublicDomain(trimmed);
|
|
185
|
+
if (!parsed) {
|
|
186
|
+
return "Public domain is required";
|
|
187
|
+
}
|
|
188
|
+
return undefined;
|
|
189
|
+
},
|
|
190
|
+
})
|
|
191
|
+
).trim();
|
|
192
|
+
const publicDomain = parsePublicDomain(publicDomainRaw);
|
|
193
|
+
|
|
194
|
+
const callbackUrl = generateCallbackUrl();
|
|
195
|
+
const pathSegment = callbackUrl.split("?")[0].split("/").pop()!;
|
|
196
|
+
await prompter.note(
|
|
197
|
+
`Generated webhook callback URL: /groupme/${pathSegment}?k=***`,
|
|
198
|
+
"Generated callback URL",
|
|
199
|
+
);
|
|
200
|
+
|
|
201
|
+
const botSpin = prompter.progress("Registering bot with GroupMe...");
|
|
202
|
+
let botId = "";
|
|
203
|
+
try {
|
|
204
|
+
const bot = await createBot({
|
|
205
|
+
accessToken,
|
|
206
|
+
name: botName,
|
|
207
|
+
groupId,
|
|
208
|
+
callbackUrl: `https://${publicDomain}${callbackUrl}`,
|
|
209
|
+
});
|
|
210
|
+
botId = bot.bot_id;
|
|
211
|
+
botSpin.stop("Bot registered");
|
|
212
|
+
} catch (error) {
|
|
213
|
+
botSpin.stop("Failed");
|
|
214
|
+
const detail = error instanceof Error ? `\n\nDetails: ${error.message}` : "";
|
|
215
|
+
await prompter.note(
|
|
216
|
+
`Failed to register bot with GroupMe. Check your access token and try again.${detail}`,
|
|
217
|
+
"GroupMe setup failed",
|
|
218
|
+
);
|
|
219
|
+
throw new Error("Failed to register GroupMe bot", {
|
|
220
|
+
cause: error instanceof Error ? error : undefined,
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
await prompter.note(
|
|
225
|
+
`Bot "${botName}" registered in group "${selectedGroup?.name ?? groupId}" (bot ID: ${redactMiddle(botId)})`,
|
|
226
|
+
"GroupMe bot registered",
|
|
227
|
+
);
|
|
116
228
|
|
|
117
229
|
const next = applyGroupMeConfig({
|
|
118
230
|
cfg,
|
|
119
231
|
accountId,
|
|
120
232
|
updates: {
|
|
121
|
-
botId,
|
|
122
|
-
accessToken,
|
|
123
233
|
botName,
|
|
124
|
-
|
|
234
|
+
accessToken,
|
|
235
|
+
botId,
|
|
236
|
+
groupId,
|
|
237
|
+
publicDomain,
|
|
238
|
+
callbackUrl,
|
|
125
239
|
requireMention,
|
|
126
240
|
},
|
|
127
241
|
});
|
|
@@ -129,9 +243,8 @@ export const groupmeOnboardingAdapter: ChannelOnboardingAdapter = {
|
|
|
129
243
|
await prompter.note(
|
|
130
244
|
[
|
|
131
245
|
"Next steps:",
|
|
132
|
-
|
|
133
|
-
"2.
|
|
134
|
-
"3. Send a message in the group to test",
|
|
246
|
+
"1. Restart the gateway: openclaw gateway restart",
|
|
247
|
+
"2. Send a message in the group to test",
|
|
135
248
|
].join("\n"),
|
|
136
249
|
"GroupMe next steps",
|
|
137
250
|
);
|
|
@@ -141,6 +254,268 @@ export const groupmeOnboardingAdapter: ChannelOnboardingAdapter = {
|
|
|
141
254
|
accountId,
|
|
142
255
|
};
|
|
143
256
|
},
|
|
257
|
+
configureWhenConfigured: async ({
|
|
258
|
+
cfg,
|
|
259
|
+
prompter,
|
|
260
|
+
runtime,
|
|
261
|
+
accountOverrides,
|
|
262
|
+
}) => {
|
|
263
|
+
const accountId = accountOverrides.groupme ?? DEFAULT_ACCOUNT_ID;
|
|
264
|
+
const account = resolveGroupMeAccount({
|
|
265
|
+
cfg: cfg as CoreConfig,
|
|
266
|
+
accountId,
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
const action = await prompter.select<string>({
|
|
270
|
+
message: "GroupMe is already configured. What would you like to do?",
|
|
271
|
+
options: [
|
|
272
|
+
{ value: "skip", label: "Skip", hint: "no changes" },
|
|
273
|
+
{ value: "rotate_token", label: "Rotate access token" },
|
|
274
|
+
{ value: "change_group", label: "Change group" },
|
|
275
|
+
{ value: "regen_callback", label: "Regenerate callback URL" },
|
|
276
|
+
{ value: "toggle_mention", label: "Toggle requireMention" },
|
|
277
|
+
{ value: "update_domain", label: "Update public domain" },
|
|
278
|
+
{ value: "full_setup", label: "Full re-setup", hint: "start from scratch" },
|
|
279
|
+
],
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
if (action === "skip") {
|
|
283
|
+
return "skip";
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
if (action === "full_setup") {
|
|
287
|
+
return groupmeOnboardingAdapter.configure({
|
|
288
|
+
cfg,
|
|
289
|
+
prompter,
|
|
290
|
+
runtime,
|
|
291
|
+
accountOverrides,
|
|
292
|
+
options: {},
|
|
293
|
+
shouldPromptAccountIds: false,
|
|
294
|
+
forceAllowFrom: false,
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
if (action === "rotate_token") {
|
|
299
|
+
const newToken = (
|
|
300
|
+
await prompter.text({
|
|
301
|
+
message: "New GroupMe access token",
|
|
302
|
+
validate: (value) =>
|
|
303
|
+
value.trim() ? undefined : "Access token is required",
|
|
304
|
+
})
|
|
305
|
+
).trim();
|
|
306
|
+
|
|
307
|
+
const spin = prompter.progress("Validating access token...");
|
|
308
|
+
try {
|
|
309
|
+
await fetchGroups(newToken);
|
|
310
|
+
spin.stop("Token validated");
|
|
311
|
+
} catch {
|
|
312
|
+
spin.stop("Failed");
|
|
313
|
+
await prompter.note(
|
|
314
|
+
"Could not validate token. Check your access token and try again.",
|
|
315
|
+
"Validation failed",
|
|
316
|
+
);
|
|
317
|
+
throw new Error("Could not validate access token");
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
const next = applyGroupMeConfig({
|
|
321
|
+
cfg,
|
|
322
|
+
accountId,
|
|
323
|
+
updates: { accessToken: newToken },
|
|
324
|
+
});
|
|
325
|
+
await prompter.note("Access token updated.", "Token rotated");
|
|
326
|
+
return { cfg: next, accountId };
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
if (action === "change_group") {
|
|
330
|
+
const existingToken = account.accessToken;
|
|
331
|
+
if (!existingToken) {
|
|
332
|
+
await prompter.note(
|
|
333
|
+
"No access token configured. Use \"Rotate access token\" first.",
|
|
334
|
+
"Missing token",
|
|
335
|
+
);
|
|
336
|
+
return "skip";
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
const spin = prompter.progress("Fetching your GroupMe groups...");
|
|
340
|
+
let groups: Awaited<ReturnType<typeof fetchGroups>>;
|
|
341
|
+
try {
|
|
342
|
+
groups = await fetchGroups(existingToken);
|
|
343
|
+
} catch {
|
|
344
|
+
spin.stop("Failed");
|
|
345
|
+
await prompter.note(
|
|
346
|
+
"Could not fetch groups. Check your access token and try again.",
|
|
347
|
+
"GroupMe error",
|
|
348
|
+
);
|
|
349
|
+
throw new Error("Could not fetch groups");
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
if (groups.length === 0) {
|
|
353
|
+
spin.stop("No groups found");
|
|
354
|
+
await prompter.note(
|
|
355
|
+
"No groups found. Create or join a GroupMe group first.",
|
|
356
|
+
"No groups",
|
|
357
|
+
);
|
|
358
|
+
return "skip";
|
|
359
|
+
}
|
|
360
|
+
spin.stop(`Found ${groups.length} groups`);
|
|
361
|
+
|
|
362
|
+
const newGroupId = await prompter.select<string>({
|
|
363
|
+
message: "Select a GroupMe group",
|
|
364
|
+
options: groups.map((group) => ({
|
|
365
|
+
value: group.id,
|
|
366
|
+
label: group.name || group.id,
|
|
367
|
+
hint: group.id === account.config.groupId ? "current" : group.id,
|
|
368
|
+
})),
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
const selectedGroup = groups.find((g) => g.id === newGroupId);
|
|
372
|
+
const updates: Record<string, unknown> = { groupId: newGroupId };
|
|
373
|
+
|
|
374
|
+
const registerNew = await prompter.confirm({
|
|
375
|
+
message: "Register a new bot in this group?",
|
|
376
|
+
initialValue: true,
|
|
377
|
+
});
|
|
378
|
+
|
|
379
|
+
if (!registerNew) {
|
|
380
|
+
const newBotId = (
|
|
381
|
+
await prompter.text({
|
|
382
|
+
message:
|
|
383
|
+
"Bot ID for the new group (existing bot won't work in a different group)",
|
|
384
|
+
validate: (value) =>
|
|
385
|
+
value.trim() ? undefined : "Bot ID is required",
|
|
386
|
+
})
|
|
387
|
+
).trim();
|
|
388
|
+
updates.botId = newBotId;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
if (registerNew) {
|
|
392
|
+
const botName = account.config.botName || "openclaw";
|
|
393
|
+
let publicDomain = account.config.publicDomain;
|
|
394
|
+
if (!publicDomain) {
|
|
395
|
+
const domainRaw = (
|
|
396
|
+
await prompter.text({
|
|
397
|
+
message: "Public domain (required for bot registration)",
|
|
398
|
+
validate: (value) => {
|
|
399
|
+
const trimmed = value.trim();
|
|
400
|
+
if (!trimmed) return "Public domain is required";
|
|
401
|
+
const normalized = trimmed
|
|
402
|
+
.replace(/^https?:\/\//, "")
|
|
403
|
+
.replace(/\/+$/, "");
|
|
404
|
+
if (!normalized) return "Public domain is required";
|
|
405
|
+
const parsed = parsePublicDomain(trimmed);
|
|
406
|
+
if (!parsed) return "Public domain is required";
|
|
407
|
+
return undefined;
|
|
408
|
+
},
|
|
409
|
+
})
|
|
410
|
+
).trim();
|
|
411
|
+
publicDomain = parsePublicDomain(domainRaw);
|
|
412
|
+
updates.publicDomain = publicDomain;
|
|
413
|
+
}
|
|
414
|
+
let rawCallbackUrl = account.config.callbackUrl;
|
|
415
|
+
if (!rawCallbackUrl) {
|
|
416
|
+
rawCallbackUrl = generateCallbackUrl();
|
|
417
|
+
updates.callbackUrl = rawCallbackUrl;
|
|
418
|
+
}
|
|
419
|
+
const parsedCallback = new URL(rawCallbackUrl, "http://localhost");
|
|
420
|
+
const callbackPath = `${parsedCallback.pathname}${parsedCallback.search}`;
|
|
421
|
+
|
|
422
|
+
const botSpin = prompter.progress("Registering bot with GroupMe...");
|
|
423
|
+
try {
|
|
424
|
+
const bot = await createBot({
|
|
425
|
+
accessToken: existingToken,
|
|
426
|
+
name: botName,
|
|
427
|
+
groupId: newGroupId,
|
|
428
|
+
callbackUrl: `https://${publicDomain}${callbackPath}`,
|
|
429
|
+
});
|
|
430
|
+
updates.botId = bot.bot_id;
|
|
431
|
+
botSpin.stop("Bot registered");
|
|
432
|
+
} catch (error) {
|
|
433
|
+
botSpin.stop("Failed");
|
|
434
|
+
const detail =
|
|
435
|
+
error instanceof Error ? `\n\nDetails: ${error.message}` : "";
|
|
436
|
+
await prompter.note(
|
|
437
|
+
`Failed to register bot.${detail}`,
|
|
438
|
+
"Bot registration failed",
|
|
439
|
+
);
|
|
440
|
+
throw new Error("Failed to register GroupMe bot", {
|
|
441
|
+
cause: error instanceof Error ? error : undefined,
|
|
442
|
+
});
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
const next = applyGroupMeConfig({ cfg, accountId, updates });
|
|
447
|
+
await prompter.note(
|
|
448
|
+
`Group changed to "${selectedGroup?.name ?? newGroupId}".`,
|
|
449
|
+
"Group updated",
|
|
450
|
+
);
|
|
451
|
+
return { cfg: next, accountId };
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
if (action === "regen_callback") {
|
|
455
|
+
const callbackUrl = generateCallbackUrl();
|
|
456
|
+
const next = applyGroupMeConfig({
|
|
457
|
+
cfg,
|
|
458
|
+
accountId,
|
|
459
|
+
updates: { callbackUrl },
|
|
460
|
+
});
|
|
461
|
+
await prompter.note(
|
|
462
|
+
[
|
|
463
|
+
"Callback URL regenerated.",
|
|
464
|
+
"Remember to update your GroupMe bot settings or re-register the bot.",
|
|
465
|
+
].join("\n"),
|
|
466
|
+
"Callback URL updated",
|
|
467
|
+
);
|
|
468
|
+
return { cfg: next, accountId };
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
if (action === "toggle_mention") {
|
|
472
|
+
const current = account.config.requireMention ?? true;
|
|
473
|
+
const next = applyGroupMeConfig({
|
|
474
|
+
cfg,
|
|
475
|
+
accountId,
|
|
476
|
+
updates: { requireMention: !current },
|
|
477
|
+
});
|
|
478
|
+
await prompter.note(
|
|
479
|
+
`requireMention changed from ${current} to ${!current}.`,
|
|
480
|
+
"Mention setting updated",
|
|
481
|
+
);
|
|
482
|
+
return { cfg: next, accountId };
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
if (action === "update_domain") {
|
|
486
|
+
const newDomainRaw = (
|
|
487
|
+
await prompter.text({
|
|
488
|
+
message: "New public domain",
|
|
489
|
+
initialValue: account.config.publicDomain ?? "",
|
|
490
|
+
validate: (value) => {
|
|
491
|
+
const trimmed = value.trim();
|
|
492
|
+
if (!trimmed) return "Public domain is required";
|
|
493
|
+
const normalized = trimmed
|
|
494
|
+
.replace(/^https?:\/\//, "")
|
|
495
|
+
.replace(/\/+$/, "");
|
|
496
|
+
if (!normalized) return "Public domain is required";
|
|
497
|
+
const parsed = parsePublicDomain(trimmed);
|
|
498
|
+
if (!parsed) return "Public domain is required";
|
|
499
|
+
return undefined;
|
|
500
|
+
},
|
|
501
|
+
})
|
|
502
|
+
).trim();
|
|
503
|
+
|
|
504
|
+
const publicDomain = parsePublicDomain(newDomainRaw);
|
|
505
|
+
const next = applyGroupMeConfig({
|
|
506
|
+
cfg,
|
|
507
|
+
accountId,
|
|
508
|
+
updates: { publicDomain },
|
|
509
|
+
});
|
|
510
|
+
await prompter.note(
|
|
511
|
+
`Public domain updated to "${publicDomain}".`,
|
|
512
|
+
"Domain updated",
|
|
513
|
+
);
|
|
514
|
+
return { cfg: next, accountId };
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
return "skip";
|
|
518
|
+
},
|
|
144
519
|
disable: (cfg) => ({
|
|
145
520
|
...cfg,
|
|
146
521
|
channels: {
|
package/src/parse.ts
CHANGED
|
@@ -51,7 +51,7 @@ function parseNumberMatrix(value: unknown): number[][] {
|
|
|
51
51
|
const parsedRow: number[] = [];
|
|
52
52
|
for (const cell of row) {
|
|
53
53
|
const num = readNumber(cell);
|
|
54
|
-
if (
|
|
54
|
+
if (num === undefined) {
|
|
55
55
|
continue;
|
|
56
56
|
}
|
|
57
57
|
parsedRow.push(num);
|
|
@@ -68,7 +68,9 @@ function parseStringArray(value: unknown): string[] {
|
|
|
68
68
|
return [];
|
|
69
69
|
}
|
|
70
70
|
|
|
71
|
-
return value
|
|
71
|
+
return value
|
|
72
|
+
.map((entry) => readString(entry))
|
|
73
|
+
.filter((entry): entry is string => Boolean(entry));
|
|
72
74
|
}
|
|
73
75
|
|
|
74
76
|
function parseAttachment(entry: unknown): GroupMeAttachment | null {
|
|
@@ -86,11 +88,7 @@ function parseAttachment(entry: unknown): GroupMeAttachment | null {
|
|
|
86
88
|
if (!url) {
|
|
87
89
|
return null;
|
|
88
90
|
}
|
|
89
|
-
|
|
90
|
-
type,
|
|
91
|
-
url,
|
|
92
|
-
};
|
|
93
|
-
return imageAttachment;
|
|
91
|
+
return { type, url } satisfies GroupMeImageAttachment;
|
|
94
92
|
}
|
|
95
93
|
|
|
96
94
|
if (type === "location") {
|
|
@@ -100,22 +98,15 @@ function parseAttachment(entry: unknown): GroupMeAttachment | null {
|
|
|
100
98
|
if (!lat || !lng || !name) {
|
|
101
99
|
return null;
|
|
102
100
|
}
|
|
103
|
-
|
|
104
|
-
type,
|
|
105
|
-
lat,
|
|
106
|
-
lng,
|
|
107
|
-
name,
|
|
108
|
-
};
|
|
109
|
-
return locationAttachment;
|
|
101
|
+
return { type, lat, lng, name } satisfies GroupMeLocationAttachment;
|
|
110
102
|
}
|
|
111
103
|
|
|
112
104
|
if (type === "mentions") {
|
|
113
|
-
|
|
105
|
+
return {
|
|
114
106
|
type,
|
|
115
107
|
user_ids: parseStringArray(entry.user_ids),
|
|
116
108
|
loci: parseNumberMatrix(entry.loci),
|
|
117
|
-
};
|
|
118
|
-
return mentionsAttachment;
|
|
109
|
+
} satisfies GroupMeMentionsAttachment;
|
|
119
110
|
}
|
|
120
111
|
|
|
121
112
|
if (type === "emoji") {
|
|
@@ -123,12 +114,11 @@ function parseAttachment(entry: unknown): GroupMeAttachment | null {
|
|
|
123
114
|
if (!placeholder) {
|
|
124
115
|
return null;
|
|
125
116
|
}
|
|
126
|
-
|
|
117
|
+
return {
|
|
127
118
|
type,
|
|
128
119
|
placeholder,
|
|
129
120
|
charmap: parseNumberMatrix(entry.charmap),
|
|
130
|
-
};
|
|
131
|
-
return emojiAttachment;
|
|
121
|
+
} satisfies GroupMeEmojiAttachment;
|
|
132
122
|
}
|
|
133
123
|
|
|
134
124
|
return {
|
|
@@ -141,18 +131,14 @@ function parseAttachments(value: unknown): GroupMeAttachment[] {
|
|
|
141
131
|
if (!Array.isArray(value)) {
|
|
142
132
|
return [];
|
|
143
133
|
}
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
const parsed = parseAttachment(entry);
|
|
148
|
-
if (parsed) {
|
|
149
|
-
attachments.push(parsed);
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
|
-
return attachments;
|
|
134
|
+
return value
|
|
135
|
+
.map((entry) => parseAttachment(entry))
|
|
136
|
+
.filter((parsed): parsed is GroupMeAttachment => parsed !== null);
|
|
153
137
|
}
|
|
154
138
|
|
|
155
|
-
export function parseGroupMeCallback(
|
|
139
|
+
export function parseGroupMeCallback(
|
|
140
|
+
data: unknown,
|
|
141
|
+
): GroupMeCallbackData | null {
|
|
156
142
|
if (!isRecord(data)) {
|
|
157
143
|
return null;
|
|
158
144
|
}
|
|
@@ -166,7 +152,15 @@ export function parseGroupMeCallback(data: unknown): GroupMeCallbackData | null
|
|
|
166
152
|
const sourceGuid = readString(data.source_guid);
|
|
167
153
|
const createdAt = readNumber(data.created_at);
|
|
168
154
|
|
|
169
|
-
if (
|
|
155
|
+
if (
|
|
156
|
+
!id ||
|
|
157
|
+
!name ||
|
|
158
|
+
!senderType ||
|
|
159
|
+
!senderId ||
|
|
160
|
+
!userId ||
|
|
161
|
+
!groupId ||
|
|
162
|
+
!sourceGuid
|
|
163
|
+
) {
|
|
170
164
|
return null;
|
|
171
165
|
}
|
|
172
166
|
if (typeof createdAt !== "number") {
|
|
@@ -212,12 +206,17 @@ export function shouldProcessCallback(msg: GroupMeCallbackData): string | null {
|
|
|
212
206
|
|
|
213
207
|
export function extractImageUrls(attachments: GroupMeAttachment[]): string[] {
|
|
214
208
|
return attachments
|
|
215
|
-
.filter(
|
|
209
|
+
.filter(
|
|
210
|
+
(attachment): attachment is GroupMeImageAttachment =>
|
|
211
|
+
attachment.type === "image",
|
|
212
|
+
)
|
|
216
213
|
.map((attachment) => attachment.url);
|
|
217
214
|
}
|
|
218
215
|
|
|
219
216
|
function normalizeMentionText(text: string): string {
|
|
220
|
-
return text
|
|
217
|
+
return text
|
|
218
|
+
.replace(/[\u200b-\u200f\u202a-\u202e\u2060-\u206f]/g, "")
|
|
219
|
+
.toLowerCase();
|
|
221
220
|
}
|
|
222
221
|
|
|
223
222
|
function buildRegexes(patterns?: string[]): RegExp[] {
|
package/src/policy.ts
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
normalizeGroupMeAllowEntry,
|
|
3
|
+
normalizeStringId,
|
|
4
|
+
} from "./normalize.js";
|
|
2
5
|
|
|
3
6
|
export function resolveSenderAccess(params: {
|
|
4
7
|
senderId: string;
|
|
5
8
|
allowFrom?: Array<string | number>;
|
|
6
9
|
}): boolean {
|
|
7
|
-
const senderId =
|
|
10
|
+
const senderId = normalizeStringId(params.senderId);
|
|
8
11
|
if (!senderId) {
|
|
9
12
|
return false;
|
|
10
13
|
}
|