chore: Enable "curly" rule to avoid single-statement if confusion/errors.

This commit is contained in:
cpojer
2026-01-31 16:19:20 +09:00
parent 009b16fab8
commit 5ceff756e1
1266 changed files with 27871 additions and 9393 deletions

View File

@@ -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;
}

View File

@@ -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);
}

View File

@@ -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 {

View File

@@ -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}`,
);

View File

@@ -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";

View File

@@ -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) {

View File

@@ -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",

View File

@@ -46,7 +46,9 @@ export const createTelegramMessageProcessor = (deps) => {
resolveGroupRequireMention,
resolveTelegramGroupConfig,
});
if (!context) return;
if (!context) {
return;
}
await dispatchTelegramMessage({
context,
bot,

View File

@@ -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;

View File

@@ -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;

View File

@@ -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>;
};

View File

@@ -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: {

View File

@@ -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>;
};

View File

@@ -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>;
};

View File

@@ -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>;
};

View File

@@ -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>;
};

View File

@@ -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>;
};

View File

@@ -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>;
};

View File

@@ -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>;
};

View File

@@ -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: {

View File

@@ -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

View File

@@ -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 };
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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();

View File

@@ -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");

View File

@@ -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 });
}

View File

@@ -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 };

View File

@@ -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";
}

View File

@@ -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 {

View File

@@ -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 {

View File

@@ -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();

View File

@@ -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 });
}
}

View File

@@ -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,

View File

@@ -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}).`,

View File

@@ -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);

View File

@@ -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 };

View File

@@ -18,7 +18,9 @@ export function stripTelegramInternalPrefixes(to: string): string {
}
return trimmed;
})();
if (next === trimmed) return trimmed;
if (next === trimmed) {
return trimmed;
}
trimmed = next;
}
}

View File

@@ -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;

View File

@@ -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 });
}
}

View File

@@ -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;
}
}

View File

@@ -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 {

View File

@@ -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();

View File

@@ -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();
});
}