mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-08 07:51:26 +00:00
chore: Enable "curly" rule to avoid single-statement if confusion/errors.
This commit is contained in:
@@ -22,10 +22,14 @@ export type ResolvedTelegramAccount = {
|
||||
|
||||
function listConfiguredAccountIds(cfg: OpenClawConfig): string[] {
|
||||
const accounts = cfg.channels?.telegram?.accounts;
|
||||
if (!accounts || typeof accounts !== "object") return [];
|
||||
if (!accounts || typeof accounts !== "object") {
|
||||
return [];
|
||||
}
|
||||
const ids = new Set<string>();
|
||||
for (const key of Object.keys(accounts)) {
|
||||
if (!key) continue;
|
||||
if (!key) {
|
||||
continue;
|
||||
}
|
||||
ids.add(normalizeAccountId(key));
|
||||
}
|
||||
return [...ids];
|
||||
@@ -36,15 +40,21 @@ export function listTelegramAccountIds(cfg: OpenClawConfig): string[] {
|
||||
new Set([...listConfiguredAccountIds(cfg), ...listBoundAccountIds(cfg, "telegram")]),
|
||||
);
|
||||
debugAccounts("listTelegramAccountIds", ids);
|
||||
if (ids.length === 0) return [DEFAULT_ACCOUNT_ID];
|
||||
if (ids.length === 0) {
|
||||
return [DEFAULT_ACCOUNT_ID];
|
||||
}
|
||||
return ids.toSorted((a, b) => a.localeCompare(b));
|
||||
}
|
||||
|
||||
export function resolveDefaultTelegramAccountId(cfg: OpenClawConfig): string {
|
||||
const boundDefault = resolveDefaultAgentBoundAccountId(cfg, "telegram");
|
||||
if (boundDefault) return boundDefault;
|
||||
if (boundDefault) {
|
||||
return boundDefault;
|
||||
}
|
||||
const ids = listTelegramAccountIds(cfg);
|
||||
if (ids.includes(DEFAULT_ACCOUNT_ID)) return DEFAULT_ACCOUNT_ID;
|
||||
if (ids.includes(DEFAULT_ACCOUNT_ID)) {
|
||||
return DEFAULT_ACCOUNT_ID;
|
||||
}
|
||||
return ids[0] ?? DEFAULT_ACCOUNT_ID;
|
||||
}
|
||||
|
||||
@@ -53,9 +63,13 @@ function resolveAccountConfig(
|
||||
accountId: string,
|
||||
): TelegramAccountConfig | undefined {
|
||||
const accounts = cfg.channels?.telegram?.accounts;
|
||||
if (!accounts || typeof accounts !== "object") return undefined;
|
||||
if (!accounts || typeof accounts !== "object") {
|
||||
return undefined;
|
||||
}
|
||||
const direct = accounts[accountId] as TelegramAccountConfig | undefined;
|
||||
if (direct) return direct;
|
||||
if (direct) {
|
||||
return direct;
|
||||
}
|
||||
const normalized = normalizeAccountId(accountId);
|
||||
const matchKey = Object.keys(accounts).find((key) => normalizeAccountId(key) === normalized);
|
||||
return matchKey ? (accounts[matchKey] as TelegramAccountConfig | undefined) : undefined;
|
||||
@@ -97,16 +111,24 @@ export function resolveTelegramAccount(params: {
|
||||
|
||||
const normalized = normalizeAccountId(params.accountId);
|
||||
const primary = resolve(normalized);
|
||||
if (hasExplicitAccountId) return primary;
|
||||
if (primary.tokenSource !== "none") return primary;
|
||||
if (hasExplicitAccountId) {
|
||||
return primary;
|
||||
}
|
||||
if (primary.tokenSource !== "none") {
|
||||
return primary;
|
||||
}
|
||||
|
||||
// If accountId is omitted, prefer a configured account token over failing on
|
||||
// the implicit "default" account. This keeps env-based setups working while
|
||||
// making config-only tokens work for things like heartbeats.
|
||||
const fallbackId = resolveDefaultTelegramAccountId(params.cfg);
|
||||
if (fallbackId === primary.accountId) return primary;
|
||||
if (fallbackId === primary.accountId) {
|
||||
return primary;
|
||||
}
|
||||
const fallback = resolve(fallbackId);
|
||||
if (fallback.tokenSource === "none") return primary;
|
||||
if (fallback.tokenSource === "none") {
|
||||
return primary;
|
||||
}
|
||||
return fallback;
|
||||
}
|
||||
|
||||
|
||||
@@ -16,8 +16,12 @@ type TelegramApiLoggingParams<T> = {
|
||||
const fallbackLogger = createSubsystemLogger("telegram/api");
|
||||
|
||||
function resolveTelegramApiLogger(runtime?: RuntimeEnv, logger?: TelegramApiLogger) {
|
||||
if (logger) return logger;
|
||||
if (runtime?.error) return runtime.error;
|
||||
if (logger) {
|
||||
return logger;
|
||||
}
|
||||
if (runtime?.error) {
|
||||
return runtime.error;
|
||||
}
|
||||
return (message: string) => fallbackLogger.error(message);
|
||||
}
|
||||
|
||||
|
||||
@@ -57,12 +57,22 @@ export function collectTelegramUnmentionedGroupIds(
|
||||
const groupIds: string[] = [];
|
||||
let unresolvedGroups = 0;
|
||||
for (const [key, value] of Object.entries(groups)) {
|
||||
if (key === "*") continue;
|
||||
if (!value || typeof value !== "object") continue;
|
||||
if (value.enabled === false) continue;
|
||||
if (value.requireMention !== false) continue;
|
||||
if (key === "*") {
|
||||
continue;
|
||||
}
|
||||
if (!value || typeof value !== "object") {
|
||||
continue;
|
||||
}
|
||||
if (value.enabled === false) {
|
||||
continue;
|
||||
}
|
||||
if (value.requireMention !== false) {
|
||||
continue;
|
||||
}
|
||||
const id = String(key).trim();
|
||||
if (!id) continue;
|
||||
if (!id) {
|
||||
continue;
|
||||
}
|
||||
if (/^-?\d+$/.test(id)) {
|
||||
groupIds.push(id);
|
||||
} else {
|
||||
|
||||
@@ -36,7 +36,9 @@ export const normalizeAllowFromWithStore = (params: {
|
||||
|
||||
export const firstDefined = <T>(...values: Array<T | undefined>) => {
|
||||
for (const value of values) {
|
||||
if (typeof value !== "undefined") return value;
|
||||
if (typeof value !== "undefined") {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
@@ -47,11 +49,19 @@ export const isSenderAllowed = (params: {
|
||||
senderUsername?: string;
|
||||
}) => {
|
||||
const { allow, senderId, senderUsername } = params;
|
||||
if (!allow.hasEntries) return true;
|
||||
if (allow.hasWildcard) return true;
|
||||
if (senderId && allow.entries.includes(senderId)) return true;
|
||||
if (!allow.hasEntries) {
|
||||
return true;
|
||||
}
|
||||
if (allow.hasWildcard) {
|
||||
return true;
|
||||
}
|
||||
if (senderId && allow.entries.includes(senderId)) {
|
||||
return true;
|
||||
}
|
||||
const username = senderUsername?.toLowerCase();
|
||||
if (!username) return false;
|
||||
if (!username) {
|
||||
return false;
|
||||
}
|
||||
return allow.entriesLower.some((entry) => entry === username || entry === `@${username}`);
|
||||
};
|
||||
|
||||
@@ -64,12 +74,16 @@ export const resolveSenderAllowMatch = (params: {
|
||||
if (allow.hasWildcard) {
|
||||
return { allowed: true, matchKey: "*", matchSource: "wildcard" };
|
||||
}
|
||||
if (!allow.hasEntries) return { allowed: false };
|
||||
if (!allow.hasEntries) {
|
||||
return { allowed: false };
|
||||
}
|
||||
if (senderId && allow.entries.includes(senderId)) {
|
||||
return { allowed: true, matchKey: senderId, matchSource: "id" };
|
||||
}
|
||||
const username = senderUsername?.toLowerCase();
|
||||
if (!username) return { allowed: false };
|
||||
if (!username) {
|
||||
return { allowed: false };
|
||||
}
|
||||
const entry = allow.entriesLower.find(
|
||||
(candidate) => candidate === username || candidate === `@${username}`,
|
||||
);
|
||||
|
||||
@@ -68,14 +68,20 @@ export const registerTelegramHandlers = ({
|
||||
debounceMs,
|
||||
buildKey: (entry) => entry.debounceKey,
|
||||
shouldDebounce: (entry) => {
|
||||
if (entry.allMedia.length > 0) return false;
|
||||
if (entry.allMedia.length > 0) {
|
||||
return false;
|
||||
}
|
||||
const text = entry.msg.text ?? entry.msg.caption ?? "";
|
||||
if (!text.trim()) return false;
|
||||
if (!text.trim()) {
|
||||
return false;
|
||||
}
|
||||
return !hasControlCommand(text, cfg, { botUsername: entry.botUsername });
|
||||
},
|
||||
onFlush: async (entries) => {
|
||||
const last = entries.at(-1);
|
||||
if (!last) return;
|
||||
if (!last) {
|
||||
return;
|
||||
}
|
||||
if (entries.length === 1) {
|
||||
await processMessage(last.ctx, last.allMedia, last.storeAllowFrom);
|
||||
return;
|
||||
@@ -84,7 +90,9 @@ export const registerTelegramHandlers = ({
|
||||
.map((entry) => entry.msg.text ?? entry.msg.caption ?? "")
|
||||
.filter(Boolean)
|
||||
.join("\n");
|
||||
if (!combinedText.trim()) return;
|
||||
if (!combinedText.trim()) {
|
||||
return;
|
||||
}
|
||||
const first = entries[0];
|
||||
const baseCtx = first.ctx as { me?: unknown; getFile?: unknown } & Record<string, unknown>;
|
||||
const getFile =
|
||||
@@ -146,10 +154,14 @@ export const registerTelegramHandlers = ({
|
||||
|
||||
const first = entry.messages[0];
|
||||
const last = entry.messages.at(-1);
|
||||
if (!first || !last) return;
|
||||
if (!first || !last) {
|
||||
return;
|
||||
}
|
||||
|
||||
const combinedText = entry.messages.map((m) => m.msg.text ?? "").join("");
|
||||
if (!combinedText.trim()) return;
|
||||
if (!combinedText.trim()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const syntheticMessage: TelegramMessage = {
|
||||
...first.msg,
|
||||
@@ -191,8 +203,12 @@ export const registerTelegramHandlers = ({
|
||||
|
||||
bot.on("callback_query", async (ctx) => {
|
||||
const callback = ctx.callbackQuery;
|
||||
if (!callback) return;
|
||||
if (shouldSkipUpdate(ctx)) return;
|
||||
if (!callback) {
|
||||
return;
|
||||
}
|
||||
if (shouldSkipUpdate(ctx)) {
|
||||
return;
|
||||
}
|
||||
// Answer immediately to prevent Telegram from retrying while we process
|
||||
await withTelegramApiErrorLogging({
|
||||
operation: "answerCallbackQuery",
|
||||
@@ -202,19 +218,27 @@ export const registerTelegramHandlers = ({
|
||||
try {
|
||||
const data = (callback.data ?? "").trim();
|
||||
const callbackMessage = callback.message;
|
||||
if (!data || !callbackMessage) return;
|
||||
if (!data || !callbackMessage) {
|
||||
return;
|
||||
}
|
||||
|
||||
const inlineButtonsScope = resolveTelegramInlineButtonsScope({
|
||||
cfg,
|
||||
accountId,
|
||||
});
|
||||
if (inlineButtonsScope === "off") return;
|
||||
if (inlineButtonsScope === "off") {
|
||||
return;
|
||||
}
|
||||
|
||||
const chatId = callbackMessage.chat.id;
|
||||
const isGroup =
|
||||
callbackMessage.chat.type === "group" || callbackMessage.chat.type === "supergroup";
|
||||
if (inlineButtonsScope === "dm" && isGroup) return;
|
||||
if (inlineButtonsScope === "group" && !isGroup) return;
|
||||
if (inlineButtonsScope === "dm" && isGroup) {
|
||||
return;
|
||||
}
|
||||
if (inlineButtonsScope === "group" && !isGroup) {
|
||||
return;
|
||||
}
|
||||
|
||||
const messageThreadId = (callbackMessage as { message_thread_id?: number }).message_thread_id;
|
||||
const isForum = (callbackMessage.chat as { is_forum?: boolean }).is_forum === true;
|
||||
@@ -303,7 +327,9 @@ export const registerTelegramHandlers = ({
|
||||
|
||||
if (inlineButtonsScope === "allowlist") {
|
||||
if (!isGroup) {
|
||||
if (dmPolicy === "disabled") return;
|
||||
if (dmPolicy === "disabled") {
|
||||
return;
|
||||
}
|
||||
if (dmPolicy !== "open") {
|
||||
const allowed =
|
||||
effectiveDmAllow.hasWildcard ||
|
||||
@@ -313,7 +339,9 @@ export const registerTelegramHandlers = ({
|
||||
senderId,
|
||||
senderUsername,
|
||||
}));
|
||||
if (!allowed) return;
|
||||
if (!allowed) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
const allowed =
|
||||
@@ -324,17 +352,23 @@ export const registerTelegramHandlers = ({
|
||||
senderId,
|
||||
senderUsername,
|
||||
}));
|
||||
if (!allowed) return;
|
||||
if (!allowed) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const paginationMatch = data.match(/^commands_page_(\d+|noop)(?::(.+))?$/);
|
||||
if (paginationMatch) {
|
||||
const pageValue = paginationMatch[1];
|
||||
if (pageValue === "noop") return;
|
||||
if (pageValue === "noop") {
|
||||
return;
|
||||
}
|
||||
|
||||
const page = Number.parseInt(pageValue, 10);
|
||||
if (Number.isNaN(page) || page < 1) return;
|
||||
if (Number.isNaN(page) || page < 1) {
|
||||
return;
|
||||
}
|
||||
|
||||
const agentId = paginationMatch[2]?.trim() || resolveDefaultAgentId(cfg) || undefined;
|
||||
const skillCommands = listSkillCommandsForAgents({
|
||||
@@ -391,8 +425,12 @@ export const registerTelegramHandlers = ({
|
||||
bot.on("message:migrate_to_chat_id", async (ctx) => {
|
||||
try {
|
||||
const msg = ctx.message;
|
||||
if (!msg?.migrate_to_chat_id) return;
|
||||
if (shouldSkipUpdate(ctx)) return;
|
||||
if (!msg?.migrate_to_chat_id) {
|
||||
return;
|
||||
}
|
||||
if (shouldSkipUpdate(ctx)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const oldChatId = String(msg.chat.id);
|
||||
const newChatId = String(msg.migrate_to_chat_id);
|
||||
@@ -438,8 +476,12 @@ export const registerTelegramHandlers = ({
|
||||
bot.on("message", async (ctx) => {
|
||||
try {
|
||||
const msg = ctx.message;
|
||||
if (!msg) return;
|
||||
if (shouldSkipUpdate(ctx)) return;
|
||||
if (!msg) {
|
||||
return;
|
||||
}
|
||||
if (shouldSkipUpdate(ctx)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const chatId = msg.chat.id;
|
||||
const isGroup = msg.chat.type === "group" || msg.chat.type === "supergroup";
|
||||
|
||||
@@ -121,7 +121,9 @@ async function resolveStickerVisionSupport(params: {
|
||||
agentId: params.agentId,
|
||||
});
|
||||
const entry = findModelInCatalog(catalog, defaultModel.provider, defaultModel.model);
|
||||
if (!entry) return false;
|
||||
if (!entry) {
|
||||
return false;
|
||||
}
|
||||
return modelSupportsVision(entry);
|
||||
} catch {
|
||||
return false;
|
||||
@@ -221,7 +223,9 @@ export const buildTelegramMessageContext = async ({
|
||||
|
||||
// DM access control (secure defaults): "pairing" (default) / "allowlist" / "open" / "disabled"
|
||||
if (!isGroup) {
|
||||
if (dmPolicy === "disabled") return null;
|
||||
if (dmPolicy === "disabled") {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (dmPolicy !== "open") {
|
||||
const candidate = String(chatId);
|
||||
@@ -333,12 +337,19 @@ export const buildTelegramMessageContext = async ({
|
||||
const historyKey = isGroup ? buildTelegramGroupPeerId(chatId, resolvedThreadId) : undefined;
|
||||
|
||||
let placeholder = "";
|
||||
if (msg.photo) placeholder = "<media:image>";
|
||||
else if (msg.video) placeholder = "<media:video>";
|
||||
else if (msg.video_note) placeholder = "<media:video>";
|
||||
else if (msg.audio || msg.voice) placeholder = "<media:audio>";
|
||||
else if (msg.document) placeholder = "<media:document>";
|
||||
else if (msg.sticker) placeholder = "<media:sticker>";
|
||||
if (msg.photo) {
|
||||
placeholder = "<media:image>";
|
||||
} else if (msg.video) {
|
||||
placeholder = "<media:video>";
|
||||
} else if (msg.video_note) {
|
||||
placeholder = "<media:video>";
|
||||
} else if (msg.audio || msg.voice) {
|
||||
placeholder = "<media:audio>";
|
||||
} else if (msg.document) {
|
||||
placeholder = "<media:document>";
|
||||
} else if (msg.sticker) {
|
||||
placeholder = "<media:sticker>";
|
||||
}
|
||||
|
||||
// Check if sticker has a cached description - if so, use it instead of sending the image
|
||||
const cachedStickerDescription = allMedia[0]?.stickerMetadata?.cachedDescription;
|
||||
@@ -359,8 +370,12 @@ export const buildTelegramMessageContext = async ({
|
||||
const rawTextSource = msg.text ?? msg.caption ?? "";
|
||||
const rawText = expandTextLinks(rawTextSource, msg.entities ?? msg.caption_entities).trim();
|
||||
let rawBody = [rawText, locationText].filter(Boolean).join("\n").trim();
|
||||
if (!rawBody) rawBody = placeholder;
|
||||
if (!rawBody && allMedia.length === 0) return null;
|
||||
if (!rawBody) {
|
||||
rawBody = placeholder;
|
||||
}
|
||||
if (!rawBody && allMedia.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let bodyText = rawBody;
|
||||
if (!bodyText && allMedia.length > 0) {
|
||||
|
||||
@@ -28,7 +28,9 @@ async function resolveStickerVisionSupport(cfg, agentId) {
|
||||
const catalog = await loadModelCatalog({ config: cfg });
|
||||
const defaultModel = resolveDefaultModelForAgent({ cfg, agentId });
|
||||
const entry = findModelInCatalog(catalog, defaultModel.provider, defaultModel.model);
|
||||
if (!entry) return false;
|
||||
if (!entry) {
|
||||
return false;
|
||||
}
|
||||
return modelSupportsVision(entry);
|
||||
} catch {
|
||||
return false;
|
||||
@@ -92,8 +94,12 @@ export const dispatchTelegramMessage = async ({
|
||||
let lastPartialText = "";
|
||||
let draftText = "";
|
||||
const updateDraftFromPartial = (text?: string) => {
|
||||
if (!draftStream || !text) return;
|
||||
if (text === lastPartialText) return;
|
||||
if (!draftStream || !text) {
|
||||
return;
|
||||
}
|
||||
if (text === lastPartialText) {
|
||||
return;
|
||||
}
|
||||
if (streamMode === "partial") {
|
||||
lastPartialText = text;
|
||||
draftStream.update(text);
|
||||
@@ -108,7 +114,9 @@ export const dispatchTelegramMessage = async ({
|
||||
draftText = "";
|
||||
}
|
||||
lastPartialText = text;
|
||||
if (!delta) return;
|
||||
if (!delta) {
|
||||
return;
|
||||
}
|
||||
if (!draftChunker) {
|
||||
draftText = text;
|
||||
draftStream.update(draftText);
|
||||
@@ -124,7 +132,9 @@ export const dispatchTelegramMessage = async ({
|
||||
});
|
||||
};
|
||||
const flushDraft = async () => {
|
||||
if (!draftStream) return;
|
||||
if (!draftStream) {
|
||||
return;
|
||||
}
|
||||
if (draftChunker?.hasBuffered()) {
|
||||
draftChunker.drain({
|
||||
force: true,
|
||||
@@ -133,7 +143,9 @@ export const dispatchTelegramMessage = async ({
|
||||
},
|
||||
});
|
||||
draftChunker.reset();
|
||||
if (draftText) draftStream.update(draftText);
|
||||
if (draftText) {
|
||||
draftStream.update(draftText);
|
||||
}
|
||||
}
|
||||
await draftStream.flush();
|
||||
};
|
||||
@@ -240,7 +252,9 @@ export const dispatchTelegramMessage = async ({
|
||||
}
|
||||
},
|
||||
onSkip: (_payload, info) => {
|
||||
if (info.reason !== "silent") deliveryState.skippedNonSilent += 1;
|
||||
if (info.reason !== "silent") {
|
||||
deliveryState.skippedNonSilent += 1;
|
||||
}
|
||||
},
|
||||
onError: (err, info) => {
|
||||
runtime.error?.(danger(`telegram ${info.kind} reply failed: ${String(err)}`));
|
||||
@@ -262,7 +276,9 @@ export const dispatchTelegramMessage = async ({
|
||||
onPartialReply: draftStream ? (payload) => updateDraftFromPartial(payload.text) : undefined,
|
||||
onReasoningStream: draftStream
|
||||
? (payload) => {
|
||||
if (payload.text) draftStream.update(payload.text);
|
||||
if (payload.text) {
|
||||
draftStream.update(payload.text);
|
||||
}
|
||||
}
|
||||
: undefined,
|
||||
disableBlockStreaming,
|
||||
@@ -304,7 +320,9 @@ export const dispatchTelegramMessage = async ({
|
||||
ackReactionValue: ackReactionPromise ? "ack" : null,
|
||||
remove: () => reactionApi?.(chatId, msg.message_id ?? 0, []) ?? Promise.resolve(),
|
||||
onError: (err) => {
|
||||
if (!msg.message_id) return;
|
||||
if (!msg.message_id) {
|
||||
return;
|
||||
}
|
||||
logAckFailure({
|
||||
log: logVerbose,
|
||||
channel: "telegram",
|
||||
|
||||
@@ -46,7 +46,9 @@ export const createTelegramMessageProcessor = (deps) => {
|
||||
resolveGroupRequireMention,
|
||||
resolveTelegramGroupConfig,
|
||||
});
|
||||
if (!context) return;
|
||||
if (!context) {
|
||||
return;
|
||||
}
|
||||
await dispatchTelegramMessage({
|
||||
context,
|
||||
bot,
|
||||
|
||||
@@ -344,8 +344,12 @@ export const registerTelegramNativeCommands = ({
|
||||
for (const command of nativeCommands) {
|
||||
bot.command(command.name, async (ctx: TelegramNativeCommandContext) => {
|
||||
const msg = ctx.message;
|
||||
if (!msg) return;
|
||||
if (shouldSkipUpdate(ctx)) return;
|
||||
if (!msg) {
|
||||
return;
|
||||
}
|
||||
if (shouldSkipUpdate(ctx)) {
|
||||
return;
|
||||
}
|
||||
const auth = await resolveTelegramCommandAuth({
|
||||
msg,
|
||||
bot,
|
||||
@@ -358,7 +362,9 @@ export const registerTelegramNativeCommands = ({
|
||||
resolveTelegramGroupConfig,
|
||||
requireAuth: true,
|
||||
});
|
||||
if (!auth) return;
|
||||
if (!auth) {
|
||||
return;
|
||||
}
|
||||
const {
|
||||
chatId,
|
||||
isGroup,
|
||||
@@ -522,7 +528,9 @@ export const registerTelegramNativeCommands = ({
|
||||
}
|
||||
},
|
||||
onSkip: (_payload, info) => {
|
||||
if (info.reason !== "silent") deliveryState.skippedNonSilent += 1;
|
||||
if (info.reason !== "silent") {
|
||||
deliveryState.skippedNonSilent += 1;
|
||||
}
|
||||
},
|
||||
onError: (err, info) => {
|
||||
runtime.error?.(danger(`telegram slash ${info.kind} reply failed: ${String(err)}`));
|
||||
@@ -554,8 +562,12 @@ export const registerTelegramNativeCommands = ({
|
||||
for (const pluginCommand of pluginCommands) {
|
||||
bot.command(pluginCommand.command, async (ctx: TelegramNativeCommandContext) => {
|
||||
const msg = ctx.message;
|
||||
if (!msg) return;
|
||||
if (shouldSkipUpdate(ctx)) return;
|
||||
if (!msg) {
|
||||
return;
|
||||
}
|
||||
if (shouldSkipUpdate(ctx)) {
|
||||
return;
|
||||
}
|
||||
const chatId = msg.chat.id;
|
||||
const rawText = ctx.match?.trim() ?? "";
|
||||
const commandBody = `/${pluginCommand.command}${rawText ? ` ${rawText}` : ""}`;
|
||||
@@ -580,7 +592,9 @@ export const registerTelegramNativeCommands = ({
|
||||
resolveTelegramGroupConfig,
|
||||
requireAuth: match.command.requireAuth !== false,
|
||||
});
|
||||
if (!auth) return;
|
||||
if (!auth) {
|
||||
return;
|
||||
}
|
||||
const { resolvedThreadId, senderId, commandAuthorized, isGroup } = auth;
|
||||
const messageThreadId = (msg as { message_thread_id?: number }).message_thread_id;
|
||||
const threadIdForSend = isGroup ? resolvedThreadId : messageThreadId;
|
||||
|
||||
@@ -29,9 +29,13 @@ export const resolveTelegramUpdateId = (ctx: TelegramUpdateKeyContext) =>
|
||||
|
||||
export const buildTelegramUpdateKey = (ctx: TelegramUpdateKeyContext) => {
|
||||
const updateId = resolveTelegramUpdateId(ctx);
|
||||
if (typeof updateId === "number") return `update:${updateId}`;
|
||||
if (typeof updateId === "number") {
|
||||
return `update:${updateId}`;
|
||||
}
|
||||
const callbackId = ctx.callbackQuery?.id;
|
||||
if (callbackId) return `callback:${callbackId}`;
|
||||
if (callbackId) {
|
||||
return `callback:${callbackId}`;
|
||||
}
|
||||
const msg =
|
||||
ctx.message ?? ctx.update?.message ?? ctx.update?.edited_message ?? ctx.callbackQuery?.message;
|
||||
const chatId = msg?.chat?.id;
|
||||
|
||||
@@ -129,7 +129,9 @@ let replyModule: typeof import("../auto-reply/reply.js");
|
||||
|
||||
const getOnHandler = (event: string) => {
|
||||
const handler = onSpy.mock.calls.find((call) => call[0] === event)?.[1];
|
||||
if (!handler) throw new Error(`Missing handler for event: ${event}`);
|
||||
if (!handler) {
|
||||
throw new Error(`Missing handler for event: ${event}`);
|
||||
}
|
||||
return handler as (ctx: Record<string, unknown>) => Promise<void>;
|
||||
};
|
||||
|
||||
|
||||
@@ -128,7 +128,9 @@ let replyModule: typeof import("../auto-reply/reply.js");
|
||||
|
||||
const getOnHandler = (event: string) => {
|
||||
const handler = onSpy.mock.calls.find((call) => call[0] === event)?.[1];
|
||||
if (!handler) throw new Error(`Missing handler for event: ${event}`);
|
||||
if (!handler) {
|
||||
throw new Error(`Missing handler for event: ${event}`);
|
||||
}
|
||||
return handler as (ctx: Record<string, unknown>) => Promise<void>;
|
||||
};
|
||||
|
||||
@@ -326,7 +328,9 @@ describe("createTelegramBot", () => {
|
||||
const verboseHandler = commandSpy.mock.calls.find((call) => call[0] === "verbose")?.[1] as
|
||||
| ((ctx: Record<string, unknown>) => Promise<void>)
|
||||
| undefined;
|
||||
if (!verboseHandler) throw new Error("verbose command handler missing");
|
||||
if (!verboseHandler) {
|
||||
throw new Error("verbose command handler missing");
|
||||
}
|
||||
|
||||
await verboseHandler({
|
||||
message: {
|
||||
|
||||
@@ -128,7 +128,9 @@ let replyModule: typeof import("../auto-reply/reply.js");
|
||||
|
||||
const getOnHandler = (event: string) => {
|
||||
const handler = onSpy.mock.calls.find((call) => call[0] === event)?.[1];
|
||||
if (!handler) throw new Error(`Missing handler for event: ${event}`);
|
||||
if (!handler) {
|
||||
throw new Error(`Missing handler for event: ${event}`);
|
||||
}
|
||||
return handler as (ctx: Record<string, unknown>) => Promise<void>;
|
||||
};
|
||||
|
||||
|
||||
@@ -128,7 +128,9 @@ let replyModule: typeof import("../auto-reply/reply.js");
|
||||
|
||||
const getOnHandler = (event: string) => {
|
||||
const handler = onSpy.mock.calls.find((call) => call[0] === event)?.[1];
|
||||
if (!handler) throw new Error(`Missing handler for event: ${event}`);
|
||||
if (!handler) {
|
||||
throw new Error(`Missing handler for event: ${event}`);
|
||||
}
|
||||
return handler as (ctx: Record<string, unknown>) => Promise<void>;
|
||||
};
|
||||
|
||||
|
||||
@@ -132,7 +132,9 @@ let replyModule: typeof import("../auto-reply/reply.js");
|
||||
|
||||
const getOnHandler = (event: string) => {
|
||||
const handler = onSpy.mock.calls.find((call) => call[0] === event)?.[1];
|
||||
if (!handler) throw new Error(`Missing handler for event: ${event}`);
|
||||
if (!handler) {
|
||||
throw new Error(`Missing handler for event: ${event}`);
|
||||
}
|
||||
return handler as (ctx: Record<string, unknown>) => Promise<void>;
|
||||
};
|
||||
|
||||
|
||||
@@ -128,7 +128,9 @@ let replyModule: typeof import("../auto-reply/reply.js");
|
||||
|
||||
const getOnHandler = (event: string) => {
|
||||
const handler = onSpy.mock.calls.find((call) => call[0] === event)?.[1];
|
||||
if (!handler) throw new Error(`Missing handler for event: ${event}`);
|
||||
if (!handler) {
|
||||
throw new Error(`Missing handler for event: ${event}`);
|
||||
}
|
||||
return handler as (ctx: Record<string, unknown>) => Promise<void>;
|
||||
};
|
||||
|
||||
|
||||
@@ -128,7 +128,9 @@ let replyModule: typeof import("../auto-reply/reply.js");
|
||||
|
||||
const getOnHandler = (event: string) => {
|
||||
const handler = onSpy.mock.calls.find((call) => call[0] === event)?.[1];
|
||||
if (!handler) throw new Error(`Missing handler for event: ${event}`);
|
||||
if (!handler) {
|
||||
throw new Error(`Missing handler for event: ${event}`);
|
||||
}
|
||||
return handler as (ctx: Record<string, unknown>) => Promise<void>;
|
||||
};
|
||||
|
||||
|
||||
@@ -128,7 +128,9 @@ let replyModule: typeof import("../auto-reply/reply.js");
|
||||
|
||||
const getOnHandler = (event: string) => {
|
||||
const handler = onSpy.mock.calls.find((call) => call[0] === event)?.[1];
|
||||
if (!handler) throw new Error(`Missing handler for event: ${event}`);
|
||||
if (!handler) {
|
||||
throw new Error(`Missing handler for event: ${event}`);
|
||||
}
|
||||
return handler as (ctx: Record<string, unknown>) => Promise<void>;
|
||||
};
|
||||
|
||||
|
||||
@@ -133,7 +133,9 @@ let replyModule: typeof import("../auto-reply/reply.js");
|
||||
|
||||
const getOnHandler = (event: string) => {
|
||||
const handler = onSpy.mock.calls.find((call) => call[0] === event)?.[1];
|
||||
if (!handler) throw new Error(`Missing handler for event: ${event}`);
|
||||
if (!handler) {
|
||||
throw new Error(`Missing handler for event: ${event}`);
|
||||
}
|
||||
return handler as (ctx: Record<string, unknown>) => Promise<void>;
|
||||
};
|
||||
|
||||
|
||||
@@ -167,7 +167,9 @@ vi.mock("../auto-reply/reply.js", () => {
|
||||
|
||||
const getOnHandler = (event: string) => {
|
||||
const handler = onSpy.mock.calls.find((call) => call[0] === event)?.[1];
|
||||
if (!handler) throw new Error(`Missing handler for event: ${event}`);
|
||||
if (!handler) {
|
||||
throw new Error(`Missing handler for event: ${event}`);
|
||||
}
|
||||
return handler as (ctx: Record<string, unknown>) => Promise<void>;
|
||||
};
|
||||
|
||||
@@ -2339,7 +2341,9 @@ describe("createTelegramBot", () => {
|
||||
const handler = commandSpy.mock.calls.find((call) => call[0] === "status")?.[1] as
|
||||
| ((ctx: Record<string, unknown>) => Promise<void>)
|
||||
| undefined;
|
||||
if (!handler) throw new Error("status command handler missing");
|
||||
if (!handler) {
|
||||
throw new Error("status command handler missing");
|
||||
}
|
||||
|
||||
await handler({
|
||||
message: {
|
||||
@@ -2380,7 +2384,9 @@ describe("createTelegramBot", () => {
|
||||
const handler = commandSpy.mock.calls.find((call) => call[0] === "status")?.[1] as
|
||||
| ((ctx: Record<string, unknown>) => Promise<void>)
|
||||
| undefined;
|
||||
if (!handler) throw new Error("status command handler missing");
|
||||
if (!handler) {
|
||||
throw new Error("status command handler missing");
|
||||
}
|
||||
|
||||
await handler({
|
||||
message: {
|
||||
@@ -2422,7 +2428,9 @@ describe("createTelegramBot", () => {
|
||||
const handler = commandSpy.mock.calls.find((call) => call[0] === "status")?.[1] as
|
||||
| ((ctx: Record<string, unknown>) => Promise<void>)
|
||||
| undefined;
|
||||
if (!handler) throw new Error("status command handler missing");
|
||||
if (!handler) {
|
||||
throw new Error("status command handler missing");
|
||||
}
|
||||
|
||||
await handler({
|
||||
message: {
|
||||
@@ -2467,7 +2475,9 @@ describe("createTelegramBot", () => {
|
||||
const verboseHandler = commandSpy.mock.calls.find((call) => call[0] === "verbose")?.[1] as
|
||||
| ((ctx: Record<string, unknown>) => Promise<void>)
|
||||
| undefined;
|
||||
if (!verboseHandler) throw new Error("verbose command handler missing");
|
||||
if (!verboseHandler) {
|
||||
throw new Error("verbose command handler missing");
|
||||
}
|
||||
|
||||
await verboseHandler({
|
||||
message: {
|
||||
|
||||
@@ -91,7 +91,9 @@ export function getTelegramSequentialKey(ctx: {
|
||||
rawText &&
|
||||
isControlCommandMessage(rawText, undefined, botUsername ? { botUsername } : undefined)
|
||||
) {
|
||||
if (typeof chatId === "number") return `telegram:${chatId}:control`;
|
||||
if (typeof chatId === "number") {
|
||||
return `telegram:${chatId}:control`;
|
||||
}
|
||||
return "telegram:control";
|
||||
}
|
||||
const isGroup = msg?.chat?.type === "group" || msg?.chat?.type === "supergroup";
|
||||
@@ -158,8 +160,12 @@ export function createTelegramBot(opts: TelegramBotOptions) {
|
||||
|
||||
const recordUpdateId = (ctx: TelegramUpdateKeyContext) => {
|
||||
const updateId = resolveTelegramUpdateId(ctx);
|
||||
if (typeof updateId !== "number") return;
|
||||
if (lastUpdateId !== null && updateId <= lastUpdateId) return;
|
||||
if (typeof updateId !== "number") {
|
||||
return;
|
||||
}
|
||||
if (lastUpdateId !== null && updateId <= lastUpdateId) {
|
||||
return;
|
||||
}
|
||||
lastUpdateId = updateId;
|
||||
void opts.updateOffset?.onUpdateId?.(updateId);
|
||||
};
|
||||
@@ -167,7 +173,9 @@ export function createTelegramBot(opts: TelegramBotOptions) {
|
||||
const shouldSkipUpdate = (ctx: TelegramUpdateKeyContext) => {
|
||||
const updateId = resolveTelegramUpdateId(ctx);
|
||||
if (typeof updateId === "number" && lastUpdateId !== null) {
|
||||
if (updateId <= lastUpdateId) return true;
|
||||
if (updateId <= lastUpdateId) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
const key = buildTelegramUpdateKey(ctx);
|
||||
const skipped = recentUpdates.check(key);
|
||||
@@ -195,7 +203,9 @@ export function createTelegramBot(opts: TelegramBotOptions) {
|
||||
}
|
||||
if (value && typeof value === "object") {
|
||||
const obj = value as object;
|
||||
if (seen.has(obj)) return "[Circular]";
|
||||
if (seen.has(obj)) {
|
||||
return "[Circular]";
|
||||
}
|
||||
seen.add(obj);
|
||||
}
|
||||
return value;
|
||||
@@ -261,7 +271,9 @@ export function createTelegramBot(opts: TelegramBotOptions) {
|
||||
botHasTopicsEnabled = fromCtx.has_topics_enabled;
|
||||
return botHasTopicsEnabled;
|
||||
}
|
||||
if (typeof botHasTopicsEnabled === "boolean") return botHasTopicsEnabled;
|
||||
if (typeof botHasTopicsEnabled === "boolean") {
|
||||
return botHasTopicsEnabled;
|
||||
}
|
||||
try {
|
||||
const me = (await withTelegramApiErrorLogging({
|
||||
operation: "getMe",
|
||||
@@ -296,8 +308,12 @@ export function createTelegramBot(opts: TelegramBotOptions) {
|
||||
try {
|
||||
const store = loadSessionStore(storePath);
|
||||
const entry = store[sessionKey];
|
||||
if (entry?.groupActivation === "always") return false;
|
||||
if (entry?.groupActivation === "mention") return true;
|
||||
if (entry?.groupActivation === "always") {
|
||||
return false;
|
||||
}
|
||||
if (entry?.groupActivation === "mention") {
|
||||
return true;
|
||||
}
|
||||
} catch (err) {
|
||||
logVerbose(`Failed to load session for activation check: ${String(err)}`);
|
||||
}
|
||||
@@ -314,7 +330,9 @@ export function createTelegramBot(opts: TelegramBotOptions) {
|
||||
});
|
||||
const resolveTelegramGroupConfig = (chatId: string | number, messageThreadId?: number) => {
|
||||
const groups = telegramCfg.groups;
|
||||
if (!groups) return { groupConfig: undefined, topicConfig: undefined };
|
||||
if (!groups) {
|
||||
return { groupConfig: undefined, topicConfig: undefined };
|
||||
}
|
||||
const groupKey = String(chatId);
|
||||
const groupConfig = groups[groupKey] ?? groups["*"];
|
||||
const topicConfig =
|
||||
@@ -369,8 +387,12 @@ export function createTelegramBot(opts: TelegramBotOptions) {
|
||||
bot.on("message_reaction", async (ctx) => {
|
||||
try {
|
||||
const reaction = ctx.messageReaction;
|
||||
if (!reaction) return;
|
||||
if (shouldSkipUpdate(ctx)) return;
|
||||
if (!reaction) {
|
||||
return;
|
||||
}
|
||||
if (shouldSkipUpdate(ctx)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const chatId = reaction.chat.id;
|
||||
const messageId = reaction.message_id;
|
||||
@@ -378,9 +400,15 @@ export function createTelegramBot(opts: TelegramBotOptions) {
|
||||
|
||||
// Resolve reaction notification mode (default: "own")
|
||||
const reactionMode = telegramCfg.reactionNotifications ?? "own";
|
||||
if (reactionMode === "off") return;
|
||||
if (user?.is_bot) return;
|
||||
if (reactionMode === "own" && !wasSentByBot(chatId, messageId)) return;
|
||||
if (reactionMode === "off") {
|
||||
return;
|
||||
}
|
||||
if (user?.is_bot) {
|
||||
return;
|
||||
}
|
||||
if (reactionMode === "own" && !wasSentByBot(chatId, messageId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Detect added reactions
|
||||
const oldEmojis = new Set(
|
||||
@@ -392,7 +420,9 @@ export function createTelegramBot(opts: TelegramBotOptions) {
|
||||
.filter((r): r is { type: "emoji"; emoji: string } => r.type === "emoji")
|
||||
.filter((r) => !oldEmojis.has(r.emoji));
|
||||
|
||||
if (addedReactions.length === 0) return;
|
||||
if (addedReactions.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Build sender label
|
||||
const senderName = user
|
||||
|
||||
@@ -105,7 +105,9 @@ export async function deliverReplies(params: {
|
||||
const chunks = chunkText(reply.text || "");
|
||||
for (let i = 0; i < chunks.length; i += 1) {
|
||||
const chunk = chunks[i];
|
||||
if (!chunk) continue;
|
||||
if (!chunk) {
|
||||
continue;
|
||||
}
|
||||
// Only attach buttons to the first chunk.
|
||||
const shouldAttachButtons = i === 0 && replyMarkup;
|
||||
await sendTelegramText(bot, chatId, chunk.html, runtime, {
|
||||
@@ -306,7 +308,9 @@ export async function resolveMedia(
|
||||
logVerbose("telegram: skipping animated/video sticker (only static stickers supported)");
|
||||
return null;
|
||||
}
|
||||
if (!sticker.file_id) return null;
|
||||
if (!sticker.file_id) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
const file = await ctx.getFile();
|
||||
@@ -389,7 +393,9 @@ export async function resolveMedia(
|
||||
msg.document ??
|
||||
msg.audio ??
|
||||
msg.voice;
|
||||
if (!m?.file_id) return null;
|
||||
if (!m?.file_id) {
|
||||
return null;
|
||||
}
|
||||
const file = await ctx.getFile();
|
||||
if (!file.file_path) {
|
||||
throw new Error("Telegram getFile returned no file_path");
|
||||
@@ -413,10 +419,15 @@ export async function resolveMedia(
|
||||
originalName,
|
||||
);
|
||||
let placeholder = "<media:document>";
|
||||
if (msg.photo) placeholder = "<media:image>";
|
||||
else if (msg.video) placeholder = "<media:video>";
|
||||
else if (msg.video_note) placeholder = "<media:video>";
|
||||
else if (msg.audio || msg.voice) placeholder = "<media:audio>";
|
||||
if (msg.photo) {
|
||||
placeholder = "<media:image>";
|
||||
} else if (msg.video) {
|
||||
placeholder = "<media:video>";
|
||||
} else if (msg.video_note) {
|
||||
placeholder = "<media:video>";
|
||||
} else if (msg.audio || msg.voice) {
|
||||
placeholder = "<media:audio>";
|
||||
}
|
||||
return { path: saved.path, contentType: saved.contentType, placeholder };
|
||||
}
|
||||
|
||||
|
||||
@@ -65,7 +65,9 @@ export function resolveTelegramStreamMode(
|
||||
telegramCfg: Pick<TelegramAccountConfig, "streamMode"> | undefined,
|
||||
): TelegramStreamMode {
|
||||
const raw = telegramCfg?.streamMode?.trim().toLowerCase();
|
||||
if (raw === "off" || raw === "partial" || raw === "block") return raw;
|
||||
if (raw === "off" || raw === "partial" || raw === "block") {
|
||||
return raw;
|
||||
}
|
||||
return "partial";
|
||||
}
|
||||
|
||||
@@ -97,8 +99,12 @@ export function buildSenderLabel(msg: TelegramMessage, senderId?: number | strin
|
||||
senderId != null && `${senderId}`.trim() ? `${senderId}`.trim() : undefined;
|
||||
const fallbackId = normalizedSenderId ?? (msg.from?.id != null ? String(msg.from.id) : undefined);
|
||||
const idPart = fallbackId ? `id:${fallbackId}` : undefined;
|
||||
if (label && idPart) return `${label} ${idPart}`;
|
||||
if (label) return label;
|
||||
if (label && idPart) {
|
||||
return `${label} ${idPart}`;
|
||||
}
|
||||
if (label) {
|
||||
return label;
|
||||
}
|
||||
return idPart ?? "id:unknown";
|
||||
}
|
||||
|
||||
@@ -109,18 +115,26 @@ export function buildGroupLabel(
|
||||
) {
|
||||
const title = msg.chat?.title;
|
||||
const topicSuffix = messageThreadId != null ? ` topic:${messageThreadId}` : "";
|
||||
if (title) return `${title} id:${chatId}${topicSuffix}`;
|
||||
if (title) {
|
||||
return `${title} id:${chatId}${topicSuffix}`;
|
||||
}
|
||||
return `group:${chatId}${topicSuffix}`;
|
||||
}
|
||||
|
||||
export function hasBotMention(msg: TelegramMessage, botUsername: string) {
|
||||
const text = (msg.text ?? msg.caption ?? "").toLowerCase();
|
||||
if (text.includes(`@${botUsername}`)) return true;
|
||||
if (text.includes(`@${botUsername}`)) {
|
||||
return true;
|
||||
}
|
||||
const entities = msg.entities ?? msg.caption_entities ?? [];
|
||||
for (const ent of entities) {
|
||||
if (ent.type !== "mention") continue;
|
||||
if (ent.type !== "mention") {
|
||||
continue;
|
||||
}
|
||||
const slice = (msg.text ?? msg.caption ?? "").slice(ent.offset, ent.offset + ent.length);
|
||||
if (slice.toLowerCase() === `@${botUsername}`) return true;
|
||||
if (slice.toLowerCase() === `@${botUsername}`) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
@@ -133,7 +147,9 @@ type TelegramTextLinkEntity = {
|
||||
};
|
||||
|
||||
export function expandTextLinks(text: string, entities?: TelegramTextLinkEntity[] | null): string {
|
||||
if (!text || !entities?.length) return text;
|
||||
if (!text || !entities?.length) {
|
||||
return text;
|
||||
}
|
||||
|
||||
const textLinks = entities
|
||||
.filter(
|
||||
@@ -142,7 +158,9 @@ export function expandTextLinks(text: string, entities?: TelegramTextLinkEntity[
|
||||
)
|
||||
.toSorted((a, b) => b.offset - a.offset);
|
||||
|
||||
if (textLinks.length === 0) return text;
|
||||
if (textLinks.length === 0) {
|
||||
return text;
|
||||
}
|
||||
|
||||
let result = text;
|
||||
for (const entity of textLinks) {
|
||||
@@ -155,9 +173,13 @@ export function expandTextLinks(text: string, entities?: TelegramTextLinkEntity[
|
||||
}
|
||||
|
||||
export function resolveTelegramReplyId(raw?: string): number | undefined {
|
||||
if (!raw) return undefined;
|
||||
if (!raw) {
|
||||
return undefined;
|
||||
}
|
||||
const parsed = Number(raw);
|
||||
if (!Number.isFinite(parsed)) return undefined;
|
||||
if (!Number.isFinite(parsed)) {
|
||||
return undefined;
|
||||
}
|
||||
return parsed;
|
||||
}
|
||||
|
||||
@@ -185,17 +207,25 @@ export function describeReplyTarget(msg: TelegramMessage): TelegramReplyTarget |
|
||||
const replyBody = (reply.text ?? reply.caption ?? "").trim();
|
||||
body = replyBody;
|
||||
if (!body) {
|
||||
if (reply.photo) body = "<media:image>";
|
||||
else if (reply.video) body = "<media:video>";
|
||||
else if (reply.audio || reply.voice) body = "<media:audio>";
|
||||
else if (reply.document) body = "<media:document>";
|
||||
else {
|
||||
if (reply.photo) {
|
||||
body = "<media:image>";
|
||||
} else if (reply.video) {
|
||||
body = "<media:video>";
|
||||
} else if (reply.audio || reply.voice) {
|
||||
body = "<media:audio>";
|
||||
} else if (reply.document) {
|
||||
body = "<media:document>";
|
||||
} else {
|
||||
const locationData = extractTelegramLocation(reply);
|
||||
if (locationData) body = formatLocationText(locationData);
|
||||
if (locationData) {
|
||||
body = formatLocationText(locationData);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!body) return null;
|
||||
if (!body) {
|
||||
return null;
|
||||
}
|
||||
const sender = reply ? buildSenderName(reply) : undefined;
|
||||
const senderLabel = sender ? `${sender}` : "unknown sender";
|
||||
|
||||
@@ -243,7 +273,9 @@ function buildForwardedContextFromUser(params: {
|
||||
type: string;
|
||||
}): TelegramForwardedContext | null {
|
||||
const { display, name, username, id } = normalizeForwardedUserLabel(params.user);
|
||||
if (!display) return null;
|
||||
if (!display) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
from: display,
|
||||
date: params.date,
|
||||
@@ -260,7 +292,9 @@ function buildForwardedContextFromHiddenName(params: {
|
||||
type: string;
|
||||
}): TelegramForwardedContext | null {
|
||||
const trimmed = params.name?.trim();
|
||||
if (!trimmed) return null;
|
||||
if (!trimmed) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
from: trimmed,
|
||||
date: params.date,
|
||||
@@ -278,7 +312,9 @@ function buildForwardedContextFromChat(params: {
|
||||
const fallbackKind =
|
||||
params.type === "channel" || params.type === "legacy_channel" ? "channel" : "chat";
|
||||
const { display, title, username, id } = normalizeForwardedChatLabel(params.chat, fallbackKind);
|
||||
if (!display) return null;
|
||||
if (!display) {
|
||||
return null;
|
||||
}
|
||||
const signature = params.signature?.trim() || undefined;
|
||||
const from = signature ? `${display} (${signature})` : display;
|
||||
return {
|
||||
@@ -339,7 +375,9 @@ export function normalizeForwardedContext(msg: TelegramMessage): TelegramForward
|
||||
|
||||
if (forwardMsg.forward_origin) {
|
||||
const originContext = resolveForwardOrigin(forwardMsg.forward_origin, signature);
|
||||
if (originContext) return originContext;
|
||||
if (originContext) {
|
||||
return originContext;
|
||||
}
|
||||
}
|
||||
|
||||
if (forwardMsg.forward_from_chat) {
|
||||
@@ -351,7 +389,9 @@ export function normalizeForwardedContext(msg: TelegramMessage): TelegramForward
|
||||
type: legacyType,
|
||||
signature,
|
||||
});
|
||||
if (legacyContext) return legacyContext;
|
||||
if (legacyContext) {
|
||||
return legacyContext;
|
||||
}
|
||||
}
|
||||
|
||||
if (forwardMsg.forward_from) {
|
||||
@@ -360,7 +400,9 @@ export function normalizeForwardedContext(msg: TelegramMessage): TelegramForward
|
||||
date: forwardMsg.forward_date,
|
||||
type: "legacy_user",
|
||||
});
|
||||
if (legacyContext) return legacyContext;
|
||||
if (legacyContext) {
|
||||
return legacyContext;
|
||||
}
|
||||
}
|
||||
|
||||
const hiddenContext = buildForwardedContextFromHiddenName({
|
||||
@@ -368,7 +410,9 @@ export function normalizeForwardedContext(msg: TelegramMessage): TelegramForward
|
||||
date: forwardMsg.forward_date,
|
||||
type: "legacy_hidden_user",
|
||||
});
|
||||
if (hiddenContext) return hiddenContext;
|
||||
if (hiddenContext) {
|
||||
return hiddenContext;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -27,7 +27,9 @@ export async function downloadTelegramFile(
|
||||
info: TelegramFileInfo,
|
||||
maxBytes?: number,
|
||||
): Promise<SavedMedia> {
|
||||
if (!info.file_path) throw new Error("file_path missing");
|
||||
if (!info.file_path) {
|
||||
throw new Error("file_path missing");
|
||||
}
|
||||
const url = `https://api.telegram.org/file/bot${token}/${info.file_path}`;
|
||||
const res = await fetch(url);
|
||||
if (!res.ok || !res.body) {
|
||||
@@ -42,6 +44,8 @@ export async function downloadTelegramFile(
|
||||
// save with inbound subdir
|
||||
const saved = await saveMediaBuffer(array, mime, "inbound", maxBytes, info.file_path);
|
||||
// Ensure extension matches mime if possible
|
||||
if (!saved.contentType && mime) saved.contentType = mime;
|
||||
if (!saved.contentType && mime) {
|
||||
saved.contentType = mime;
|
||||
}
|
||||
return saved;
|
||||
}
|
||||
|
||||
@@ -37,9 +37,13 @@ export function createTelegramDraftStream(params: {
|
||||
let stopped = false;
|
||||
|
||||
const sendDraft = async (text: string) => {
|
||||
if (stopped) return;
|
||||
if (stopped) {
|
||||
return;
|
||||
}
|
||||
const trimmed = text.trimEnd();
|
||||
if (!trimmed) return;
|
||||
if (!trimmed) {
|
||||
return;
|
||||
}
|
||||
if (trimmed.length > maxChars) {
|
||||
// Drafts are capped at 4096 chars. Stop streaming once we exceed the cap
|
||||
// so we don't keep sending failing updates or a truncated preview.
|
||||
@@ -47,7 +51,9 @@ export function createTelegramDraftStream(params: {
|
||||
params.warn?.(`telegram draft stream stopped (draft length ${trimmed.length} > ${maxChars})`);
|
||||
return;
|
||||
}
|
||||
if (trimmed === lastSentText) return;
|
||||
if (trimmed === lastSentText) {
|
||||
return;
|
||||
}
|
||||
lastSentText = trimmed;
|
||||
lastSentAt = Date.now();
|
||||
try {
|
||||
@@ -72,7 +78,9 @@ export function createTelegramDraftStream(params: {
|
||||
const text = pendingText;
|
||||
pendingText = "";
|
||||
if (!text.trim()) {
|
||||
if (pendingText) schedule();
|
||||
if (pendingText) {
|
||||
schedule();
|
||||
}
|
||||
return;
|
||||
}
|
||||
inFlight = true;
|
||||
@@ -81,11 +89,15 @@ export function createTelegramDraftStream(params: {
|
||||
} finally {
|
||||
inFlight = false;
|
||||
}
|
||||
if (pendingText) schedule();
|
||||
if (pendingText) {
|
||||
schedule();
|
||||
}
|
||||
};
|
||||
|
||||
const schedule = () => {
|
||||
if (timer) return;
|
||||
if (timer) {
|
||||
return;
|
||||
}
|
||||
const delay = Math.max(0, throttleMs - (Date.now() - lastSentAt));
|
||||
timer = setTimeout(() => {
|
||||
void flush();
|
||||
@@ -93,7 +105,9 @@ export function createTelegramDraftStream(params: {
|
||||
};
|
||||
|
||||
const update = (text: string) => {
|
||||
if (stopped) return;
|
||||
if (stopped) {
|
||||
return;
|
||||
}
|
||||
pendingText = text;
|
||||
if (inFlight) {
|
||||
schedule();
|
||||
|
||||
@@ -11,7 +11,9 @@ const log = createSubsystemLogger("telegram/network");
|
||||
// See: https://github.com/nodejs/node/issues/54359
|
||||
function applyTelegramNetworkWorkarounds(network?: TelegramNetworkConfig): void {
|
||||
const decision = resolveTelegramAutoSelectFamilyDecision({ network });
|
||||
if (decision.value === null || decision.value === appliedAutoSelectFamily) return;
|
||||
if (decision.value === null || decision.value === appliedAutoSelectFamily) {
|
||||
return;
|
||||
}
|
||||
appliedAutoSelectFamily = decision.value;
|
||||
|
||||
if (typeof net.setDefaultAutoSelectFamily === "function") {
|
||||
@@ -31,7 +33,9 @@ export function resolveTelegramFetch(
|
||||
options?: { network?: TelegramNetworkConfig },
|
||||
): typeof fetch | undefined {
|
||||
applyTelegramNetworkWorkarounds(options?.network);
|
||||
if (proxyFetch) return resolveFetch(proxyFetch);
|
||||
if (proxyFetch) {
|
||||
return resolveFetch(proxyFetch);
|
||||
}
|
||||
const fetchImpl = resolveFetch();
|
||||
if (!fetchImpl) {
|
||||
throw new Error("fetch is not available; set channels.telegram.proxy in config");
|
||||
|
||||
@@ -22,8 +22,12 @@ function escapeHtmlAttr(text: string): string {
|
||||
|
||||
function buildTelegramLink(link: MarkdownLinkSpan, _text: string) {
|
||||
const href = link.href.trim();
|
||||
if (!href) return null;
|
||||
if (link.start === link.end) return null;
|
||||
if (!href) {
|
||||
return null;
|
||||
}
|
||||
if (link.start === link.end) {
|
||||
return null;
|
||||
}
|
||||
const safeHref = escapeHtmlAttr(href);
|
||||
return {
|
||||
start: link.start,
|
||||
@@ -65,7 +69,9 @@ export function renderTelegramHtmlText(
|
||||
options: { textMode?: "markdown" | "html"; tableMode?: MarkdownTableMode } = {},
|
||||
): string {
|
||||
const textMode = options.textMode ?? "markdown";
|
||||
if (textMode === "html") return text;
|
||||
if (textMode === "html") {
|
||||
return text;
|
||||
}
|
||||
return markdownToTelegramHtml(text, { tableMode: options.tableMode });
|
||||
}
|
||||
|
||||
|
||||
@@ -16,12 +16,18 @@ function resolveAccountGroups(
|
||||
cfg: OpenClawConfig,
|
||||
accountId?: string | null,
|
||||
): { groups?: TelegramGroups } {
|
||||
if (!accountId) return {};
|
||||
if (!accountId) {
|
||||
return {};
|
||||
}
|
||||
const normalized = normalizeAccountId(accountId);
|
||||
const accounts = cfg.channels?.telegram?.accounts;
|
||||
if (!accounts || typeof accounts !== "object") return {};
|
||||
if (!accounts || typeof accounts !== "object") {
|
||||
return {};
|
||||
}
|
||||
const exact = accounts[normalized];
|
||||
if (exact?.groups) return { groups: exact.groups };
|
||||
if (exact?.groups) {
|
||||
return { groups: exact.groups };
|
||||
}
|
||||
const matchKey = Object.keys(accounts).find(
|
||||
(key) => key.toLowerCase() === normalized.toLowerCase(),
|
||||
);
|
||||
@@ -33,10 +39,18 @@ export function migrateTelegramGroupsInPlace(
|
||||
oldChatId: string,
|
||||
newChatId: string,
|
||||
): { migrated: boolean; skippedExisting: boolean } {
|
||||
if (!groups) return { migrated: false, skippedExisting: false };
|
||||
if (oldChatId === newChatId) return { migrated: false, skippedExisting: false };
|
||||
if (!Object.hasOwn(groups, oldChatId)) return { migrated: false, skippedExisting: false };
|
||||
if (Object.hasOwn(groups, newChatId)) return { migrated: false, skippedExisting: true };
|
||||
if (!groups) {
|
||||
return { migrated: false, skippedExisting: false };
|
||||
}
|
||||
if (oldChatId === newChatId) {
|
||||
return { migrated: false, skippedExisting: false };
|
||||
}
|
||||
if (!Object.hasOwn(groups, oldChatId)) {
|
||||
return { migrated: false, skippedExisting: false };
|
||||
}
|
||||
if (Object.hasOwn(groups, newChatId)) {
|
||||
return { migrated: false, skippedExisting: true };
|
||||
}
|
||||
groups[newChatId] = groups[oldChatId];
|
||||
delete groups[oldChatId];
|
||||
return { migrated: true, skippedExisting: false };
|
||||
@@ -59,7 +73,9 @@ export function migrateTelegramGroupConfig(params: {
|
||||
migrated = true;
|
||||
scopes.push("account");
|
||||
}
|
||||
if (result.skippedExisting) skippedExisting = true;
|
||||
if (result.skippedExisting) {
|
||||
skippedExisting = true;
|
||||
}
|
||||
}
|
||||
|
||||
const globalGroups = params.cfg.channels?.telegram?.groups;
|
||||
@@ -69,7 +85,9 @@ export function migrateTelegramGroupConfig(params: {
|
||||
migrated = true;
|
||||
scopes.push("global");
|
||||
}
|
||||
if (result.skippedExisting) skippedExisting = true;
|
||||
if (result.skippedExisting) {
|
||||
skippedExisting = true;
|
||||
}
|
||||
}
|
||||
|
||||
return { migrated, skippedExisting, scopes };
|
||||
|
||||
@@ -6,7 +6,9 @@ import { parseTelegramTarget } from "./targets.js";
|
||||
const DEFAULT_INLINE_BUTTONS_SCOPE: TelegramInlineButtonsScope = "allowlist";
|
||||
|
||||
function normalizeInlineButtonsScope(value: unknown): TelegramInlineButtonsScope | undefined {
|
||||
if (typeof value !== "string") return undefined;
|
||||
if (typeof value !== "string") {
|
||||
return undefined;
|
||||
}
|
||||
const trimmed = value.trim().toLowerCase();
|
||||
if (
|
||||
trimmed === "off" ||
|
||||
@@ -23,7 +25,9 @@ function normalizeInlineButtonsScope(value: unknown): TelegramInlineButtonsScope
|
||||
function resolveInlineButtonsScopeFromCapabilities(
|
||||
capabilities: unknown,
|
||||
): TelegramInlineButtonsScope {
|
||||
if (!capabilities) return DEFAULT_INLINE_BUTTONS_SCOPE;
|
||||
if (!capabilities) {
|
||||
return DEFAULT_INLINE_BUTTONS_SCOPE;
|
||||
}
|
||||
if (Array.isArray(capabilities)) {
|
||||
const enabled = capabilities.some(
|
||||
(entry) => String(entry).trim().toLowerCase() === "inlinebuttons",
|
||||
@@ -62,10 +66,14 @@ export function isTelegramInlineButtonsEnabled(params: {
|
||||
}
|
||||
|
||||
export function resolveTelegramTargetChatType(target: string): "direct" | "group" | "unknown" {
|
||||
if (!target.trim()) return "unknown";
|
||||
if (!target.trim()) {
|
||||
return "unknown";
|
||||
}
|
||||
const parsed = parseTelegramTarget(target);
|
||||
const chatId = parsed.chatId.trim();
|
||||
if (!chatId) return "unknown";
|
||||
if (!chatId) {
|
||||
return "unknown";
|
||||
}
|
||||
if (/^-?\d+$/.test(chatId)) {
|
||||
return chatId.startsWith("-") ? "group" : "direct";
|
||||
}
|
||||
|
||||
@@ -54,8 +54,12 @@ vi.mock("./bot.js", () => ({
|
||||
const chatId = ctx.message.chat.id;
|
||||
const isGroup = ctx.message.chat.type !== "private";
|
||||
const text = ctx.message.text ?? ctx.message.caption ?? "";
|
||||
if (isGroup && !text.includes("@mybot")) return;
|
||||
if (!text.trim()) return;
|
||||
if (isGroup && !text.includes("@mybot")) {
|
||||
return;
|
||||
}
|
||||
if (!text.trim()) {
|
||||
return;
|
||||
}
|
||||
await api.sendMessage(chatId, `echo:${text}`, { parse_mode: "HTML" });
|
||||
};
|
||||
return {
|
||||
|
||||
@@ -57,7 +57,9 @@ const TELEGRAM_POLL_RESTART_POLICY = {
|
||||
};
|
||||
|
||||
const isGetUpdatesConflict = (err: unknown) => {
|
||||
if (!err || typeof err !== "object") return false;
|
||||
if (!err || typeof err !== "object") {
|
||||
return false;
|
||||
}
|
||||
const typed = err as {
|
||||
error_code?: number;
|
||||
errorCode?: number;
|
||||
@@ -66,7 +68,9 @@ const isGetUpdatesConflict = (err: unknown) => {
|
||||
message?: string;
|
||||
};
|
||||
const errorCode = typed.error_code ?? typed.errorCode;
|
||||
if (errorCode !== 409) return false;
|
||||
if (errorCode !== 409) {
|
||||
return false;
|
||||
}
|
||||
const haystack = [typed.method, typed.description, typed.message]
|
||||
.filter((value): value is string => typeof value === "string")
|
||||
.join(" ")
|
||||
@@ -85,9 +89,13 @@ const NETWORK_ERROR_SNIPPETS = [
|
||||
];
|
||||
|
||||
const isNetworkRelatedError = (err: unknown) => {
|
||||
if (!err) return false;
|
||||
if (!err) {
|
||||
return false;
|
||||
}
|
||||
const message = formatErrorMessage(err).toLowerCase();
|
||||
if (!message) return false;
|
||||
if (!message) {
|
||||
return false;
|
||||
}
|
||||
return NETWORK_ERROR_SNIPPETS.some((snippet) => message.includes(snippet));
|
||||
};
|
||||
|
||||
@@ -111,7 +119,9 @@ export async function monitorTelegramProvider(opts: MonitorTelegramOpts = {}) {
|
||||
accountId: account.accountId,
|
||||
});
|
||||
const persistUpdateId = async (updateId: number) => {
|
||||
if (lastUpdateId !== null && updateId <= lastUpdateId) return;
|
||||
if (lastUpdateId !== null && updateId <= lastUpdateId) {
|
||||
return;
|
||||
}
|
||||
lastUpdateId = updateId;
|
||||
try {
|
||||
await writeTelegramUpdateOffset({
|
||||
@@ -188,7 +198,9 @@ export async function monitorTelegramProvider(opts: MonitorTelegramOpts = {}) {
|
||||
try {
|
||||
await sleepWithAbort(delayMs, opts.abortSignal);
|
||||
} catch (sleepErr) {
|
||||
if (opts.abortSignal?.aborted) return;
|
||||
if (opts.abortSignal?.aborted) {
|
||||
return;
|
||||
}
|
||||
throw sleepErr;
|
||||
}
|
||||
} finally {
|
||||
|
||||
@@ -43,17 +43,27 @@ function normalizeCode(code?: string): string {
|
||||
}
|
||||
|
||||
function getErrorName(err: unknown): string {
|
||||
if (!err || typeof err !== "object") return "";
|
||||
if (!err || typeof err !== "object") {
|
||||
return "";
|
||||
}
|
||||
return "name" in err ? String(err.name) : "";
|
||||
}
|
||||
|
||||
function getErrorCode(err: unknown): string | undefined {
|
||||
const direct = extractErrorCode(err);
|
||||
if (direct) return direct;
|
||||
if (!err || typeof err !== "object") return undefined;
|
||||
if (direct) {
|
||||
return direct;
|
||||
}
|
||||
if (!err || typeof err !== "object") {
|
||||
return undefined;
|
||||
}
|
||||
const errno = (err as { errno?: unknown }).errno;
|
||||
if (typeof errno === "string") return errno;
|
||||
if (typeof errno === "number") return String(errno);
|
||||
if (typeof errno === "string") {
|
||||
return errno;
|
||||
}
|
||||
if (typeof errno === "number") {
|
||||
return String(errno);
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
@@ -64,19 +74,27 @@ function collectErrorCandidates(err: unknown): unknown[] {
|
||||
|
||||
while (queue.length > 0) {
|
||||
const current = queue.shift();
|
||||
if (current == null || seen.has(current)) continue;
|
||||
if (current == null || seen.has(current)) {
|
||||
continue;
|
||||
}
|
||||
seen.add(current);
|
||||
candidates.push(current);
|
||||
|
||||
if (typeof current === "object") {
|
||||
const cause = (current as { cause?: unknown }).cause;
|
||||
if (cause && !seen.has(cause)) queue.push(cause);
|
||||
if (cause && !seen.has(cause)) {
|
||||
queue.push(cause);
|
||||
}
|
||||
const reason = (current as { reason?: unknown }).reason;
|
||||
if (reason && !seen.has(reason)) queue.push(reason);
|
||||
if (reason && !seen.has(reason)) {
|
||||
queue.push(reason);
|
||||
}
|
||||
const errors = (current as { errors?: unknown }).errors;
|
||||
if (Array.isArray(errors)) {
|
||||
for (const nested of errors) {
|
||||
if (nested && !seen.has(nested)) queue.push(nested);
|
||||
if (nested && !seen.has(nested)) {
|
||||
queue.push(nested);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -91,7 +109,9 @@ export function isRecoverableTelegramNetworkError(
|
||||
err: unknown,
|
||||
options: { context?: TelegramNetworkErrorContext; allowMessageMatch?: boolean } = {},
|
||||
): boolean {
|
||||
if (!err) return false;
|
||||
if (!err) {
|
||||
return false;
|
||||
}
|
||||
const allowMessageMatch =
|
||||
typeof options.allowMessageMatch === "boolean"
|
||||
? options.allowMessageMatch
|
||||
@@ -99,10 +119,14 @@ export function isRecoverableTelegramNetworkError(
|
||||
|
||||
for (const candidate of collectErrorCandidates(err)) {
|
||||
const code = normalizeCode(getErrorCode(candidate));
|
||||
if (code && RECOVERABLE_ERROR_CODES.has(code)) return true;
|
||||
if (code && RECOVERABLE_ERROR_CODES.has(code)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const name = getErrorName(candidate);
|
||||
if (name && RECOVERABLE_ERROR_NAMES.has(name)) return true;
|
||||
if (name && RECOVERABLE_ERROR_NAMES.has(name)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (allowMessageMatch) {
|
||||
const message = formatErrorMessage(candidate).toLowerCase();
|
||||
|
||||
@@ -18,8 +18,11 @@ async function withTempStateDir<T>(fn: (stateDir: string) => Promise<T>) {
|
||||
try {
|
||||
return await fn(dir);
|
||||
} finally {
|
||||
if (previous === undefined) delete process.env.OPENCLAW_STATE_DIR;
|
||||
else process.env.OPENCLAW_STATE_DIR = previous;
|
||||
if (previous === undefined) {
|
||||
delete process.env.OPENCLAW_STATE_DIR;
|
||||
} else {
|
||||
process.env.OPENCLAW_STATE_DIR = previous;
|
||||
}
|
||||
await fs.rm(dir, { recursive: true, force: true });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -79,7 +79,9 @@ export async function approveTelegramPairingCode(params: {
|
||||
code: params.code,
|
||||
env: params.env,
|
||||
});
|
||||
if (!res) return null;
|
||||
if (!res) {
|
||||
return null;
|
||||
}
|
||||
const entry = res.entry
|
||||
? {
|
||||
chatId: res.entry.id,
|
||||
|
||||
@@ -77,7 +77,9 @@ function createTelegramHttpLogger(cfg: ReturnType<typeof loadConfig>) {
|
||||
return () => {};
|
||||
}
|
||||
return (label: string, err: unknown) => {
|
||||
if (!(err instanceof HttpError)) return;
|
||||
if (!(err instanceof HttpError)) {
|
||||
return;
|
||||
}
|
||||
const detail = redactSensitiveText(formatUncaughtError(err.error ?? err));
|
||||
diagLogger.warn(`telegram http error (${label}): ${detail}`);
|
||||
};
|
||||
@@ -105,7 +107,9 @@ function resolveTelegramClientOptions(
|
||||
}
|
||||
|
||||
function resolveToken(explicit: string | undefined, params: { accountId: string; token: string }) {
|
||||
if (explicit?.trim()) return explicit.trim();
|
||||
if (explicit?.trim()) {
|
||||
return explicit.trim();
|
||||
}
|
||||
if (!params.token) {
|
||||
throw new Error(
|
||||
`Telegram bot token missing for account "${params.accountId}" (set channels.telegram.accounts.${params.accountId}.botToken/tokenFile or TELEGRAM_BOT_TOKEN for default).`,
|
||||
@@ -116,7 +120,9 @@ function resolveToken(explicit: string | undefined, params: { accountId: string;
|
||||
|
||||
function normalizeChatId(to: string): string {
|
||||
const trimmed = to.trim();
|
||||
if (!trimmed) throw new Error("Recipient is required for Telegram sends");
|
||||
if (!trimmed) {
|
||||
throw new Error("Recipient is required for Telegram sends");
|
||||
}
|
||||
|
||||
// Common internal prefixes that sometimes leak into outbound sends.
|
||||
// - ctx.To uses `telegram:<id>`
|
||||
@@ -128,14 +134,24 @@ function normalizeChatId(to: string): string {
|
||||
const m =
|
||||
/^https?:\/\/t\.me\/([A-Za-z0-9_]+)$/i.exec(normalized) ??
|
||||
/^t\.me\/([A-Za-z0-9_]+)$/i.exec(normalized);
|
||||
if (m?.[1]) normalized = `@${m[1]}`;
|
||||
if (m?.[1]) {
|
||||
normalized = `@${m[1]}`;
|
||||
}
|
||||
|
||||
if (!normalized) throw new Error("Recipient is required for Telegram sends");
|
||||
if (normalized.startsWith("@")) return normalized;
|
||||
if (/^-?\d+$/.test(normalized)) return normalized;
|
||||
if (!normalized) {
|
||||
throw new Error("Recipient is required for Telegram sends");
|
||||
}
|
||||
if (normalized.startsWith("@")) {
|
||||
return normalized;
|
||||
}
|
||||
if (/^-?\d+$/.test(normalized)) {
|
||||
return normalized;
|
||||
}
|
||||
|
||||
// If the user passed a username without `@`, assume they meant a public chat/channel.
|
||||
if (/^[A-Za-z0-9_]{5,}$/i.test(normalized)) return `@${normalized}`;
|
||||
if (/^[A-Za-z0-9_]{5,}$/i.test(normalized)) {
|
||||
return `@${normalized}`;
|
||||
}
|
||||
|
||||
return normalized;
|
||||
}
|
||||
@@ -150,7 +166,9 @@ function normalizeMessageId(raw: string | number): number {
|
||||
throw new Error("Message id is required for Telegram actions");
|
||||
}
|
||||
const parsed = Number.parseInt(value, 10);
|
||||
if (Number.isFinite(parsed)) return parsed;
|
||||
if (Number.isFinite(parsed)) {
|
||||
return parsed;
|
||||
}
|
||||
}
|
||||
throw new Error("Message id is required for Telegram actions");
|
||||
}
|
||||
@@ -158,7 +176,9 @@ function normalizeMessageId(raw: string | number): number {
|
||||
export function buildInlineKeyboard(
|
||||
buttons?: TelegramSendOpts["buttons"],
|
||||
): InlineKeyboardMarkup | undefined {
|
||||
if (!buttons?.length) return undefined;
|
||||
if (!buttons?.length) {
|
||||
return undefined;
|
||||
}
|
||||
const rows = buttons
|
||||
.map((row) =>
|
||||
row
|
||||
@@ -171,7 +191,9 @@ export function buildInlineKeyboard(
|
||||
),
|
||||
)
|
||||
.filter((row) => row.length > 0);
|
||||
if (rows.length === 0) return undefined;
|
||||
if (rows.length === 0) {
|
||||
return undefined;
|
||||
}
|
||||
return { inline_keyboard: rows };
|
||||
}
|
||||
|
||||
@@ -229,7 +251,9 @@ export async function sendMessageTelegram(
|
||||
throw err;
|
||||
});
|
||||
const wrapChatNotFound = (err: unknown) => {
|
||||
if (!/400: Bad Request: chat not found/i.test(formatErrorMessage(err))) return err;
|
||||
if (!/400: Bad Request: chat not found/i.test(formatErrorMessage(err))) {
|
||||
return err;
|
||||
}
|
||||
return new Error(
|
||||
[
|
||||
`Telegram send failed: chat not found (chat_id=${chatId}).`,
|
||||
@@ -690,7 +714,9 @@ export async function sendStickerTelegram(
|
||||
});
|
||||
|
||||
const wrapChatNotFound = (err: unknown) => {
|
||||
if (!/400: Bad Request: chat not found/i.test(formatErrorMessage(err))) return err;
|
||||
if (!/400: Bad Request: chat not found/i.test(formatErrorMessage(err))) {
|
||||
return err;
|
||||
}
|
||||
return new Error(
|
||||
[
|
||||
`Telegram send failed: chat not found (chat_id=${chatId}).`,
|
||||
|
||||
@@ -50,7 +50,9 @@ export function recordSentMessage(chatId: number | string, messageId: number): v
|
||||
export function wasSentByBot(chatId: number | string, messageId: number): boolean {
|
||||
const key = getChatKey(chatId);
|
||||
const entry = sentMessages.get(key);
|
||||
if (!entry) return false;
|
||||
if (!entry) {
|
||||
return false;
|
||||
}
|
||||
// Clean up expired entries on read
|
||||
cleanupExpired(entry);
|
||||
return entry.messageIds.has(messageId);
|
||||
|
||||
@@ -187,7 +187,9 @@ export async function describeStickerImage(params: DescribeStickerParams): Promi
|
||||
(entry) =>
|
||||
entry.provider.toLowerCase() === provider.toLowerCase() && modelSupportsVision(entry),
|
||||
);
|
||||
if (entries.length === 0) return undefined;
|
||||
if (entries.length === 0) {
|
||||
return undefined;
|
||||
}
|
||||
const defaultId =
|
||||
provider === "openai"
|
||||
? "gpt-5-mini"
|
||||
@@ -211,7 +213,9 @@ export async function describeStickerImage(params: DescribeStickerParams): Promi
|
||||
|
||||
if (!resolved) {
|
||||
for (const provider of VISION_PROVIDERS) {
|
||||
if (!(await hasProviderKey(provider))) continue;
|
||||
if (!(await hasProviderKey(provider))) {
|
||||
continue;
|
||||
}
|
||||
const entry = selectCatalogModel(provider);
|
||||
if (entry) {
|
||||
resolved = { provider, model: entry.id };
|
||||
|
||||
@@ -18,7 +18,9 @@ export function stripTelegramInternalPrefixes(to: string): string {
|
||||
}
|
||||
return trimmed;
|
||||
})();
|
||||
if (next === trimmed) return trimmed;
|
||||
if (next === trimmed) {
|
||||
return trimmed;
|
||||
}
|
||||
trimmed = next;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,10 +28,14 @@ export function resolveTelegramToken(
|
||||
// be normalized, so resolve per-account config by matching normalized IDs.
|
||||
const resolveAccountCfg = (id: string): TelegramAccountConfig | undefined => {
|
||||
const accounts = telegramCfg?.accounts;
|
||||
if (!accounts || typeof accounts !== "object" || Array.isArray(accounts)) return undefined;
|
||||
if (!accounts || typeof accounts !== "object" || Array.isArray(accounts)) {
|
||||
return undefined;
|
||||
}
|
||||
// Direct hit (already normalized key)
|
||||
const direct = accounts[id];
|
||||
if (direct) return direct;
|
||||
if (direct) {
|
||||
return direct;
|
||||
}
|
||||
// Fallback: match by normalized key
|
||||
const matchKey = Object.keys(accounts).find((key) => normalizeAccountId(key) === id);
|
||||
return matchKey ? accounts[matchKey] : undefined;
|
||||
|
||||
@@ -13,8 +13,11 @@ async function withTempStateDir<T>(fn: (dir: string) => Promise<T>) {
|
||||
try {
|
||||
return await fn(dir);
|
||||
} finally {
|
||||
if (previous === undefined) delete process.env.OPENCLAW_STATE_DIR;
|
||||
else process.env.OPENCLAW_STATE_DIR = previous;
|
||||
if (previous === undefined) {
|
||||
delete process.env.OPENCLAW_STATE_DIR;
|
||||
} else {
|
||||
process.env.OPENCLAW_STATE_DIR = previous;
|
||||
}
|
||||
await fs.rm(dir, { recursive: true, force: true });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,7 +14,9 @@ type TelegramUpdateOffsetState = {
|
||||
|
||||
function normalizeAccountId(accountId?: string) {
|
||||
const trimmed = accountId?.trim();
|
||||
if (!trimmed) return "default";
|
||||
if (!trimmed) {
|
||||
return "default";
|
||||
}
|
||||
return trimmed.replace(/[^a-z0-9._-]+/gi, "_");
|
||||
}
|
||||
|
||||
@@ -30,7 +32,9 @@ function resolveTelegramUpdateOffsetPath(
|
||||
function safeParseState(raw: string): TelegramUpdateOffsetState | null {
|
||||
try {
|
||||
const parsed = JSON.parse(raw) as TelegramUpdateOffsetState;
|
||||
if (parsed?.version !== STORE_VERSION) return null;
|
||||
if (parsed?.version !== STORE_VERSION) {
|
||||
return null;
|
||||
}
|
||||
if (parsed.lastUpdateId !== null && typeof parsed.lastUpdateId !== "number") {
|
||||
return null;
|
||||
}
|
||||
@@ -51,7 +55,9 @@ export async function readTelegramUpdateOffset(params: {
|
||||
return parsed?.lastUpdateId ?? null;
|
||||
} catch (err) {
|
||||
const code = (err as { code?: string }).code;
|
||||
if (code === "ENOENT") return null;
|
||||
if (code === "ENOENT") {
|
||||
return null;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,8 +12,12 @@ export function resolveTelegramVoiceDecision(opts: {
|
||||
contentType?: string | null;
|
||||
fileName?: string | null;
|
||||
}): { useVoice: boolean; reason?: string } {
|
||||
if (!opts.wantsVoice) return { useVoice: false };
|
||||
if (isTelegramVoiceCompatible(opts)) return { useVoice: true };
|
||||
if (!opts.wantsVoice) {
|
||||
return { useVoice: false };
|
||||
}
|
||||
if (isTelegramVoiceCompatible(opts)) {
|
||||
return { useVoice: true };
|
||||
}
|
||||
const contentType = opts.contentType ?? "unknown";
|
||||
const fileName = opts.fileName ?? "unknown";
|
||||
return {
|
||||
|
||||
@@ -44,7 +44,9 @@ describe("startTelegramWebhook", () => {
|
||||
}),
|
||||
);
|
||||
const address = server.address();
|
||||
if (!address || typeof address === "string") throw new Error("no address");
|
||||
if (!address || typeof address === "string") {
|
||||
throw new Error("no address");
|
||||
}
|
||||
const url = `http://127.0.0.1:${address.port}`;
|
||||
|
||||
const health = await fetch(`${url}/healthz`);
|
||||
@@ -74,7 +76,9 @@ describe("startTelegramWebhook", () => {
|
||||
}),
|
||||
);
|
||||
const addr = server.address();
|
||||
if (!addr || typeof addr === "string") throw new Error("no addr");
|
||||
if (!addr || typeof addr === "string") {
|
||||
throw new Error("no addr");
|
||||
}
|
||||
await fetch(`http://127.0.0.1:${addr.port}/hook`, { method: "POST" });
|
||||
expect(handlerSpy).toHaveBeenCalled();
|
||||
abort.abort();
|
||||
|
||||
@@ -89,7 +89,9 @@ export async function startTelegramWebhook(opts: {
|
||||
});
|
||||
}
|
||||
runtime.log?.(`webhook handler failed: ${errMsg}`);
|
||||
if (!res.headersSent) res.writeHead(500);
|
||||
if (!res.headersSent) {
|
||||
res.writeHead(500);
|
||||
}
|
||||
res.end();
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user