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/src/onboarding.ts CHANGED
@@ -1,8 +1,12 @@
1
- import type { ChannelOnboardingAdapter } from "openclaw/plugin-sdk";
2
- import type { OpenClawConfig } from "openclaw/plugin-sdk";
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 botId"}`,
99
+ `GroupMe (${accountId}): ${configured ? "configured" : "needs access token"}`,
64
100
  account.config.accessToken?.trim()
65
101
  ? "Access token configured"
66
- : "Access token missing (needed for image uploads)",
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 bot ID",
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
- await prompter.note(
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 ID",
86
- validate: (value) => (value.trim() ? undefined : "Bot ID is required"),
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: "Access token",
93
- validate: (value) => (value.trim() ? undefined : "Access token is required"),
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 botName = (
98
- await prompter.text({
99
- message: "Bot name (mention fallback)",
100
- initialValue: "openclaw",
101
- })
102
- ).trim();
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
- const callbackPath = (
105
- await prompter.text({
106
- message: "Webhook path",
107
- initialValue: "/groupme",
108
- validate: (value) => (value.trim().startsWith("/") ? undefined : "Path must start with /"),
109
- })
110
- ).trim();
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
- callbackPath,
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
- `1. Set GroupMe callback URL to https://<your-domain>${callbackPath}`,
133
- "2. Restart gateway",
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 (typeof num !== "number") {
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.map((entry) => readString(entry)).filter((entry): entry is string => Boolean(entry));
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
- const imageAttachment: GroupMeImageAttachment = {
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
- const locationAttachment: GroupMeLocationAttachment = {
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
- const mentionsAttachment: GroupMeMentionsAttachment = {
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
- const emojiAttachment: GroupMeEmojiAttachment = {
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
- const attachments: GroupMeAttachment[] = [];
146
- for (const entry of value) {
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(data: unknown): GroupMeCallbackData | null {
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 (!id || !name || !senderType || !senderId || !userId || !groupId || !sourceGuid) {
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((attachment): attachment is GroupMeImageAttachment => attachment.type === "image")
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.replace(/[\u200b-\u200f\u202a-\u202e\u2060-\u206f]/g, "").toLowerCase();
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 { normalizeGroupMeAllowEntry, normalizeGroupMeUserId } from "./normalize.js";
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 = normalizeGroupMeUserId(params.senderId);
10
+ const senderId = normalizeStringId(params.senderId);
8
11
  if (!senderId) {
9
12
  return false;
10
13
  }