Telegram: use Grammy types directly, add typed Probe/Audit to plugin interface (#8403)
* Telegram: replace duplicated types with Grammy imports, add Probe/Audit generics to plugin interface * Telegram: remove legacy forward metadata (deprecated in Bot API 7.0), simplify required-field checks * Telegram: clean up remaining legacy references and unnecessary casts * Telegram: keep RequestInit parameter type in proxy fetch (addresses review feedback) * Telegram: add exhaustiveness guard to resolveForwardOrigin switchmain
parent
6341819d74
commit
da6de49815
|
|
@ -25,6 +25,7 @@ import {
|
||||||
type ChannelPlugin,
|
type ChannelPlugin,
|
||||||
type OpenClawConfig,
|
type OpenClawConfig,
|
||||||
type ResolvedTelegramAccount,
|
type ResolvedTelegramAccount,
|
||||||
|
type TelegramProbe,
|
||||||
} from "openclaw/plugin-sdk";
|
} from "openclaw/plugin-sdk";
|
||||||
import { getTelegramRuntime } from "./runtime.js";
|
import { getTelegramRuntime } from "./runtime.js";
|
||||||
|
|
||||||
|
|
@ -60,7 +61,7 @@ function parseThreadId(threadId?: string | number | null) {
|
||||||
const parsed = Number.parseInt(trimmed, 10);
|
const parsed = Number.parseInt(trimmed, 10);
|
||||||
return Number.isFinite(parsed) ? parsed : undefined;
|
return Number.isFinite(parsed) ? parsed : undefined;
|
||||||
}
|
}
|
||||||
export const telegramPlugin: ChannelPlugin<ResolvedTelegramAccount> = {
|
export const telegramPlugin: ChannelPlugin<ResolvedTelegramAccount, TelegramProbe> = {
|
||||||
id: "telegram",
|
id: "telegram",
|
||||||
meta: {
|
meta: {
|
||||||
...meta,
|
...meta,
|
||||||
|
|
@ -327,11 +328,7 @@ export const telegramPlugin: ChannelPlugin<ResolvedTelegramAccount> = {
|
||||||
if (!groupIds.length && unresolvedGroups === 0 && !hasWildcardUnmentionedGroups) {
|
if (!groupIds.length && unresolvedGroups === 0 && !hasWildcardUnmentionedGroups) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
const botId =
|
const botId = probe?.ok && probe.bot?.id != null ? probe.bot.id : null;
|
||||||
(probe as { ok?: boolean; bot?: { id?: number } })?.ok &&
|
|
||||||
(probe as { bot?: { id?: number } }).bot?.id != null
|
|
||||||
? (probe as { bot: { id: number } }).bot.id
|
|
||||||
: null;
|
|
||||||
if (!botId) {
|
if (!botId) {
|
||||||
return {
|
return {
|
||||||
ok: unresolvedGroups === 0 && !hasWildcardUnmentionedGroups,
|
ok: unresolvedGroups === 0 && !hasWildcardUnmentionedGroups,
|
||||||
|
|
@ -357,15 +354,9 @@ export const telegramPlugin: ChannelPlugin<ResolvedTelegramAccount> = {
|
||||||
cfg.channels?.telegram?.accounts?.[account.accountId]?.groups ??
|
cfg.channels?.telegram?.accounts?.[account.accountId]?.groups ??
|
||||||
cfg.channels?.telegram?.groups;
|
cfg.channels?.telegram?.groups;
|
||||||
const allowUnmentionedGroups =
|
const allowUnmentionedGroups =
|
||||||
Boolean(
|
groups?.["*"]?.requireMention === false ||
|
||||||
groups?.["*"] && (groups["*"] as { requireMention?: boolean }).requireMention === false,
|
|
||||||
) ||
|
|
||||||
Object.entries(groups ?? {}).some(
|
Object.entries(groups ?? {}).some(
|
||||||
([key, value]) =>
|
([key, value]) => key !== "*" && value?.requireMention === false,
|
||||||
key !== "*" &&
|
|
||||||
Boolean(value) &&
|
|
||||||
typeof value === "object" &&
|
|
||||||
(value as { requireMention?: boolean }).requireMention === false,
|
|
||||||
);
|
);
|
||||||
return {
|
return {
|
||||||
accountId: account.accountId,
|
accountId: account.accountId,
|
||||||
|
|
|
||||||
|
|
@ -105,7 +105,7 @@ export type ChannelOutboundAdapter = {
|
||||||
sendPoll?: (ctx: ChannelPollContext) => Promise<ChannelPollResult>;
|
sendPoll?: (ctx: ChannelPollContext) => Promise<ChannelPollResult>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type ChannelStatusAdapter<ResolvedAccount> = {
|
export type ChannelStatusAdapter<ResolvedAccount, Probe = unknown, Audit = unknown> = {
|
||||||
defaultRuntime?: ChannelAccountSnapshot;
|
defaultRuntime?: ChannelAccountSnapshot;
|
||||||
buildChannelSummary?: (params: {
|
buildChannelSummary?: (params: {
|
||||||
account: ResolvedAccount;
|
account: ResolvedAccount;
|
||||||
|
|
@ -117,19 +117,19 @@ export type ChannelStatusAdapter<ResolvedAccount> = {
|
||||||
account: ResolvedAccount;
|
account: ResolvedAccount;
|
||||||
timeoutMs: number;
|
timeoutMs: number;
|
||||||
cfg: OpenClawConfig;
|
cfg: OpenClawConfig;
|
||||||
}) => Promise<unknown>;
|
}) => Promise<Probe>;
|
||||||
auditAccount?: (params: {
|
auditAccount?: (params: {
|
||||||
account: ResolvedAccount;
|
account: ResolvedAccount;
|
||||||
timeoutMs: number;
|
timeoutMs: number;
|
||||||
cfg: OpenClawConfig;
|
cfg: OpenClawConfig;
|
||||||
probe?: unknown;
|
probe?: Probe;
|
||||||
}) => Promise<unknown>;
|
}) => Promise<Audit>;
|
||||||
buildAccountSnapshot?: (params: {
|
buildAccountSnapshot?: (params: {
|
||||||
account: ResolvedAccount;
|
account: ResolvedAccount;
|
||||||
cfg: OpenClawConfig;
|
cfg: OpenClawConfig;
|
||||||
runtime?: ChannelAccountSnapshot;
|
runtime?: ChannelAccountSnapshot;
|
||||||
probe?: unknown;
|
probe?: Probe;
|
||||||
audit?: unknown;
|
audit?: Audit;
|
||||||
}) => ChannelAccountSnapshot | Promise<ChannelAccountSnapshot>;
|
}) => ChannelAccountSnapshot | Promise<ChannelAccountSnapshot>;
|
||||||
logSelfId?: (params: {
|
logSelfId?: (params: {
|
||||||
account: ResolvedAccount;
|
account: ResolvedAccount;
|
||||||
|
|
|
||||||
|
|
@ -45,7 +45,7 @@ export type ChannelConfigSchema = {
|
||||||
};
|
};
|
||||||
|
|
||||||
// oxlint-disable-next-line typescript/no-explicit-any
|
// oxlint-disable-next-line typescript/no-explicit-any
|
||||||
export type ChannelPlugin<ResolvedAccount = any> = {
|
export type ChannelPlugin<ResolvedAccount = any, Probe = unknown, Audit = unknown> = {
|
||||||
id: ChannelId;
|
id: ChannelId;
|
||||||
meta: ChannelMeta;
|
meta: ChannelMeta;
|
||||||
capabilities: ChannelCapabilities;
|
capabilities: ChannelCapabilities;
|
||||||
|
|
@ -65,7 +65,7 @@ export type ChannelPlugin<ResolvedAccount = any> = {
|
||||||
groups?: ChannelGroupAdapter;
|
groups?: ChannelGroupAdapter;
|
||||||
mentions?: ChannelMentionAdapter;
|
mentions?: ChannelMentionAdapter;
|
||||||
outbound?: ChannelOutboundAdapter;
|
outbound?: ChannelOutboundAdapter;
|
||||||
status?: ChannelStatusAdapter<ResolvedAccount>;
|
status?: ChannelStatusAdapter<ResolvedAccount, Probe, Audit>;
|
||||||
gatewayMethods?: string[];
|
gatewayMethods?: string[];
|
||||||
gateway?: ChannelGatewayAdapter<ResolvedAccount>;
|
gateway?: ChannelGatewayAdapter<ResolvedAccount>;
|
||||||
auth?: ChannelAuthAdapter;
|
auth?: ChannelAuthAdapter;
|
||||||
|
|
|
||||||
|
|
@ -306,6 +306,7 @@ export {
|
||||||
normalizeTelegramMessagingTarget,
|
normalizeTelegramMessagingTarget,
|
||||||
} from "../channels/plugins/normalize/telegram.js";
|
} from "../channels/plugins/normalize/telegram.js";
|
||||||
export { collectTelegramStatusIssues } from "../channels/plugins/status-issues/telegram.js";
|
export { collectTelegramStatusIssues } from "../channels/plugins/status-issues/telegram.js";
|
||||||
|
export { type TelegramProbe } from "../telegram/probe.js";
|
||||||
|
|
||||||
// Channel: Signal
|
// Channel: Signal
|
||||||
export {
|
export {
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import type { TelegramMessage } from "./bot/types.js";
|
|
||||||
import { resolveDefaultAgentId } from "../agents/agent-scope.js";
|
|
||||||
// @ts-nocheck
|
// @ts-nocheck
|
||||||
|
import type { Message } from "@grammyjs/types";
|
||||||
|
import { resolveDefaultAgentId } from "../agents/agent-scope.js";
|
||||||
import { hasControlCommand } from "../auto-reply/command-detection.js";
|
import { hasControlCommand } from "../auto-reply/command-detection.js";
|
||||||
import {
|
import {
|
||||||
createInboundDebouncer,
|
createInboundDebouncer,
|
||||||
|
|
@ -63,7 +63,7 @@ export const registerTelegramHandlers = ({
|
||||||
|
|
||||||
type TextFragmentEntry = {
|
type TextFragmentEntry = {
|
||||||
key: string;
|
key: string;
|
||||||
messages: Array<{ msg: TelegramMessage; ctx: unknown; receivedAtMs: number }>;
|
messages: Array<{ msg: Message; ctx: unknown; receivedAtMs: number }>;
|
||||||
timer: ReturnType<typeof setTimeout>;
|
timer: ReturnType<typeof setTimeout>;
|
||||||
};
|
};
|
||||||
const textFragmentBuffer = new Map<string, TextFragmentEntry>();
|
const textFragmentBuffer = new Map<string, TextFragmentEntry>();
|
||||||
|
|
@ -72,7 +72,7 @@ export const registerTelegramHandlers = ({
|
||||||
const debounceMs = resolveInboundDebounceMs({ cfg, channel: "telegram" });
|
const debounceMs = resolveInboundDebounceMs({ cfg, channel: "telegram" });
|
||||||
type TelegramDebounceEntry = {
|
type TelegramDebounceEntry = {
|
||||||
ctx: unknown;
|
ctx: unknown;
|
||||||
msg: TelegramMessage;
|
msg: Message;
|
||||||
allMedia: Array<{ path: string; contentType?: string }>;
|
allMedia: Array<{ path: string; contentType?: string }>;
|
||||||
storeAllowFrom: string[];
|
storeAllowFrom: string[];
|
||||||
debounceKey: string | null;
|
debounceKey: string | null;
|
||||||
|
|
@ -111,7 +111,7 @@ export const registerTelegramHandlers = ({
|
||||||
const baseCtx = first.ctx as { me?: unknown; getFile?: unknown } & Record<string, unknown>;
|
const baseCtx = first.ctx as { me?: unknown; getFile?: unknown } & Record<string, unknown>;
|
||||||
const getFile =
|
const getFile =
|
||||||
typeof baseCtx.getFile === "function" ? baseCtx.getFile.bind(baseCtx) : async () => ({});
|
typeof baseCtx.getFile === "function" ? baseCtx.getFile.bind(baseCtx) : async () => ({});
|
||||||
const syntheticMessage: TelegramMessage = {
|
const syntheticMessage: Message = {
|
||||||
...first.msg,
|
...first.msg,
|
||||||
text: combinedText,
|
text: combinedText,
|
||||||
caption: undefined,
|
caption: undefined,
|
||||||
|
|
@ -231,7 +231,7 @@ export const registerTelegramHandlers = ({
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const syntheticMessage: TelegramMessage = {
|
const syntheticMessage: Message = {
|
||||||
...first.msg,
|
...first.msg,
|
||||||
text: combinedText,
|
text: combinedText,
|
||||||
caption: undefined,
|
caption: undefined,
|
||||||
|
|
@ -557,7 +557,7 @@ export const registerTelegramHandlers = ({
|
||||||
if (modelCallback.type === "select") {
|
if (modelCallback.type === "select") {
|
||||||
const { provider, model } = modelCallback;
|
const { provider, model } = modelCallback;
|
||||||
// Process model selection as a synthetic message with /model command
|
// Process model selection as a synthetic message with /model command
|
||||||
const syntheticMessage: TelegramMessage = {
|
const syntheticMessage: Message = {
|
||||||
...callbackMessage,
|
...callbackMessage,
|
||||||
from: callback.from,
|
from: callback.from,
|
||||||
text: `/model ${provider}/${model}`,
|
text: `/model ${provider}/${model}`,
|
||||||
|
|
@ -582,7 +582,7 @@ export const registerTelegramHandlers = ({
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const syntheticMessage: TelegramMessage = {
|
const syntheticMessage: Message = {
|
||||||
...callbackMessage,
|
...callbackMessage,
|
||||||
from: callback.from,
|
from: callback.from,
|
||||||
text: data,
|
text: data,
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ import type {
|
||||||
TelegramTopicConfig,
|
TelegramTopicConfig,
|
||||||
} from "../config/types.js";
|
} from "../config/types.js";
|
||||||
import type { RuntimeEnv } from "../runtime.js";
|
import type { RuntimeEnv } from "../runtime.js";
|
||||||
|
import type { TelegramContext } from "./bot/types.js";
|
||||||
import { resolveEffectiveMessagesConfig } from "../agents/identity.js";
|
import { resolveEffectiveMessagesConfig } from "../agents/identity.js";
|
||||||
import { resolveChunkMode } from "../auto-reply/chunk.js";
|
import { resolveChunkMode } from "../auto-reply/chunk.js";
|
||||||
import {
|
import {
|
||||||
|
|
@ -86,7 +87,7 @@ export type RegisterTelegramHandlerParams = {
|
||||||
) => { groupConfig?: TelegramGroupConfig; topicConfig?: TelegramTopicConfig };
|
) => { groupConfig?: TelegramGroupConfig; topicConfig?: TelegramTopicConfig };
|
||||||
shouldSkipUpdate: (ctx: TelegramUpdateKeyContext) => boolean;
|
shouldSkipUpdate: (ctx: TelegramUpdateKeyContext) => boolean;
|
||||||
processMessage: (
|
processMessage: (
|
||||||
ctx: unknown,
|
ctx: TelegramContext,
|
||||||
allMedia: Array<{ path: string; contentType?: string }>,
|
allMedia: Array<{ path: string; contentType?: string }>,
|
||||||
storeAllowFrom: string[],
|
storeAllowFrom: string[],
|
||||||
options?: {
|
options?: {
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import type { TelegramContext, TelegramMessage } from "./bot/types.js";
|
import type { Message } from "@grammyjs/types";
|
||||||
|
import type { TelegramContext } from "./bot/types.js";
|
||||||
import { createDedupeCache } from "../infra/dedupe.js";
|
import { createDedupeCache } from "../infra/dedupe.js";
|
||||||
|
|
||||||
const MEDIA_GROUP_TIMEOUT_MS = 500;
|
const MEDIA_GROUP_TIMEOUT_MS = 500;
|
||||||
|
|
@ -7,7 +8,7 @@ const RECENT_TELEGRAM_UPDATE_MAX = 2000;
|
||||||
|
|
||||||
export type MediaGroupEntry = {
|
export type MediaGroupEntry = {
|
||||||
messages: Array<{
|
messages: Array<{
|
||||||
msg: TelegramMessage;
|
msg: Message;
|
||||||
ctx: TelegramContext;
|
ctx: TelegramContext;
|
||||||
}>;
|
}>;
|
||||||
timer: ReturnType<typeof setTimeout>;
|
timer: ReturnType<typeof setTimeout>;
|
||||||
|
|
@ -16,12 +17,12 @@ export type MediaGroupEntry = {
|
||||||
export type TelegramUpdateKeyContext = {
|
export type TelegramUpdateKeyContext = {
|
||||||
update?: {
|
update?: {
|
||||||
update_id?: number;
|
update_id?: number;
|
||||||
message?: TelegramMessage;
|
message?: Message;
|
||||||
edited_message?: TelegramMessage;
|
edited_message?: Message;
|
||||||
};
|
};
|
||||||
update_id?: number;
|
update_id?: number;
|
||||||
message?: TelegramMessage;
|
message?: Message;
|
||||||
callbackQuery?: { id?: string; message?: TelegramMessage };
|
callbackQuery?: { id?: string; message?: Message };
|
||||||
};
|
};
|
||||||
|
|
||||||
export const resolveTelegramUpdateId = (ctx: TelegramUpdateKeyContext) =>
|
export const resolveTelegramUpdateId = (ctx: TelegramUpdateKeyContext) =>
|
||||||
|
|
|
||||||
|
|
@ -2,11 +2,11 @@ import type { ApiClientOptions } from "grammy";
|
||||||
// @ts-nocheck
|
// @ts-nocheck
|
||||||
import { sequentialize } from "@grammyjs/runner";
|
import { sequentialize } from "@grammyjs/runner";
|
||||||
import { apiThrottler } from "@grammyjs/transformer-throttler";
|
import { apiThrottler } from "@grammyjs/transformer-throttler";
|
||||||
import { ReactionTypeEmoji } from "@grammyjs/types";
|
import { type Message, ReactionTypeEmoji } from "@grammyjs/types";
|
||||||
import { Bot, webhookCallback } from "grammy";
|
import { Bot, webhookCallback } from "grammy";
|
||||||
import type { OpenClawConfig, ReplyToMode } from "../config/config.js";
|
import type { OpenClawConfig, ReplyToMode } from "../config/config.js";
|
||||||
import type { RuntimeEnv } from "../runtime.js";
|
import type { RuntimeEnv } from "../runtime.js";
|
||||||
import type { TelegramContext, TelegramMessage } from "./bot/types.js";
|
import type { TelegramContext } from "./bot/types.js";
|
||||||
import { resolveDefaultAgentId } from "../agents/agent-scope.js";
|
import { resolveDefaultAgentId } from "../agents/agent-scope.js";
|
||||||
import { resolveTextChunkLimit } from "../auto-reply/chunk.js";
|
import { resolveTextChunkLimit } from "../auto-reply/chunk.js";
|
||||||
import { isControlCommandMessage } from "../auto-reply/command-detection.js";
|
import { isControlCommandMessage } from "../auto-reply/command-detection.js";
|
||||||
|
|
@ -67,11 +67,11 @@ export type TelegramBotOptions = {
|
||||||
|
|
||||||
export function getTelegramSequentialKey(ctx: {
|
export function getTelegramSequentialKey(ctx: {
|
||||||
chat?: { id?: number };
|
chat?: { id?: number };
|
||||||
message?: TelegramMessage;
|
message?: Message;
|
||||||
update?: {
|
update?: {
|
||||||
message?: TelegramMessage;
|
message?: Message;
|
||||||
edited_message?: TelegramMessage;
|
edited_message?: Message;
|
||||||
callback_query?: { message?: TelegramMessage };
|
callback_query?: { message?: Message };
|
||||||
message_reaction?: { chat?: { id?: number } };
|
message_reaction?: { chat?: { id?: number } };
|
||||||
};
|
};
|
||||||
}): string {
|
}): string {
|
||||||
|
|
|
||||||
|
|
@ -100,40 +100,6 @@ describe("normalizeForwardedContext", () => {
|
||||||
expect(ctx?.fromTitle).toBe("Hidden Name");
|
expect(ctx?.fromTitle).toBe("Hidden Name");
|
||||||
expect(ctx?.date).toBe(456);
|
expect(ctx?.date).toBe(456);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("handles legacy forwards with signatures", () => {
|
|
||||||
const ctx = normalizeForwardedContext({
|
|
||||||
forward_from_chat: {
|
|
||||||
title: "OpenClaw Updates",
|
|
||||||
username: "openclaw",
|
|
||||||
id: 99,
|
|
||||||
type: "channel",
|
|
||||||
},
|
|
||||||
forward_signature: "Stan",
|
|
||||||
forward_date: 789,
|
|
||||||
// oxlint-disable-next-line typescript/no-explicit-any
|
|
||||||
} as any);
|
|
||||||
expect(ctx).not.toBeNull();
|
|
||||||
expect(ctx?.from).toBe("OpenClaw Updates (Stan)");
|
|
||||||
expect(ctx?.fromType).toBe("legacy_channel");
|
|
||||||
expect(ctx?.fromId).toBe("99");
|
|
||||||
expect(ctx?.fromUsername).toBe("openclaw");
|
|
||||||
expect(ctx?.fromTitle).toBe("OpenClaw Updates");
|
|
||||||
expect(ctx?.fromSignature).toBe("Stan");
|
|
||||||
expect(ctx?.date).toBe(789);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("handles legacy hidden sender names", () => {
|
|
||||||
const ctx = normalizeForwardedContext({
|
|
||||||
forward_sender_name: "Legacy Hidden",
|
|
||||||
forward_date: 111,
|
|
||||||
// oxlint-disable-next-line typescript/no-explicit-any
|
|
||||||
} as any);
|
|
||||||
expect(ctx).not.toBeNull();
|
|
||||||
expect(ctx?.from).toBe("Legacy Hidden");
|
|
||||||
expect(ctx?.fromType).toBe("legacy_hidden_user");
|
|
||||||
expect(ctx?.date).toBe(111);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("expandTextLinks", () => {
|
describe("expandTextLinks", () => {
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,5 @@
|
||||||
import type {
|
import type { Chat, Message, MessageOrigin, User } from "@grammyjs/types";
|
||||||
TelegramForwardChat,
|
import type { TelegramStreamMode } from "./types.js";
|
||||||
TelegramForwardOrigin,
|
|
||||||
TelegramForwardUser,
|
|
||||||
TelegramForwardedMessage,
|
|
||||||
TelegramLocation,
|
|
||||||
TelegramMessage,
|
|
||||||
TelegramStreamMode,
|
|
||||||
TelegramVenue,
|
|
||||||
} from "./types.js";
|
|
||||||
import { formatLocationText, type NormalizedLocation } from "../../channels/location.js";
|
import { formatLocationText, type NormalizedLocation } from "../../channels/location.js";
|
||||||
|
|
||||||
const TELEGRAM_GENERAL_TOPIC_ID = 1;
|
const TELEGRAM_GENERAL_TOPIC_ID = 1;
|
||||||
|
|
@ -107,14 +99,14 @@ export function buildTelegramGroupFrom(chatId: number | string, messageThreadId?
|
||||||
return `telegram:group:${buildTelegramGroupPeerId(chatId, messageThreadId)}`;
|
return `telegram:group:${buildTelegramGroupPeerId(chatId, messageThreadId)}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function buildSenderName(msg: TelegramMessage) {
|
export function buildSenderName(msg: Message) {
|
||||||
const name =
|
const name =
|
||||||
[msg.from?.first_name, msg.from?.last_name].filter(Boolean).join(" ").trim() ||
|
[msg.from?.first_name, msg.from?.last_name].filter(Boolean).join(" ").trim() ||
|
||||||
msg.from?.username;
|
msg.from?.username;
|
||||||
return name || undefined;
|
return name || undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function buildSenderLabel(msg: TelegramMessage, senderId?: number | string) {
|
export function buildSenderLabel(msg: Message, senderId?: number | string) {
|
||||||
const name = buildSenderName(msg);
|
const name = buildSenderName(msg);
|
||||||
const username = msg.from?.username ? `@${msg.from.username}` : undefined;
|
const username = msg.from?.username ? `@${msg.from.username}` : undefined;
|
||||||
let label = name;
|
let label = name;
|
||||||
|
|
@ -136,11 +128,7 @@ export function buildSenderLabel(msg: TelegramMessage, senderId?: number | strin
|
||||||
return idPart ?? "id:unknown";
|
return idPart ?? "id:unknown";
|
||||||
}
|
}
|
||||||
|
|
||||||
export function buildGroupLabel(
|
export function buildGroupLabel(msg: Message, chatId: number | string, messageThreadId?: number) {
|
||||||
msg: TelegramMessage,
|
|
||||||
chatId: number | string,
|
|
||||||
messageThreadId?: number,
|
|
||||||
) {
|
|
||||||
const title = msg.chat?.title;
|
const title = msg.chat?.title;
|
||||||
const topicSuffix = messageThreadId != null ? ` topic:${messageThreadId}` : "";
|
const topicSuffix = messageThreadId != null ? ` topic:${messageThreadId}` : "";
|
||||||
if (title) {
|
if (title) {
|
||||||
|
|
@ -149,7 +137,7 @@ export function buildGroupLabel(
|
||||||
return `group:${chatId}${topicSuffix}`;
|
return `group:${chatId}${topicSuffix}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function hasBotMention(msg: TelegramMessage, botUsername: string) {
|
export function hasBotMention(msg: Message, botUsername: string) {
|
||||||
const text = (msg.text ?? msg.caption ?? "").toLowerCase();
|
const text = (msg.text ?? msg.caption ?? "").toLowerCase();
|
||||||
if (text.includes(`@${botUsername}`)) {
|
if (text.includes(`@${botUsername}`)) {
|
||||||
return true;
|
return true;
|
||||||
|
|
@ -218,7 +206,7 @@ export type TelegramReplyTarget = {
|
||||||
kind: "reply" | "quote";
|
kind: "reply" | "quote";
|
||||||
};
|
};
|
||||||
|
|
||||||
export function describeReplyTarget(msg: TelegramMessage): TelegramReplyTarget | null {
|
export function describeReplyTarget(msg: Message): TelegramReplyTarget | null {
|
||||||
const reply = msg.reply_to_message;
|
const reply = msg.reply_to_message;
|
||||||
const quote = msg.quote;
|
const quote = msg.quote;
|
||||||
let body = "";
|
let body = "";
|
||||||
|
|
@ -275,28 +263,27 @@ export type TelegramForwardedContext = {
|
||||||
fromSignature?: string;
|
fromSignature?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
function normalizeForwardedUserLabel(user: TelegramForwardUser) {
|
function normalizeForwardedUserLabel(user: User) {
|
||||||
const name = [user.first_name, user.last_name].filter(Boolean).join(" ").trim();
|
const name = [user.first_name, user.last_name].filter(Boolean).join(" ").trim();
|
||||||
const username = user.username?.trim() || undefined;
|
const username = user.username?.trim() || undefined;
|
||||||
const id = user.id != null ? String(user.id) : undefined;
|
const id = String(user.id);
|
||||||
const display =
|
const display =
|
||||||
(name && username
|
(name && username
|
||||||
? `${name} (@${username})`
|
? `${name} (@${username})`
|
||||||
: name || (username ? `@${username}` : undefined)) || (id ? `user:${id}` : undefined);
|
: name || (username ? `@${username}` : undefined)) || `user:${id}`;
|
||||||
return { display, name: name || undefined, username, id };
|
return { display, name: name || undefined, username, id };
|
||||||
}
|
}
|
||||||
|
|
||||||
function normalizeForwardedChatLabel(chat: TelegramForwardChat, fallbackKind: "chat" | "channel") {
|
function normalizeForwardedChatLabel(chat: Chat, fallbackKind: "chat" | "channel") {
|
||||||
const title = chat.title?.trim() || undefined;
|
const title = chat.title?.trim() || undefined;
|
||||||
const username = chat.username?.trim() || undefined;
|
const username = chat.username?.trim() || undefined;
|
||||||
const id = chat.id != null ? String(chat.id) : undefined;
|
const id = String(chat.id);
|
||||||
const display =
|
const display = title || (username ? `@${username}` : undefined) || `${fallbackKind}:${id}`;
|
||||||
title || (username ? `@${username}` : undefined) || (id ? `${fallbackKind}:${id}` : undefined);
|
|
||||||
return { display, title, username, id };
|
return { display, title, username, id };
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildForwardedContextFromUser(params: {
|
function buildForwardedContextFromUser(params: {
|
||||||
user: TelegramForwardUser;
|
user: User;
|
||||||
date?: number;
|
date?: number;
|
||||||
type: string;
|
type: string;
|
||||||
}): TelegramForwardedContext | null {
|
}): TelegramForwardedContext | null {
|
||||||
|
|
@ -332,13 +319,12 @@ function buildForwardedContextFromHiddenName(params: {
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildForwardedContextFromChat(params: {
|
function buildForwardedContextFromChat(params: {
|
||||||
chat: TelegramForwardChat;
|
chat: Chat;
|
||||||
date?: number;
|
date?: number;
|
||||||
type: string;
|
type: string;
|
||||||
signature?: string;
|
signature?: string;
|
||||||
}): TelegramForwardedContext | null {
|
}): TelegramForwardedContext | null {
|
||||||
const fallbackKind =
|
const fallbackKind = params.type === "channel" ? "channel" : "chat";
|
||||||
params.type === "channel" || params.type === "legacy_channel" ? "channel" : "chat";
|
|
||||||
const { display, title, username, id } = normalizeForwardedChatLabel(params.chat, fallbackKind);
|
const { display, title, username, id } = normalizeForwardedChatLabel(params.chat, fallbackKind);
|
||||||
if (!display) {
|
if (!display) {
|
||||||
return null;
|
return null;
|
||||||
|
|
@ -356,101 +342,52 @@ function buildForwardedContextFromChat(params: {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function resolveForwardOrigin(
|
function resolveForwardOrigin(origin: MessageOrigin): TelegramForwardedContext | null {
|
||||||
origin: TelegramForwardOrigin,
|
switch (origin.type) {
|
||||||
signature?: string,
|
case "user":
|
||||||
): TelegramForwardedContext | null {
|
return buildForwardedContextFromUser({
|
||||||
if (origin.type === "user" && origin.sender_user) {
|
user: origin.sender_user,
|
||||||
return buildForwardedContextFromUser({
|
date: origin.date,
|
||||||
user: origin.sender_user,
|
type: "user",
|
||||||
date: origin.date,
|
});
|
||||||
type: "user",
|
case "hidden_user":
|
||||||
});
|
return buildForwardedContextFromHiddenName({
|
||||||
|
name: origin.sender_user_name,
|
||||||
|
date: origin.date,
|
||||||
|
type: "hidden_user",
|
||||||
|
});
|
||||||
|
case "chat":
|
||||||
|
return buildForwardedContextFromChat({
|
||||||
|
chat: origin.sender_chat,
|
||||||
|
date: origin.date,
|
||||||
|
type: "chat",
|
||||||
|
signature: origin.author_signature,
|
||||||
|
});
|
||||||
|
case "channel":
|
||||||
|
return buildForwardedContextFromChat({
|
||||||
|
chat: origin.chat,
|
||||||
|
date: origin.date,
|
||||||
|
type: "channel",
|
||||||
|
signature: origin.author_signature,
|
||||||
|
});
|
||||||
|
default:
|
||||||
|
// Exhaustiveness guard: if Grammy adds a new MessageOrigin variant,
|
||||||
|
// TypeScript will flag this assignment as an error.
|
||||||
|
origin satisfies never;
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
if (origin.type === "hidden_user") {
|
|
||||||
return buildForwardedContextFromHiddenName({
|
|
||||||
name: origin.sender_user_name,
|
|
||||||
date: origin.date,
|
|
||||||
type: "hidden_user",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if (origin.type === "chat" && origin.sender_chat) {
|
|
||||||
return buildForwardedContextFromChat({
|
|
||||||
chat: origin.sender_chat,
|
|
||||||
date: origin.date,
|
|
||||||
type: "chat",
|
|
||||||
signature,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if (origin.type === "channel" && origin.chat) {
|
|
||||||
return buildForwardedContextFromChat({
|
|
||||||
chat: origin.chat,
|
|
||||||
date: origin.date,
|
|
||||||
type: "channel",
|
|
||||||
signature,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** Extract forwarded message origin info from Telegram message. */
|
||||||
* Extract forwarded message origin info from Telegram message.
|
export function normalizeForwardedContext(msg: Message): TelegramForwardedContext | null {
|
||||||
* Supports both new forward_origin API and legacy forward_from/forward_from_chat fields.
|
if (!msg.forward_origin) {
|
||||||
*/
|
return null;
|
||||||
export function normalizeForwardedContext(msg: TelegramMessage): TelegramForwardedContext | null {
|
|
||||||
const forwardMsg = msg as TelegramForwardedMessage;
|
|
||||||
const signature = forwardMsg.forward_signature?.trim() || undefined;
|
|
||||||
|
|
||||||
if (forwardMsg.forward_origin) {
|
|
||||||
const originContext = resolveForwardOrigin(forwardMsg.forward_origin, signature);
|
|
||||||
if (originContext) {
|
|
||||||
return originContext;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
return resolveForwardOrigin(msg.forward_origin);
|
||||||
if (forwardMsg.forward_from_chat) {
|
|
||||||
const legacyType =
|
|
||||||
forwardMsg.forward_from_chat.type === "channel" ? "legacy_channel" : "legacy_chat";
|
|
||||||
const legacyContext = buildForwardedContextFromChat({
|
|
||||||
chat: forwardMsg.forward_from_chat,
|
|
||||||
date: forwardMsg.forward_date,
|
|
||||||
type: legacyType,
|
|
||||||
signature,
|
|
||||||
});
|
|
||||||
if (legacyContext) {
|
|
||||||
return legacyContext;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (forwardMsg.forward_from) {
|
|
||||||
const legacyContext = buildForwardedContextFromUser({
|
|
||||||
user: forwardMsg.forward_from,
|
|
||||||
date: forwardMsg.forward_date,
|
|
||||||
type: "legacy_user",
|
|
||||||
});
|
|
||||||
if (legacyContext) {
|
|
||||||
return legacyContext;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const hiddenContext = buildForwardedContextFromHiddenName({
|
|
||||||
name: forwardMsg.forward_sender_name,
|
|
||||||
date: forwardMsg.forward_date,
|
|
||||||
type: "legacy_hidden_user",
|
|
||||||
});
|
|
||||||
if (hiddenContext) {
|
|
||||||
return hiddenContext;
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function extractTelegramLocation(msg: TelegramMessage): NormalizedLocation | null {
|
export function extractTelegramLocation(msg: Message): NormalizedLocation | null {
|
||||||
const msgWithLocation = msg as {
|
const { venue, location } = msg;
|
||||||
location?: TelegramLocation;
|
|
||||||
venue?: TelegramVenue;
|
|
||||||
};
|
|
||||||
const { venue, location } = msgWithLocation;
|
|
||||||
|
|
||||||
if (venue) {
|
if (venue) {
|
||||||
return {
|
return {
|
||||||
|
|
|
||||||
|
|
@ -1,80 +1,20 @@
|
||||||
import type { Message } from "@grammyjs/types";
|
import type { Message } from "@grammyjs/types";
|
||||||
|
|
||||||
export type TelegramQuote = {
|
/** App-specific stream mode for Telegram draft streaming. */
|
||||||
text?: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type TelegramMessage = Message & {
|
|
||||||
quote?: TelegramQuote;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type TelegramStreamMode = "off" | "partial" | "block";
|
export type TelegramStreamMode = "off" | "partial" | "block";
|
||||||
|
|
||||||
export type TelegramForwardOriginType = "user" | "hidden_user" | "chat" | "channel";
|
/**
|
||||||
|
* Minimal context projection from Grammy's Context class.
|
||||||
export type TelegramForwardUser = {
|
* Decouples the message processing pipeline from Grammy's full Context,
|
||||||
first_name?: string;
|
* and allows constructing synthetic contexts for debounced/combined messages.
|
||||||
last_name?: string;
|
*/
|
||||||
username?: string;
|
|
||||||
id?: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type TelegramForwardChat = {
|
|
||||||
title?: string;
|
|
||||||
id?: number;
|
|
||||||
username?: string;
|
|
||||||
type?: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type TelegramForwardOrigin = {
|
|
||||||
type: TelegramForwardOriginType;
|
|
||||||
sender_user?: TelegramForwardUser;
|
|
||||||
sender_user_name?: string;
|
|
||||||
sender_chat?: TelegramForwardChat;
|
|
||||||
chat?: TelegramForwardChat;
|
|
||||||
date?: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type TelegramForwardMetadata = {
|
|
||||||
forward_origin?: TelegramForwardOrigin;
|
|
||||||
forward_from?: TelegramForwardUser;
|
|
||||||
forward_from_chat?: TelegramForwardChat;
|
|
||||||
forward_sender_name?: string;
|
|
||||||
forward_signature?: string;
|
|
||||||
forward_date?: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type TelegramForwardedMessage = TelegramMessage & TelegramForwardMetadata;
|
|
||||||
|
|
||||||
export type TelegramContext = {
|
export type TelegramContext = {
|
||||||
message: TelegramMessage;
|
message: Message;
|
||||||
me?: { id?: number; username?: string };
|
me?: { id?: number; username?: string };
|
||||||
getFile: () => Promise<{
|
getFile: () => Promise<{ file_path?: string }>;
|
||||||
file_path?: string;
|
|
||||||
}>;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/** Telegram Location object */
|
/** Telegram sticker metadata for context enrichment and caching. */
|
||||||
export interface TelegramLocation {
|
|
||||||
latitude: number;
|
|
||||||
longitude: number;
|
|
||||||
horizontal_accuracy?: number;
|
|
||||||
live_period?: number;
|
|
||||||
heading?: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Telegram Venue object */
|
|
||||||
export interface TelegramVenue {
|
|
||||||
location: TelegramLocation;
|
|
||||||
title: string;
|
|
||||||
address: string;
|
|
||||||
foursquare_id?: string;
|
|
||||||
foursquare_type?: string;
|
|
||||||
google_place_id?: string;
|
|
||||||
google_place_type?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Telegram sticker metadata for context enrichment. */
|
|
||||||
export interface StickerMetadata {
|
export interface StickerMetadata {
|
||||||
/** Emoji associated with the sticker. */
|
/** Emoji associated with the sticker. */
|
||||||
emoji?: string;
|
emoji?: string;
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,14 @@
|
||||||
// @ts-nocheck
|
|
||||||
import { ProxyAgent, fetch as undiciFetch } from "undici";
|
import { ProxyAgent, fetch as undiciFetch } from "undici";
|
||||||
import { wrapFetchWithAbortSignal } from "../infra/fetch.js";
|
import { wrapFetchWithAbortSignal } from "../infra/fetch.js";
|
||||||
|
|
||||||
export function makeProxyFetch(proxyUrl: string): typeof fetch {
|
export function makeProxyFetch(proxyUrl: string): typeof fetch {
|
||||||
const agent = new ProxyAgent(proxyUrl);
|
const agent = new ProxyAgent(proxyUrl);
|
||||||
return wrapFetchWithAbortSignal((input: RequestInfo | URL, init?: RequestInit) => {
|
// undici's fetch is runtime-compatible with global fetch but the types diverge
|
||||||
const base = init ? { ...init } : {};
|
// on stream/body internals. Single cast at the boundary keeps the rest type-safe.
|
||||||
return undiciFetch(input, { ...base, dispatcher: agent });
|
const fetcher = (input: RequestInfo | URL, init?: RequestInit) =>
|
||||||
});
|
undiciFetch(input as string | URL, {
|
||||||
|
...(init as Record<string, unknown>),
|
||||||
|
dispatcher: agent,
|
||||||
|
}) as unknown as Promise<Response>;
|
||||||
|
return wrapFetchWithAbortSignal(fetcher);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue