mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-09 19:54:32 +00:00
fix(telegram): block unauthorized DM media downloads
This commit is contained in:
@@ -25,6 +25,7 @@ Docs: https://docs.openclaw.ai
|
|||||||
- Config/Plugins: treat stale removed `google-antigravity-auth` plugin references as compatibility warnings (not hard validation errors) across `plugins.entries`, `plugins.allow`, `plugins.deny`, and `plugins.slots.memory`, so startup no longer fails after antigravity removal. (#25538, #25862) Thanks @chilu18.
|
- Config/Plugins: treat stale removed `google-antigravity-auth` plugin references as compatibility warnings (not hard validation errors) across `plugins.entries`, `plugins.allow`, `plugins.deny`, and `plugins.slots.memory`, so startup no longer fails after antigravity removal. (#25538, #25862) Thanks @chilu18.
|
||||||
- Security/Message actions: enforce local media root checks for `sendAttachment` and `setGroupIcon` when `sandboxRoot` is unset, preventing attachment hydration from reading arbitrary host files via local absolute paths. This ships in the next npm release. Thanks @GCXWLP for reporting.
|
- Security/Message actions: enforce local media root checks for `sendAttachment` and `setGroupIcon` when `sandboxRoot` is unset, preventing attachment hydration from reading arbitrary host files via local absolute paths. This ships in the next npm release. Thanks @GCXWLP for reporting.
|
||||||
- Zalo/Group policy: enforce sender authorization for group messages with `groupPolicy` + `groupAllowFrom` (fallback to `allowFrom`), default runtime group behavior to fail-closed allowlist, and block unauthorized non-command group messages before dispatch. This ships in the next npm release. Thanks @tdjackey for reporting.
|
- Zalo/Group policy: enforce sender authorization for group messages with `groupPolicy` + `groupAllowFrom` (fallback to `allowFrom`), default runtime group behavior to fail-closed allowlist, and block unauthorized non-command group messages before dispatch. This ships in the next npm release. Thanks @tdjackey for reporting.
|
||||||
|
- Security/Telegram: enforce DM authorization before media download/write (including media groups) and move telegram inbound activity tracking after DM authorization, preventing unauthorized sender-triggered inbound media disk writes. This ships in the next npm release. Thanks @v8hid for reporting.
|
||||||
- Security/Workspace FS: normalize `@`-prefixed paths before workspace-boundary checks (including workspace-only read/write/edit and sandbox mount path guards), preventing absolute-path escape attempts from bypassing guard validation. This ships in the next npm release. Thanks @tdjackey for reporting.
|
- Security/Workspace FS: normalize `@`-prefixed paths before workspace-boundary checks (including workspace-only read/write/edit and sandbox mount path guards), preventing absolute-path escape attempts from bypassing guard validation. This ships in the next npm release. Thanks @tdjackey for reporting.
|
||||||
- Security/Synology Chat: enforce fail-closed allowlist behavior for DM ingress so `dmPolicy: "allowlist"` with empty `allowedUserIds` rejects all senders instead of allowing unauthorized dispatch. This ships in the next npm release. Thanks @tdjackey for reporting.
|
- Security/Synology Chat: enforce fail-closed allowlist behavior for DM ingress so `dmPolicy: "allowlist"` with empty `allowedUserIds` rejects all senders instead of allowing unauthorized dispatch. This ships in the next npm release. Thanks @tdjackey for reporting.
|
||||||
- Security/Native images: enforce `tools.fs.workspaceOnly` for native prompt image auto-load (including history refs), preventing out-of-workspace sandbox mounts from being implicitly ingested as vision input. This ships in the next npm release. Thanks @tdjackey for reporting.
|
- Security/Native images: enforce `tools.fs.workspaceOnly` for native prompt image auto-load (including history refs), preventing out-of-workspace sandbox mounts from being implicitly ingested as vision input. This ships in the next npm release. Thanks @tdjackey for reporting.
|
||||||
|
|||||||
@@ -45,6 +45,7 @@ import {
|
|||||||
resolveTelegramGroupAllowFromContext,
|
resolveTelegramGroupAllowFromContext,
|
||||||
} from "./bot/helpers.js";
|
} from "./bot/helpers.js";
|
||||||
import type { TelegramContext } from "./bot/types.js";
|
import type { TelegramContext } from "./bot/types.js";
|
||||||
|
import { enforceTelegramDmAccess } from "./dm-access.js";
|
||||||
import {
|
import {
|
||||||
evaluateTelegramGroupBaseAccess,
|
evaluateTelegramGroupBaseAccess,
|
||||||
evaluateTelegramGroupPolicyAccess,
|
evaluateTelegramGroupPolicyAccess,
|
||||||
@@ -79,6 +80,7 @@ export const registerTelegramHandlers = ({
|
|||||||
runtime,
|
runtime,
|
||||||
mediaMaxBytes,
|
mediaMaxBytes,
|
||||||
telegramCfg,
|
telegramCfg,
|
||||||
|
allowFrom,
|
||||||
groupAllowFrom,
|
groupAllowFrom,
|
||||||
resolveGroupPolicy,
|
resolveGroupPolicy,
|
||||||
resolveTelegramGroupConfig,
|
resolveTelegramGroupConfig,
|
||||||
@@ -1182,6 +1184,38 @@ export const registerTelegramHandlers = ({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const hasInboundMedia =
|
||||||
|
Boolean(event.msg.media_group_id) ||
|
||||||
|
(Array.isArray(event.msg.photo) && event.msg.photo.length > 0) ||
|
||||||
|
Boolean(
|
||||||
|
event.msg.video ??
|
||||||
|
event.msg.video_note ??
|
||||||
|
event.msg.document ??
|
||||||
|
event.msg.audio ??
|
||||||
|
event.msg.voice ??
|
||||||
|
event.msg.sticker,
|
||||||
|
);
|
||||||
|
if (!event.isGroup && hasInboundMedia) {
|
||||||
|
const effectiveDmAllow = normalizeAllowFromWithStore({
|
||||||
|
allowFrom,
|
||||||
|
storeAllowFrom,
|
||||||
|
dmPolicy: telegramCfg.dmPolicy ?? "pairing",
|
||||||
|
});
|
||||||
|
const dmAuthorized = await enforceTelegramDmAccess({
|
||||||
|
isGroup: event.isGroup,
|
||||||
|
dmPolicy: telegramCfg.dmPolicy ?? "pairing",
|
||||||
|
msg: event.msg,
|
||||||
|
chatId: event.chatId,
|
||||||
|
effectiveDmAllow,
|
||||||
|
accountId,
|
||||||
|
bot,
|
||||||
|
logger,
|
||||||
|
});
|
||||||
|
if (!dmAuthorized) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
await processInboundMessage({
|
await processInboundMessage({
|
||||||
ctx: event.ctx,
|
ctx: event.ctx,
|
||||||
msg: event.msg,
|
msg: event.msg,
|
||||||
|
|||||||
@@ -33,17 +33,10 @@ import { readSessionUpdatedAt, resolveStorePath } from "../config/sessions.js";
|
|||||||
import type { DmPolicy, TelegramGroupConfig, TelegramTopicConfig } from "../config/types.js";
|
import type { DmPolicy, TelegramGroupConfig, TelegramTopicConfig } from "../config/types.js";
|
||||||
import { logVerbose, shouldLogVerbose } from "../globals.js";
|
import { logVerbose, shouldLogVerbose } from "../globals.js";
|
||||||
import { recordChannelActivity } from "../infra/channel-activity.js";
|
import { recordChannelActivity } from "../infra/channel-activity.js";
|
||||||
import { buildPairingReply } from "../pairing/pairing-messages.js";
|
|
||||||
import { upsertChannelPairingRequest } from "../pairing/pairing-store.js";
|
|
||||||
import { resolveAgentRoute } from "../routing/resolve-route.js";
|
import { resolveAgentRoute } from "../routing/resolve-route.js";
|
||||||
import { resolveThreadSessionKeys } from "../routing/session-key.js";
|
import { resolveThreadSessionKeys } from "../routing/session-key.js";
|
||||||
import { withTelegramApiErrorLogging } from "./api-logging.js";
|
import { withTelegramApiErrorLogging } from "./api-logging.js";
|
||||||
import {
|
import { firstDefined, isSenderAllowed, normalizeAllowFromWithStore } from "./bot-access.js";
|
||||||
firstDefined,
|
|
||||||
isSenderAllowed,
|
|
||||||
normalizeAllowFromWithStore,
|
|
||||||
resolveSenderAllowMatch,
|
|
||||||
} from "./bot-access.js";
|
|
||||||
import {
|
import {
|
||||||
buildGroupLabel,
|
buildGroupLabel,
|
||||||
buildSenderLabel,
|
buildSenderLabel,
|
||||||
@@ -61,6 +54,7 @@ import {
|
|||||||
resolveTelegramThreadSpec,
|
resolveTelegramThreadSpec,
|
||||||
} from "./bot/helpers.js";
|
} from "./bot/helpers.js";
|
||||||
import type { StickerMetadata, TelegramContext } from "./bot/types.js";
|
import type { StickerMetadata, TelegramContext } from "./bot/types.js";
|
||||||
|
import { enforceTelegramDmAccess } from "./dm-access.js";
|
||||||
import { evaluateTelegramGroupBaseAccess } from "./group-access.js";
|
import { evaluateTelegramGroupBaseAccess } from "./group-access.js";
|
||||||
import { resolveTelegramGroupPromptSettings } from "./group-config-helpers.js";
|
import { resolveTelegramGroupPromptSettings } from "./group-config-helpers.js";
|
||||||
import {
|
import {
|
||||||
@@ -159,11 +153,6 @@ export const buildTelegramMessageContext = async ({
|
|||||||
resolveTelegramGroupConfig,
|
resolveTelegramGroupConfig,
|
||||||
}: BuildTelegramMessageContextParams) => {
|
}: BuildTelegramMessageContextParams) => {
|
||||||
const msg = primaryCtx.message;
|
const msg = primaryCtx.message;
|
||||||
recordChannelActivity({
|
|
||||||
channel: "telegram",
|
|
||||||
accountId: account.accountId,
|
|
||||||
direction: "inbound",
|
|
||||||
});
|
|
||||||
const chatId = msg.chat.id;
|
const chatId = msg.chat.id;
|
||||||
const isGroup = msg.chat.type === "group" || msg.chat.type === "supergroup";
|
const isGroup = msg.chat.type === "group" || msg.chat.type === "supergroup";
|
||||||
const messageThreadId = (msg as { message_thread_id?: number }).message_thread_id;
|
const messageThreadId = (msg as { message_thread_id?: number }).message_thread_id;
|
||||||
@@ -268,87 +257,27 @@ export const buildTelegramMessageContext = async ({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// DM access control (secure defaults): "pairing" (default) / "allowlist" / "open" / "disabled"
|
if (
|
||||||
if (!isGroup) {
|
!(await enforceTelegramDmAccess({
|
||||||
if (dmPolicy === "disabled") {
|
isGroup,
|
||||||
return null;
|
dmPolicy,
|
||||||
}
|
msg,
|
||||||
|
chatId,
|
||||||
if (dmPolicy !== "open") {
|
effectiveDmAllow,
|
||||||
const senderUsername = msg.from?.username ?? "";
|
accountId: account.accountId,
|
||||||
const senderUserId = msg.from?.id != null ? String(msg.from.id) : null;
|
bot,
|
||||||
const candidate = senderUserId ?? String(chatId);
|
logger,
|
||||||
const allowMatch = resolveSenderAllowMatch({
|
}))
|
||||||
allow: effectiveDmAllow,
|
) {
|
||||||
senderId: candidate,
|
return null;
|
||||||
senderUsername,
|
|
||||||
});
|
|
||||||
const allowMatchMeta = `matchKey=${allowMatch.matchKey ?? "none"} matchSource=${
|
|
||||||
allowMatch.matchSource ?? "none"
|
|
||||||
}`;
|
|
||||||
const allowed =
|
|
||||||
effectiveDmAllow.hasWildcard || (effectiveDmAllow.hasEntries && allowMatch.allowed);
|
|
||||||
if (!allowed) {
|
|
||||||
if (dmPolicy === "pairing") {
|
|
||||||
try {
|
|
||||||
const from = msg.from as
|
|
||||||
| {
|
|
||||||
first_name?: string;
|
|
||||||
last_name?: string;
|
|
||||||
username?: string;
|
|
||||||
id?: number;
|
|
||||||
}
|
|
||||||
| undefined;
|
|
||||||
const telegramUserId = from?.id ? String(from.id) : candidate;
|
|
||||||
const { code, created } = await upsertChannelPairingRequest({
|
|
||||||
channel: "telegram",
|
|
||||||
id: telegramUserId,
|
|
||||||
accountId: account.accountId,
|
|
||||||
meta: {
|
|
||||||
username: from?.username,
|
|
||||||
firstName: from?.first_name,
|
|
||||||
lastName: from?.last_name,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
if (created) {
|
|
||||||
logger.info(
|
|
||||||
{
|
|
||||||
chatId: String(chatId),
|
|
||||||
senderUserId: senderUserId ?? undefined,
|
|
||||||
username: from?.username,
|
|
||||||
firstName: from?.first_name,
|
|
||||||
lastName: from?.last_name,
|
|
||||||
matchKey: allowMatch.matchKey ?? "none",
|
|
||||||
matchSource: allowMatch.matchSource ?? "none",
|
|
||||||
},
|
|
||||||
"telegram pairing request",
|
|
||||||
);
|
|
||||||
await withTelegramApiErrorLogging({
|
|
||||||
operation: "sendMessage",
|
|
||||||
fn: () =>
|
|
||||||
bot.api.sendMessage(
|
|
||||||
chatId,
|
|
||||||
buildPairingReply({
|
|
||||||
channel: "telegram",
|
|
||||||
idLine: `Your Telegram user id: ${telegramUserId}`,
|
|
||||||
code,
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
logVerbose(`telegram pairing reply failed for chat ${chatId}: ${String(err)}`);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
logVerbose(
|
|
||||||
`Blocked unauthorized telegram sender ${candidate} (dmPolicy=${dmPolicy}, ${allowMatchMeta})`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
recordChannelActivity({
|
||||||
|
channel: "telegram",
|
||||||
|
accountId: account.accountId,
|
||||||
|
direction: "inbound",
|
||||||
|
});
|
||||||
|
|
||||||
const botUsername = primaryCtx.me?.username?.toLowerCase();
|
const botUsername = primaryCtx.me?.username?.toLowerCase();
|
||||||
const allowForCommands = isGroup ? effectiveGroupAllow : effectiveDmAllow;
|
const allowForCommands = isGroup ? effectiveGroupAllow : effectiveDmAllow;
|
||||||
const senderAllowedForCommands = isSenderAllowed({
|
const senderAllowedForCommands = isSenderAllowed({
|
||||||
|
|||||||
@@ -91,6 +91,7 @@ export type RegisterTelegramHandlerParams = {
|
|||||||
opts: TelegramBotOptions;
|
opts: TelegramBotOptions;
|
||||||
runtime: RuntimeEnv;
|
runtime: RuntimeEnv;
|
||||||
telegramCfg: TelegramAccountConfig;
|
telegramCfg: TelegramAccountConfig;
|
||||||
|
allowFrom?: Array<string | number>;
|
||||||
groupAllowFrom?: Array<string | number>;
|
groupAllowFrom?: Array<string | number>;
|
||||||
resolveGroupPolicy: (chatId: string | number) => ChannelGroupPolicy;
|
resolveGroupPolicy: (chatId: string | number) => ChannelGroupPolicy;
|
||||||
resolveTelegramGroupConfig: (
|
resolveTelegramGroupConfig: (
|
||||||
|
|||||||
@@ -329,6 +329,133 @@ describe("createTelegramBot", () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
it("blocks unauthorized DM media before download and sends pairing reply", async () => {
|
||||||
|
loadConfig.mockReturnValue({
|
||||||
|
channels: { telegram: { dmPolicy: "pairing" } },
|
||||||
|
});
|
||||||
|
readChannelAllowFromStore.mockResolvedValue([]);
|
||||||
|
upsertChannelPairingRequest.mockResolvedValue({ code: "PAIRME12", created: true });
|
||||||
|
sendMessageSpy.mockClear();
|
||||||
|
replySpy.mockClear();
|
||||||
|
|
||||||
|
const fetchSpy = vi.spyOn(globalThis, "fetch").mockImplementation(
|
||||||
|
async () =>
|
||||||
|
new Response(new Uint8Array([0xff, 0xd8, 0xff, 0x00]), {
|
||||||
|
status: 200,
|
||||||
|
headers: { "content-type": "image/jpeg" },
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
const getFileSpy = vi.fn(async () => ({ file_path: "photos/p1.jpg" }));
|
||||||
|
|
||||||
|
try {
|
||||||
|
createTelegramBot({ token: "tok" });
|
||||||
|
const handler = getOnHandler("message") as (ctx: Record<string, unknown>) => Promise<void>;
|
||||||
|
|
||||||
|
await handler({
|
||||||
|
message: {
|
||||||
|
chat: { id: 1234, type: "private" },
|
||||||
|
message_id: 410,
|
||||||
|
date: 1736380800,
|
||||||
|
photo: [{ file_id: "p1" }],
|
||||||
|
from: { id: 999, username: "random" },
|
||||||
|
},
|
||||||
|
me: { username: "openclaw_bot" },
|
||||||
|
getFile: getFileSpy,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(getFileSpy).not.toHaveBeenCalled();
|
||||||
|
expect(fetchSpy).not.toHaveBeenCalled();
|
||||||
|
expect(sendMessageSpy).toHaveBeenCalledTimes(1);
|
||||||
|
expect(String(sendMessageSpy.mock.calls[0]?.[1])).toContain("Pairing code:");
|
||||||
|
expect(replySpy).not.toHaveBeenCalled();
|
||||||
|
} finally {
|
||||||
|
fetchSpy.mockRestore();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
it("blocks DM media downloads completely when dmPolicy is disabled", async () => {
|
||||||
|
loadConfig.mockReturnValue({
|
||||||
|
channels: { telegram: { dmPolicy: "disabled" } },
|
||||||
|
});
|
||||||
|
sendMessageSpy.mockClear();
|
||||||
|
replySpy.mockClear();
|
||||||
|
|
||||||
|
const fetchSpy = vi.spyOn(globalThis, "fetch").mockImplementation(
|
||||||
|
async () =>
|
||||||
|
new Response(new Uint8Array([0xff, 0xd8, 0xff, 0x00]), {
|
||||||
|
status: 200,
|
||||||
|
headers: { "content-type": "image/jpeg" },
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
const getFileSpy = vi.fn(async () => ({ file_path: "photos/p1.jpg" }));
|
||||||
|
|
||||||
|
try {
|
||||||
|
createTelegramBot({ token: "tok" });
|
||||||
|
const handler = getOnHandler("message") as (ctx: Record<string, unknown>) => Promise<void>;
|
||||||
|
|
||||||
|
await handler({
|
||||||
|
message: {
|
||||||
|
chat: { id: 1234, type: "private" },
|
||||||
|
message_id: 411,
|
||||||
|
date: 1736380800,
|
||||||
|
photo: [{ file_id: "p1" }],
|
||||||
|
from: { id: 999, username: "random" },
|
||||||
|
},
|
||||||
|
me: { username: "openclaw_bot" },
|
||||||
|
getFile: getFileSpy,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(getFileSpy).not.toHaveBeenCalled();
|
||||||
|
expect(fetchSpy).not.toHaveBeenCalled();
|
||||||
|
expect(sendMessageSpy).not.toHaveBeenCalled();
|
||||||
|
expect(replySpy).not.toHaveBeenCalled();
|
||||||
|
} finally {
|
||||||
|
fetchSpy.mockRestore();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
it("blocks unauthorized DM media groups before any photo download", async () => {
|
||||||
|
loadConfig.mockReturnValue({
|
||||||
|
channels: { telegram: { dmPolicy: "pairing" } },
|
||||||
|
});
|
||||||
|
readChannelAllowFromStore.mockResolvedValue([]);
|
||||||
|
upsertChannelPairingRequest.mockResolvedValue({ code: "PAIRME12", created: true });
|
||||||
|
sendMessageSpy.mockClear();
|
||||||
|
replySpy.mockClear();
|
||||||
|
|
||||||
|
const fetchSpy = vi.spyOn(globalThis, "fetch").mockImplementation(
|
||||||
|
async () =>
|
||||||
|
new Response(new Uint8Array([0xff, 0xd8, 0xff, 0x00]), {
|
||||||
|
status: 200,
|
||||||
|
headers: { "content-type": "image/jpeg" },
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
const getFileSpy = vi.fn(async () => ({ file_path: "photos/p1.jpg" }));
|
||||||
|
|
||||||
|
try {
|
||||||
|
createTelegramBot({ token: "tok", testTimings: TELEGRAM_TEST_TIMINGS });
|
||||||
|
const handler = getOnHandler("message") as (ctx: Record<string, unknown>) => Promise<void>;
|
||||||
|
|
||||||
|
await handler({
|
||||||
|
message: {
|
||||||
|
chat: { id: 1234, type: "private" },
|
||||||
|
message_id: 412,
|
||||||
|
media_group_id: "dm-album-1",
|
||||||
|
date: 1736380800,
|
||||||
|
photo: [{ file_id: "p1" }],
|
||||||
|
from: { id: 999, username: "random" },
|
||||||
|
},
|
||||||
|
me: { username: "openclaw_bot" },
|
||||||
|
getFile: getFileSpy,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(getFileSpy).not.toHaveBeenCalled();
|
||||||
|
expect(fetchSpy).not.toHaveBeenCalled();
|
||||||
|
expect(sendMessageSpy).toHaveBeenCalledTimes(1);
|
||||||
|
expect(String(sendMessageSpy.mock.calls[0]?.[1])).toContain("Pairing code:");
|
||||||
|
expect(replySpy).not.toHaveBeenCalled();
|
||||||
|
} finally {
|
||||||
|
fetchSpy.mockRestore();
|
||||||
|
}
|
||||||
|
});
|
||||||
it("triggers typing cue via onReplyStart", async () => {
|
it("triggers typing cue via onReplyStart", async () => {
|
||||||
createTelegramBot({ token: "tok" });
|
createTelegramBot({ token: "tok" });
|
||||||
const handler = getOnHandler("message") as (ctx: Record<string, unknown>) => Promise<void>;
|
const handler = getOnHandler("message") as (ctx: Record<string, unknown>) => Promise<void>;
|
||||||
|
|||||||
@@ -370,7 +370,7 @@ describe("telegram media groups", () => {
|
|||||||
() => {
|
() => {
|
||||||
expect(replySpy).toHaveBeenCalledTimes(scenario.expectedReplyCount);
|
expect(replySpy).toHaveBeenCalledTimes(scenario.expectedReplyCount);
|
||||||
},
|
},
|
||||||
{ timeout: MEDIA_GROUP_FLUSH_MS * 2, interval: 2 },
|
{ timeout: MEDIA_GROUP_FLUSH_MS * 4, interval: 2 },
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(runtimeError).not.toHaveBeenCalled();
|
expect(runtimeError).not.toHaveBeenCalled();
|
||||||
|
|||||||
@@ -398,6 +398,7 @@ export function createTelegramBot(opts: TelegramBotOptions) {
|
|||||||
runtime,
|
runtime,
|
||||||
mediaMaxBytes,
|
mediaMaxBytes,
|
||||||
telegramCfg,
|
telegramCfg,
|
||||||
|
allowFrom,
|
||||||
groupAllowFrom,
|
groupAllowFrom,
|
||||||
resolveGroupPolicy,
|
resolveGroupPolicy,
|
||||||
resolveTelegramGroupConfig,
|
resolveTelegramGroupConfig,
|
||||||
|
|||||||
109
src/telegram/dm-access.ts
Normal file
109
src/telegram/dm-access.ts
Normal file
@@ -0,0 +1,109 @@
|
|||||||
|
import type { Message } from "@grammyjs/types";
|
||||||
|
import type { Bot } from "grammy";
|
||||||
|
import type { DmPolicy } from "../config/types.js";
|
||||||
|
import { logVerbose } from "../globals.js";
|
||||||
|
import { buildPairingReply } from "../pairing/pairing-messages.js";
|
||||||
|
import { upsertChannelPairingRequest } from "../pairing/pairing-store.js";
|
||||||
|
import { withTelegramApiErrorLogging } from "./api-logging.js";
|
||||||
|
import { resolveSenderAllowMatch, type NormalizedAllowFrom } from "./bot-access.js";
|
||||||
|
|
||||||
|
type TelegramDmAccessLogger = {
|
||||||
|
info: (obj: Record<string, unknown>, msg: string) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export async function enforceTelegramDmAccess(params: {
|
||||||
|
isGroup: boolean;
|
||||||
|
dmPolicy: DmPolicy;
|
||||||
|
msg: Message;
|
||||||
|
chatId: number;
|
||||||
|
effectiveDmAllow: NormalizedAllowFrom;
|
||||||
|
accountId: string;
|
||||||
|
bot: Bot;
|
||||||
|
logger: TelegramDmAccessLogger;
|
||||||
|
}): Promise<boolean> {
|
||||||
|
const { isGroup, dmPolicy, msg, chatId, effectiveDmAllow, accountId, bot, logger } = params;
|
||||||
|
if (isGroup) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (dmPolicy === "disabled") {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (dmPolicy === "open") {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const senderUsername = msg.from?.username ?? "";
|
||||||
|
const senderUserId = msg.from?.id != null ? String(msg.from.id) : null;
|
||||||
|
const candidate = senderUserId ?? String(chatId);
|
||||||
|
const allowMatch = resolveSenderAllowMatch({
|
||||||
|
allow: effectiveDmAllow,
|
||||||
|
senderId: candidate,
|
||||||
|
senderUsername,
|
||||||
|
});
|
||||||
|
const allowMatchMeta = `matchKey=${allowMatch.matchKey ?? "none"} matchSource=${
|
||||||
|
allowMatch.matchSource ?? "none"
|
||||||
|
}`;
|
||||||
|
const allowed =
|
||||||
|
effectiveDmAllow.hasWildcard || (effectiveDmAllow.hasEntries && allowMatch.allowed);
|
||||||
|
if (allowed) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (dmPolicy === "pairing") {
|
||||||
|
try {
|
||||||
|
const from = msg.from as
|
||||||
|
| {
|
||||||
|
first_name?: string;
|
||||||
|
last_name?: string;
|
||||||
|
username?: string;
|
||||||
|
id?: number;
|
||||||
|
}
|
||||||
|
| undefined;
|
||||||
|
const telegramUserId = from?.id ? String(from.id) : candidate;
|
||||||
|
const { code, created } = await upsertChannelPairingRequest({
|
||||||
|
channel: "telegram",
|
||||||
|
id: telegramUserId,
|
||||||
|
accountId,
|
||||||
|
meta: {
|
||||||
|
username: from?.username,
|
||||||
|
firstName: from?.first_name,
|
||||||
|
lastName: from?.last_name,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
if (created) {
|
||||||
|
logger.info(
|
||||||
|
{
|
||||||
|
chatId: String(chatId),
|
||||||
|
senderUserId: senderUserId ?? undefined,
|
||||||
|
username: from?.username,
|
||||||
|
firstName: from?.first_name,
|
||||||
|
lastName: from?.last_name,
|
||||||
|
matchKey: allowMatch.matchKey ?? "none",
|
||||||
|
matchSource: allowMatch.matchSource ?? "none",
|
||||||
|
},
|
||||||
|
"telegram pairing request",
|
||||||
|
);
|
||||||
|
await withTelegramApiErrorLogging({
|
||||||
|
operation: "sendMessage",
|
||||||
|
fn: () =>
|
||||||
|
bot.api.sendMessage(
|
||||||
|
chatId,
|
||||||
|
buildPairingReply({
|
||||||
|
channel: "telegram",
|
||||||
|
idLine: `Your Telegram user id: ${telegramUserId}`,
|
||||||
|
code,
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
logVerbose(`telegram pairing reply failed for chat ${chatId}: ${String(err)}`);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
logVerbose(
|
||||||
|
`Blocked unauthorized telegram sender ${candidate} (dmPolicy=${dmPolicy}, ${allowMatchMeta})`,
|
||||||
|
);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user