mirror of
https://github.com/openclaw/openclaw.git
synced 2026-02-09 05:19:32 +08:00
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 switch
This commit is contained in:
@@ -25,6 +25,7 @@ import {
|
||||
type ChannelPlugin,
|
||||
type OpenClawConfig,
|
||||
type ResolvedTelegramAccount,
|
||||
type TelegramProbe,
|
||||
} from "openclaw/plugin-sdk";
|
||||
import { getTelegramRuntime } from "./runtime.js";
|
||||
|
||||
@@ -60,7 +61,7 @@ function parseThreadId(threadId?: string | number | null) {
|
||||
const parsed = Number.parseInt(trimmed, 10);
|
||||
return Number.isFinite(parsed) ? parsed : undefined;
|
||||
}
|
||||
export const telegramPlugin: ChannelPlugin<ResolvedTelegramAccount> = {
|
||||
export const telegramPlugin: ChannelPlugin<ResolvedTelegramAccount, TelegramProbe> = {
|
||||
id: "telegram",
|
||||
meta: {
|
||||
...meta,
|
||||
@@ -327,11 +328,7 @@ export const telegramPlugin: ChannelPlugin<ResolvedTelegramAccount> = {
|
||||
if (!groupIds.length && unresolvedGroups === 0 && !hasWildcardUnmentionedGroups) {
|
||||
return undefined;
|
||||
}
|
||||
const botId =
|
||||
(probe as { ok?: boolean; bot?: { id?: number } })?.ok &&
|
||||
(probe as { bot?: { id?: number } }).bot?.id != null
|
||||
? (probe as { bot: { id: number } }).bot.id
|
||||
: null;
|
||||
const botId = probe?.ok && probe.bot?.id != null ? probe.bot.id : null;
|
||||
if (!botId) {
|
||||
return {
|
||||
ok: unresolvedGroups === 0 && !hasWildcardUnmentionedGroups,
|
||||
@@ -357,15 +354,9 @@ export const telegramPlugin: ChannelPlugin<ResolvedTelegramAccount> = {
|
||||
cfg.channels?.telegram?.accounts?.[account.accountId]?.groups ??
|
||||
cfg.channels?.telegram?.groups;
|
||||
const allowUnmentionedGroups =
|
||||
Boolean(
|
||||
groups?.["*"] && (groups["*"] as { requireMention?: boolean }).requireMention === false,
|
||||
) ||
|
||||
groups?.["*"]?.requireMention === false ||
|
||||
Object.entries(groups ?? {}).some(
|
||||
([key, value]) =>
|
||||
key !== "*" &&
|
||||
Boolean(value) &&
|
||||
typeof value === "object" &&
|
||||
(value as { requireMention?: boolean }).requireMention === false,
|
||||
([key, value]) => key !== "*" && value?.requireMention === false,
|
||||
);
|
||||
return {
|
||||
accountId: account.accountId,
|
||||
|
||||
@@ -105,7 +105,7 @@ export type ChannelOutboundAdapter = {
|
||||
sendPoll?: (ctx: ChannelPollContext) => Promise<ChannelPollResult>;
|
||||
};
|
||||
|
||||
export type ChannelStatusAdapter<ResolvedAccount> = {
|
||||
export type ChannelStatusAdapter<ResolvedAccount, Probe = unknown, Audit = unknown> = {
|
||||
defaultRuntime?: ChannelAccountSnapshot;
|
||||
buildChannelSummary?: (params: {
|
||||
account: ResolvedAccount;
|
||||
@@ -117,19 +117,19 @@ export type ChannelStatusAdapter<ResolvedAccount> = {
|
||||
account: ResolvedAccount;
|
||||
timeoutMs: number;
|
||||
cfg: OpenClawConfig;
|
||||
}) => Promise<unknown>;
|
||||
}) => Promise<Probe>;
|
||||
auditAccount?: (params: {
|
||||
account: ResolvedAccount;
|
||||
timeoutMs: number;
|
||||
cfg: OpenClawConfig;
|
||||
probe?: unknown;
|
||||
}) => Promise<unknown>;
|
||||
probe?: Probe;
|
||||
}) => Promise<Audit>;
|
||||
buildAccountSnapshot?: (params: {
|
||||
account: ResolvedAccount;
|
||||
cfg: OpenClawConfig;
|
||||
runtime?: ChannelAccountSnapshot;
|
||||
probe?: unknown;
|
||||
audit?: unknown;
|
||||
probe?: Probe;
|
||||
audit?: Audit;
|
||||
}) => ChannelAccountSnapshot | Promise<ChannelAccountSnapshot>;
|
||||
logSelfId?: (params: {
|
||||
account: ResolvedAccount;
|
||||
|
||||
@@ -45,7 +45,7 @@ export type ChannelConfigSchema = {
|
||||
};
|
||||
|
||||
// oxlint-disable-next-line typescript/no-explicit-any
|
||||
export type ChannelPlugin<ResolvedAccount = any> = {
|
||||
export type ChannelPlugin<ResolvedAccount = any, Probe = unknown, Audit = unknown> = {
|
||||
id: ChannelId;
|
||||
meta: ChannelMeta;
|
||||
capabilities: ChannelCapabilities;
|
||||
@@ -65,7 +65,7 @@ export type ChannelPlugin<ResolvedAccount = any> = {
|
||||
groups?: ChannelGroupAdapter;
|
||||
mentions?: ChannelMentionAdapter;
|
||||
outbound?: ChannelOutboundAdapter;
|
||||
status?: ChannelStatusAdapter<ResolvedAccount>;
|
||||
status?: ChannelStatusAdapter<ResolvedAccount, Probe, Audit>;
|
||||
gatewayMethods?: string[];
|
||||
gateway?: ChannelGatewayAdapter<ResolvedAccount>;
|
||||
auth?: ChannelAuthAdapter;
|
||||
|
||||
@@ -306,6 +306,7 @@ export {
|
||||
normalizeTelegramMessagingTarget,
|
||||
} from "../channels/plugins/normalize/telegram.js";
|
||||
export { collectTelegramStatusIssues } from "../channels/plugins/status-issues/telegram.js";
|
||||
export { type TelegramProbe } from "../telegram/probe.js";
|
||||
|
||||
// Channel: Signal
|
||||
export {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { TelegramMessage } from "./bot/types.js";
|
||||
import { resolveDefaultAgentId } from "../agents/agent-scope.js";
|
||||
// @ts-nocheck
|
||||
import type { Message } from "@grammyjs/types";
|
||||
import { resolveDefaultAgentId } from "../agents/agent-scope.js";
|
||||
import { hasControlCommand } from "../auto-reply/command-detection.js";
|
||||
import {
|
||||
createInboundDebouncer,
|
||||
@@ -63,7 +63,7 @@ export const registerTelegramHandlers = ({
|
||||
|
||||
type TextFragmentEntry = {
|
||||
key: string;
|
||||
messages: Array<{ msg: TelegramMessage; ctx: unknown; receivedAtMs: number }>;
|
||||
messages: Array<{ msg: Message; ctx: unknown; receivedAtMs: number }>;
|
||||
timer: ReturnType<typeof setTimeout>;
|
||||
};
|
||||
const textFragmentBuffer = new Map<string, TextFragmentEntry>();
|
||||
@@ -72,7 +72,7 @@ export const registerTelegramHandlers = ({
|
||||
const debounceMs = resolveInboundDebounceMs({ cfg, channel: "telegram" });
|
||||
type TelegramDebounceEntry = {
|
||||
ctx: unknown;
|
||||
msg: TelegramMessage;
|
||||
msg: Message;
|
||||
allMedia: Array<{ path: string; contentType?: string }>;
|
||||
storeAllowFrom: string[];
|
||||
debounceKey: string | null;
|
||||
@@ -111,7 +111,7 @@ export const registerTelegramHandlers = ({
|
||||
const baseCtx = first.ctx as { me?: unknown; getFile?: unknown } & Record<string, unknown>;
|
||||
const getFile =
|
||||
typeof baseCtx.getFile === "function" ? baseCtx.getFile.bind(baseCtx) : async () => ({});
|
||||
const syntheticMessage: TelegramMessage = {
|
||||
const syntheticMessage: Message = {
|
||||
...first.msg,
|
||||
text: combinedText,
|
||||
caption: undefined,
|
||||
@@ -231,7 +231,7 @@ export const registerTelegramHandlers = ({
|
||||
return;
|
||||
}
|
||||
|
||||
const syntheticMessage: TelegramMessage = {
|
||||
const syntheticMessage: Message = {
|
||||
...first.msg,
|
||||
text: combinedText,
|
||||
caption: undefined,
|
||||
@@ -557,7 +557,7 @@ export const registerTelegramHandlers = ({
|
||||
if (modelCallback.type === "select") {
|
||||
const { provider, model } = modelCallback;
|
||||
// Process model selection as a synthetic message with /model command
|
||||
const syntheticMessage: TelegramMessage = {
|
||||
const syntheticMessage: Message = {
|
||||
...callbackMessage,
|
||||
from: callback.from,
|
||||
text: `/model ${provider}/${model}`,
|
||||
@@ -582,7 +582,7 @@ export const registerTelegramHandlers = ({
|
||||
return;
|
||||
}
|
||||
|
||||
const syntheticMessage: TelegramMessage = {
|
||||
const syntheticMessage: Message = {
|
||||
...callbackMessage,
|
||||
from: callback.from,
|
||||
text: data,
|
||||
|
||||
@@ -9,6 +9,7 @@ import type {
|
||||
TelegramTopicConfig,
|
||||
} from "../config/types.js";
|
||||
import type { RuntimeEnv } from "../runtime.js";
|
||||
import type { TelegramContext } from "./bot/types.js";
|
||||
import { resolveEffectiveMessagesConfig } from "../agents/identity.js";
|
||||
import { resolveChunkMode } from "../auto-reply/chunk.js";
|
||||
import {
|
||||
@@ -86,7 +87,7 @@ export type RegisterTelegramHandlerParams = {
|
||||
) => { groupConfig?: TelegramGroupConfig; topicConfig?: TelegramTopicConfig };
|
||||
shouldSkipUpdate: (ctx: TelegramUpdateKeyContext) => boolean;
|
||||
processMessage: (
|
||||
ctx: unknown,
|
||||
ctx: TelegramContext,
|
||||
allMedia: Array<{ path: string; contentType?: string }>,
|
||||
storeAllowFrom: string[],
|
||||
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";
|
||||
|
||||
const MEDIA_GROUP_TIMEOUT_MS = 500;
|
||||
@@ -7,7 +8,7 @@ const RECENT_TELEGRAM_UPDATE_MAX = 2000;
|
||||
|
||||
export type MediaGroupEntry = {
|
||||
messages: Array<{
|
||||
msg: TelegramMessage;
|
||||
msg: Message;
|
||||
ctx: TelegramContext;
|
||||
}>;
|
||||
timer: ReturnType<typeof setTimeout>;
|
||||
@@ -16,12 +17,12 @@ export type MediaGroupEntry = {
|
||||
export type TelegramUpdateKeyContext = {
|
||||
update?: {
|
||||
update_id?: number;
|
||||
message?: TelegramMessage;
|
||||
edited_message?: TelegramMessage;
|
||||
message?: Message;
|
||||
edited_message?: Message;
|
||||
};
|
||||
update_id?: number;
|
||||
message?: TelegramMessage;
|
||||
callbackQuery?: { id?: string; message?: TelegramMessage };
|
||||
message?: Message;
|
||||
callbackQuery?: { id?: string; message?: Message };
|
||||
};
|
||||
|
||||
export const resolveTelegramUpdateId = (ctx: TelegramUpdateKeyContext) =>
|
||||
|
||||
@@ -2,11 +2,11 @@ import type { ApiClientOptions } from "grammy";
|
||||
// @ts-nocheck
|
||||
import { sequentialize } from "@grammyjs/runner";
|
||||
import { apiThrottler } from "@grammyjs/transformer-throttler";
|
||||
import { ReactionTypeEmoji } from "@grammyjs/types";
|
||||
import { type Message, ReactionTypeEmoji } from "@grammyjs/types";
|
||||
import { Bot, webhookCallback } from "grammy";
|
||||
import type { OpenClawConfig, ReplyToMode } from "../config/config.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 { resolveTextChunkLimit } from "../auto-reply/chunk.js";
|
||||
import { isControlCommandMessage } from "../auto-reply/command-detection.js";
|
||||
@@ -67,11 +67,11 @@ export type TelegramBotOptions = {
|
||||
|
||||
export function getTelegramSequentialKey(ctx: {
|
||||
chat?: { id?: number };
|
||||
message?: TelegramMessage;
|
||||
message?: Message;
|
||||
update?: {
|
||||
message?: TelegramMessage;
|
||||
edited_message?: TelegramMessage;
|
||||
callback_query?: { message?: TelegramMessage };
|
||||
message?: Message;
|
||||
edited_message?: Message;
|
||||
callback_query?: { message?: Message };
|
||||
message_reaction?: { chat?: { id?: number } };
|
||||
};
|
||||
}): string {
|
||||
|
||||
@@ -100,40 +100,6 @@ describe("normalizeForwardedContext", () => {
|
||||
expect(ctx?.fromTitle).toBe("Hidden Name");
|
||||
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", () => {
|
||||
|
||||
@@ -1,13 +1,5 @@
|
||||
import type {
|
||||
TelegramForwardChat,
|
||||
TelegramForwardOrigin,
|
||||
TelegramForwardUser,
|
||||
TelegramForwardedMessage,
|
||||
TelegramLocation,
|
||||
TelegramMessage,
|
||||
TelegramStreamMode,
|
||||
TelegramVenue,
|
||||
} from "./types.js";
|
||||
import type { Chat, Message, MessageOrigin, User } from "@grammyjs/types";
|
||||
import type { TelegramStreamMode } from "./types.js";
|
||||
import { formatLocationText, type NormalizedLocation } from "../../channels/location.js";
|
||||
|
||||
const TELEGRAM_GENERAL_TOPIC_ID = 1;
|
||||
@@ -107,14 +99,14 @@ export function buildTelegramGroupFrom(chatId: number | string, messageThreadId?
|
||||
return `telegram:group:${buildTelegramGroupPeerId(chatId, messageThreadId)}`;
|
||||
}
|
||||
|
||||
export function buildSenderName(msg: TelegramMessage) {
|
||||
export function buildSenderName(msg: Message) {
|
||||
const name =
|
||||
[msg.from?.first_name, msg.from?.last_name].filter(Boolean).join(" ").trim() ||
|
||||
msg.from?.username;
|
||||
return name || undefined;
|
||||
}
|
||||
|
||||
export function buildSenderLabel(msg: TelegramMessage, senderId?: number | string) {
|
||||
export function buildSenderLabel(msg: Message, senderId?: number | string) {
|
||||
const name = buildSenderName(msg);
|
||||
const username = msg.from?.username ? `@${msg.from.username}` : undefined;
|
||||
let label = name;
|
||||
@@ -136,11 +128,7 @@ export function buildSenderLabel(msg: TelegramMessage, senderId?: number | strin
|
||||
return idPart ?? "id:unknown";
|
||||
}
|
||||
|
||||
export function buildGroupLabel(
|
||||
msg: TelegramMessage,
|
||||
chatId: number | string,
|
||||
messageThreadId?: number,
|
||||
) {
|
||||
export function buildGroupLabel(msg: Message, chatId: number | string, messageThreadId?: number) {
|
||||
const title = msg.chat?.title;
|
||||
const topicSuffix = messageThreadId != null ? ` topic:${messageThreadId}` : "";
|
||||
if (title) {
|
||||
@@ -149,7 +137,7 @@ export function buildGroupLabel(
|
||||
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();
|
||||
if (text.includes(`@${botUsername}`)) {
|
||||
return true;
|
||||
@@ -218,7 +206,7 @@ export type TelegramReplyTarget = {
|
||||
kind: "reply" | "quote";
|
||||
};
|
||||
|
||||
export function describeReplyTarget(msg: TelegramMessage): TelegramReplyTarget | null {
|
||||
export function describeReplyTarget(msg: Message): TelegramReplyTarget | null {
|
||||
const reply = msg.reply_to_message;
|
||||
const quote = msg.quote;
|
||||
let body = "";
|
||||
@@ -275,28 +263,27 @@ export type TelegramForwardedContext = {
|
||||
fromSignature?: string;
|
||||
};
|
||||
|
||||
function normalizeForwardedUserLabel(user: TelegramForwardUser) {
|
||||
function normalizeForwardedUserLabel(user: User) {
|
||||
const name = [user.first_name, user.last_name].filter(Boolean).join(" ").trim();
|
||||
const username = user.username?.trim() || undefined;
|
||||
const id = user.id != null ? String(user.id) : undefined;
|
||||
const id = String(user.id);
|
||||
const display =
|
||||
(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 };
|
||||
}
|
||||
|
||||
function normalizeForwardedChatLabel(chat: TelegramForwardChat, fallbackKind: "chat" | "channel") {
|
||||
function normalizeForwardedChatLabel(chat: Chat, fallbackKind: "chat" | "channel") {
|
||||
const title = chat.title?.trim() || undefined;
|
||||
const username = chat.username?.trim() || undefined;
|
||||
const id = chat.id != null ? String(chat.id) : undefined;
|
||||
const display =
|
||||
title || (username ? `@${username}` : undefined) || (id ? `${fallbackKind}:${id}` : undefined);
|
||||
const id = String(chat.id);
|
||||
const display = title || (username ? `@${username}` : undefined) || `${fallbackKind}:${id}`;
|
||||
return { display, title, username, id };
|
||||
}
|
||||
|
||||
function buildForwardedContextFromUser(params: {
|
||||
user: TelegramForwardUser;
|
||||
user: User;
|
||||
date?: number;
|
||||
type: string;
|
||||
}): TelegramForwardedContext | null {
|
||||
@@ -332,13 +319,12 @@ function buildForwardedContextFromHiddenName(params: {
|
||||
}
|
||||
|
||||
function buildForwardedContextFromChat(params: {
|
||||
chat: TelegramForwardChat;
|
||||
chat: Chat;
|
||||
date?: number;
|
||||
type: string;
|
||||
signature?: string;
|
||||
}): TelegramForwardedContext | null {
|
||||
const fallbackKind =
|
||||
params.type === "channel" || params.type === "legacy_channel" ? "channel" : "chat";
|
||||
const fallbackKind = params.type === "channel" ? "channel" : "chat";
|
||||
const { display, title, username, id } = normalizeForwardedChatLabel(params.chat, fallbackKind);
|
||||
if (!display) {
|
||||
return null;
|
||||
@@ -356,101 +342,52 @@ function buildForwardedContextFromChat(params: {
|
||||
};
|
||||
}
|
||||
|
||||
function resolveForwardOrigin(
|
||||
origin: TelegramForwardOrigin,
|
||||
signature?: string,
|
||||
): TelegramForwardedContext | null {
|
||||
if (origin.type === "user" && origin.sender_user) {
|
||||
return buildForwardedContextFromUser({
|
||||
user: origin.sender_user,
|
||||
date: origin.date,
|
||||
type: "user",
|
||||
});
|
||||
function resolveForwardOrigin(origin: MessageOrigin): TelegramForwardedContext | null {
|
||||
switch (origin.type) {
|
||||
case "user":
|
||||
return buildForwardedContextFromUser({
|
||||
user: origin.sender_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.
|
||||
* Supports both new forward_origin API and legacy forward_from/forward_from_chat fields.
|
||||
*/
|
||||
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;
|
||||
}
|
||||
/** Extract forwarded message origin info from Telegram message. */
|
||||
export function normalizeForwardedContext(msg: Message): TelegramForwardedContext | null {
|
||||
if (!msg.forward_origin) {
|
||||
return null;
|
||||
}
|
||||
|
||||
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;
|
||||
return resolveForwardOrigin(msg.forward_origin);
|
||||
}
|
||||
|
||||
export function extractTelegramLocation(msg: TelegramMessage): NormalizedLocation | null {
|
||||
const msgWithLocation = msg as {
|
||||
location?: TelegramLocation;
|
||||
venue?: TelegramVenue;
|
||||
};
|
||||
const { venue, location } = msgWithLocation;
|
||||
export function extractTelegramLocation(msg: Message): NormalizedLocation | null {
|
||||
const { venue, location } = msg;
|
||||
|
||||
if (venue) {
|
||||
return {
|
||||
|
||||
@@ -1,80 +1,20 @@
|
||||
import type { Message } from "@grammyjs/types";
|
||||
|
||||
export type TelegramQuote = {
|
||||
text?: string;
|
||||
};
|
||||
|
||||
export type TelegramMessage = Message & {
|
||||
quote?: TelegramQuote;
|
||||
};
|
||||
|
||||
/** App-specific stream mode for Telegram draft streaming. */
|
||||
export type TelegramStreamMode = "off" | "partial" | "block";
|
||||
|
||||
export type TelegramForwardOriginType = "user" | "hidden_user" | "chat" | "channel";
|
||||
|
||||
export type TelegramForwardUser = {
|
||||
first_name?: string;
|
||||
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;
|
||||
|
||||
/**
|
||||
* Minimal context projection from Grammy's Context class.
|
||||
* Decouples the message processing pipeline from Grammy's full Context,
|
||||
* and allows constructing synthetic contexts for debounced/combined messages.
|
||||
*/
|
||||
export type TelegramContext = {
|
||||
message: TelegramMessage;
|
||||
message: Message;
|
||||
me?: { id?: number; username?: string };
|
||||
getFile: () => Promise<{
|
||||
file_path?: string;
|
||||
}>;
|
||||
getFile: () => Promise<{ file_path?: string }>;
|
||||
};
|
||||
|
||||
/** Telegram Location object */
|
||||
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. */
|
||||
/** Telegram sticker metadata for context enrichment and caching. */
|
||||
export interface StickerMetadata {
|
||||
/** Emoji associated with the sticker. */
|
||||
emoji?: string;
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
// @ts-nocheck
|
||||
import { ProxyAgent, fetch as undiciFetch } from "undici";
|
||||
import { wrapFetchWithAbortSignal } from "../infra/fetch.js";
|
||||
|
||||
export function makeProxyFetch(proxyUrl: string): typeof fetch {
|
||||
const agent = new ProxyAgent(proxyUrl);
|
||||
return wrapFetchWithAbortSignal((input: RequestInfo | URL, init?: RequestInit) => {
|
||||
const base = init ? { ...init } : {};
|
||||
return undiciFetch(input, { ...base, dispatcher: agent });
|
||||
});
|
||||
// undici's fetch is runtime-compatible with global fetch but the types diverge
|
||||
// on stream/body internals. Single cast at the boundary keeps the rest type-safe.
|
||||
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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user