Merge f03fab3a9f into da71eaebd2
This commit is contained in:
commit
8dcb54dc8c
@ -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);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -151,27 +151,33 @@ 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 (typeof groupAllowOverride !== "undefined") {
|
|
||||||
if (!senderId) {
|
|
||||||
logVerbose("Blocked line group message (group allowFrom override, no sender ID)");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (!isSenderAllowed({ allow: effectiveGroupAllow, senderId })) {
|
|
||||||
logVerbose(`Blocked line group sender ${senderId} (group allowFrom override)`);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (groupPolicy === "disabled") {
|
if (groupPolicy === "disabled") {
|
||||||
logVerbose("Blocked line group message (groupPolicy: disabled)");
|
logVerbose("Blocked line group message (groupPolicy: disabled)");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
if (groupPolicy === "open") {
|
||||||
|
// Per-group allowFrom still applies even with open policy, when explicitly set
|
||||||
|
if (typeof groupAllowOverride !== "undefined") {
|
||||||
|
if (!senderId) {
|
||||||
|
logVerbose("Blocked line group message (group allowFrom override, no sender ID)");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!isSenderAllowed({ allow: effectiveGroupAllow, senderId })) {
|
||||||
|
logVerbose(`Blocked line group sender ${senderId} (group allowFrom override)`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
if (groupPolicy === "allowlist") {
|
if (groupPolicy === "allowlist") {
|
||||||
if (!senderId) {
|
if (!senderId) {
|
||||||
logVerbose("Blocked line group message (no sender ID, groupPolicy: allowlist)");
|
logVerbose("Blocked line group message (no sender ID, groupPolicy: allowlist)");
|
||||||
|
|||||||
@ -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(),
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user