This commit is contained in:
tokyo-product-team-rgb 2026-01-30 17:05:54 +05:30 committed by GitHub
commit 8dcb54dc8c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 184 additions and 11 deletions

View File

@ -170,4 +170,169 @@ describe("handleLineWebhookEvents", () => {
expect(processMessage).not.toHaveBeenCalled(); expect(processMessage).not.toHaveBeenCalled();
expect(buildLineMessageContextMock).not.toHaveBeenCalled(); expect(buildLineMessageContextMock).not.toHaveBeenCalled();
}); });
it("allows group messages when per-group policy is open despite channel allowlist", async () => {
const processMessage = vi.fn();
const event = {
type: "message",
message: { id: "m5", type: "text", text: "hi" },
replyToken: "reply-token",
timestamp: Date.now(),
source: { type: "group", groupId: "group-open", userId: "user-5" },
mode: "active",
webhookEventId: "evt-5",
deliveryContext: { isRedelivery: false },
} as MessageEvent;
await handleLineWebhookEvents([event], {
cfg: { channels: { line: { groupPolicy: "allowlist" } } },
account: {
accountId: "default",
enabled: true,
channelAccessToken: "token",
channelSecret: "secret",
tokenSource: "config",
config: {
groupPolicy: "allowlist",
groups: { "group-open": { policy: "open" } },
},
},
runtime: { error: vi.fn() },
mediaMaxBytes: 1,
processMessage,
});
expect(buildLineMessageContextMock).toHaveBeenCalledTimes(1);
expect(processMessage).toHaveBeenCalledTimes(1);
});
it("blocks group messages when per-group policy is disabled despite channel open", async () => {
const processMessage = vi.fn();
const event = {
type: "message",
message: { id: "m6", type: "text", text: "hi" },
replyToken: "reply-token",
timestamp: Date.now(),
source: { type: "group", groupId: "group-disabled", userId: "user-6" },
mode: "active",
webhookEventId: "evt-6",
deliveryContext: { isRedelivery: false },
} as MessageEvent;
await handleLineWebhookEvents([event], {
cfg: { channels: { line: { groupPolicy: "open" } } },
account: {
accountId: "default",
enabled: true,
channelAccessToken: "token",
channelSecret: "secret",
tokenSource: "config",
config: {
groupPolicy: "open",
groups: { "group-disabled": { policy: "disabled" } },
},
},
runtime: { error: vi.fn() },
mediaMaxBytes: 1,
processMessage,
});
expect(processMessage).not.toHaveBeenCalled();
expect(buildLineMessageContextMock).not.toHaveBeenCalled();
});
it("uses per-group allowlist with group-specific allowFrom", async () => {
const processMessage = vi.fn();
const allowedEvent = {
type: "message",
message: { id: "m7", type: "text", text: "hi" },
replyToken: "reply-token",
timestamp: Date.now(),
source: { type: "group", groupId: "group-restricted", userId: "allowed-user" },
mode: "active",
webhookEventId: "evt-7",
deliveryContext: { isRedelivery: false },
} as MessageEvent;
const blockedEvent = {
type: "message",
message: { id: "m8", type: "text", text: "hi" },
replyToken: "reply-token",
timestamp: Date.now(),
source: { type: "group", groupId: "group-restricted", userId: "blocked-user" },
mode: "active",
webhookEventId: "evt-8",
deliveryContext: { isRedelivery: false },
} as MessageEvent;
const context = {
cfg: { channels: { line: { groupPolicy: "open" } } },
account: {
accountId: "default",
enabled: true,
channelAccessToken: "token",
channelSecret: "secret",
tokenSource: "config",
config: {
groupPolicy: "open",
groups: {
"group-restricted": {
policy: "allowlist",
allowFrom: ["allowed-user"],
},
},
},
},
runtime: { error: vi.fn() },
mediaMaxBytes: 1,
processMessage,
};
await handleLineWebhookEvents([allowedEvent], context);
expect(processMessage).toHaveBeenCalledTimes(1);
processMessage.mockClear();
buildLineMessageContextMock.mockClear();
await handleLineWebhookEvents([blockedEvent], context);
expect(processMessage).not.toHaveBeenCalled();
});
it("falls back to channel-wide policy when no per-group policy is set", async () => {
const processMessage = vi.fn();
const event = {
type: "message",
message: { id: "m9", type: "text", text: "hi" },
replyToken: "reply-token",
timestamp: Date.now(),
source: { type: "group", groupId: "group-no-override", userId: "user-9" },
mode: "active",
webhookEventId: "evt-9",
deliveryContext: { isRedelivery: false },
} as MessageEvent;
await handleLineWebhookEvents([event], {
cfg: { channels: { line: { groupPolicy: "open" } } },
account: {
accountId: "default",
enabled: true,
channelAccessToken: "token",
channelSecret: "secret",
tokenSource: "config",
config: {
groupPolicy: "open",
groups: {
"group-other": { policy: "disabled" },
},
},
},
runtime: { error: vi.fn() },
mediaMaxBytes: 1,
processMessage,
});
// group-no-override has no per-group policy, so channel-wide "open" applies
expect(buildLineMessageContextMock).toHaveBeenCalledTimes(1);
expect(processMessage).toHaveBeenCalledTimes(1);
});
}); });

View File

@ -151,13 +151,21 @@ async function shouldProcessLineEvent(
}); });
const dmPolicy = account.config.dmPolicy ?? "pairing"; const dmPolicy = account.config.dmPolicy ?? "pairing";
const defaultGroupPolicy = cfg.channels?.defaults?.groupPolicy; const defaultGroupPolicy = cfg.channels?.defaults?.groupPolicy;
const groupPolicy = account.config.groupPolicy ?? defaultGroupPolicy ?? "allowlist"; const channelGroupPolicy = account.config.groupPolicy ?? defaultGroupPolicy ?? "allowlist";
// Per-group policy override: groups.C1234.policy takes precedence over channel-wide groupPolicy
const groupPolicy = groupConfig?.policy ?? channelGroupPolicy;
if (isGroup) { if (isGroup) {
if (groupConfig?.enabled === false) { if (groupConfig?.enabled === false) {
logVerbose(`Blocked line group ${groupId ?? roomId ?? "unknown"} (group disabled)`); logVerbose(`Blocked line group ${groupId ?? roomId ?? "unknown"} (group disabled)`);
return false; return false;
} }
if (groupPolicy === "disabled") {
logVerbose("Blocked line group message (groupPolicy: disabled)");
return false;
}
if (groupPolicy === "open") {
// Per-group allowFrom still applies even with open policy, when explicitly set
if (typeof groupAllowOverride !== "undefined") { if (typeof groupAllowOverride !== "undefined") {
if (!senderId) { if (!senderId) {
logVerbose("Blocked line group message (group allowFrom override, no sender ID)"); logVerbose("Blocked line group message (group allowFrom override, no sender ID)");
@ -168,9 +176,7 @@ async function shouldProcessLineEvent(
return false; return false;
} }
} }
if (groupPolicy === "disabled") { return true;
logVerbose("Blocked line group message (groupPolicy: disabled)");
return false;
} }
if (groupPolicy === "allowlist") { if (groupPolicy === "allowlist") {
if (!senderId) { if (!senderId) {

View File

@ -6,6 +6,7 @@ const GroupPolicySchema = z.enum(["open", "allowlist", "disabled"]);
const LineGroupConfigSchema = z const LineGroupConfigSchema = z
.object({ .object({
enabled: z.boolean().optional(), enabled: z.boolean().optional(),
policy: GroupPolicySchema.optional(),
allowFrom: z.array(z.union([z.string(), z.number()])).optional(), allowFrom: z.array(z.union([z.string(), z.number()])).optional(),
requireMention: z.boolean().optional(), requireMention: z.boolean().optional(),
systemPrompt: z.string().optional(), systemPrompt: z.string().optional(),

View File

@ -45,6 +45,7 @@ export interface LineAccountConfig {
export interface LineGroupConfig { export interface LineGroupConfig {
enabled?: boolean; enabled?: boolean;
policy?: "open" | "allowlist" | "disabled";
allowFrom?: Array<string | number>; allowFrom?: Array<string | number>;
requireMention?: boolean; requireMention?: boolean;
systemPrompt?: string; systemPrompt?: string;