fix(telegram): migrate group config on supergroup IDs (#906)
Thanks @sleontenko. Co-authored-by: Stan <sleontenko@users.noreply.github.com>
This commit is contained in:
@@ -15,6 +15,7 @@
|
||||
- macOS: fix cron preview/testing payload to use `channel` key. (#867) — thanks @wes-davis.
|
||||
- Telegram: honor `channels.telegram.timeoutSeconds` for grammY API requests. (#863) — thanks @Snaver.
|
||||
- Telegram: split long captions into media + follow-up text messages. (#907) - thanks @jalehman.
|
||||
- Telegram: migrate group config when supergroups change chat IDs. (#906) — thanks @sleontenko.
|
||||
- Messaging: unify markdown formatting + format-first chunking for Slack/Telegram/Signal. (#920) — thanks @TheSethRose.
|
||||
- Slack: drop Socket Mode events with mismatched `api_app_id`/`team_id`. (#889) — thanks @roshanasingh4.
|
||||
- Discord: isolate autoThread thread context. (#856) — thanks @davidguttman.
|
||||
|
||||
@@ -7,9 +7,12 @@ import { resolveTelegramForumThreadId } from "./bot/helpers.js";
|
||||
import type { TelegramMessage } from "./bot/types.js";
|
||||
import { firstDefined, isSenderAllowed, normalizeAllowFrom } from "./bot-access.js";
|
||||
import { MEDIA_GROUP_TIMEOUT_MS, type MediaGroupEntry } from "./bot-updates.js";
|
||||
import { migrateTelegramGroupConfig } from "./group-migration.js";
|
||||
import { readTelegramAllowFromStore } from "./pairing-store.js";
|
||||
|
||||
export const registerTelegramHandlers = ({
|
||||
cfg,
|
||||
accountId,
|
||||
bot,
|
||||
opts,
|
||||
runtime,
|
||||
@@ -82,6 +85,7 @@ export const registerTelegramHandlers = ({
|
||||
try {
|
||||
const msg = ctx.message;
|
||||
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);
|
||||
@@ -94,26 +98,28 @@ export const registerTelegramHandlers = ({
|
||||
);
|
||||
|
||||
// Check if old chat ID has config and migrate it
|
||||
const currentConfig = await loadConfig();
|
||||
const telegramGroups = currentConfig.channels?.telegram?.groups;
|
||||
const currentConfig = loadConfig();
|
||||
const migration = migrateTelegramGroupConfig({
|
||||
cfg: currentConfig,
|
||||
accountId,
|
||||
oldChatId,
|
||||
newChatId,
|
||||
});
|
||||
|
||||
if (telegramGroups && telegramGroups[oldChatId]) {
|
||||
const groupConfig = telegramGroups[oldChatId];
|
||||
if (migration.migrated) {
|
||||
runtime.log?.(
|
||||
warn(
|
||||
`[telegram] Migrating group config from ${oldChatId} to ${newChatId}`,
|
||||
),
|
||||
);
|
||||
|
||||
// Copy config to new ID
|
||||
telegramGroups[newChatId] = groupConfig;
|
||||
// Remove old ID
|
||||
delete telegramGroups[oldChatId];
|
||||
|
||||
// Save updated config
|
||||
migrateTelegramGroupConfig({ cfg, accountId, oldChatId, newChatId });
|
||||
await writeConfigFile(currentConfig);
|
||||
runtime.log?.(warn(`[telegram] Group config migrated and saved successfully`));
|
||||
} else if (migration.skippedExisting) {
|
||||
runtime.log?.(
|
||||
warn(`[telegram] Group config migrated and saved successfully`),
|
||||
warn(
|
||||
`[telegram] Group config already exists for ${newChatId}; leaving ${oldChatId} unchanged`,
|
||||
),
|
||||
);
|
||||
} else {
|
||||
runtime.log?.(
|
||||
|
||||
@@ -282,6 +282,8 @@ export function createTelegramBot(opts: TelegramBotOptions) {
|
||||
});
|
||||
|
||||
registerTelegramHandlers({
|
||||
cfg,
|
||||
accountId: account.accountId,
|
||||
bot,
|
||||
opts,
|
||||
runtime,
|
||||
|
||||
113
src/telegram/group-migration.test.ts
Normal file
113
src/telegram/group-migration.test.ts
Normal file
@@ -0,0 +1,113 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { migrateTelegramGroupConfig } from "./group-migration.js";
|
||||
|
||||
describe("migrateTelegramGroupConfig", () => {
|
||||
it("migrates global group ids", () => {
|
||||
const cfg = {
|
||||
channels: {
|
||||
telegram: {
|
||||
groups: {
|
||||
"-123": { requireMention: false },
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const result = migrateTelegramGroupConfig({
|
||||
cfg,
|
||||
accountId: "default",
|
||||
oldChatId: "-123",
|
||||
newChatId: "-100123",
|
||||
});
|
||||
|
||||
expect(result.migrated).toBe(true);
|
||||
expect(cfg.channels.telegram.groups).toEqual({
|
||||
"-100123": { requireMention: false },
|
||||
});
|
||||
});
|
||||
|
||||
it("migrates account-scoped groups", () => {
|
||||
const cfg = {
|
||||
channels: {
|
||||
telegram: {
|
||||
accounts: {
|
||||
primary: {
|
||||
groups: {
|
||||
"-123": { requireMention: true },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const result = migrateTelegramGroupConfig({
|
||||
cfg,
|
||||
accountId: "primary",
|
||||
oldChatId: "-123",
|
||||
newChatId: "-100123",
|
||||
});
|
||||
|
||||
expect(result.migrated).toBe(true);
|
||||
expect(result.scopes).toEqual(["account"]);
|
||||
expect(cfg.channels.telegram.accounts.primary.groups).toEqual({
|
||||
"-100123": { requireMention: true },
|
||||
});
|
||||
});
|
||||
|
||||
it("matches account ids case-insensitively", () => {
|
||||
const cfg = {
|
||||
channels: {
|
||||
telegram: {
|
||||
accounts: {
|
||||
Primary: {
|
||||
groups: {
|
||||
"-123": {},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const result = migrateTelegramGroupConfig({
|
||||
cfg,
|
||||
accountId: "primary",
|
||||
oldChatId: "-123",
|
||||
newChatId: "-100123",
|
||||
});
|
||||
|
||||
expect(result.migrated).toBe(true);
|
||||
expect(cfg.channels.telegram.accounts.Primary.groups).toEqual({
|
||||
"-100123": {},
|
||||
});
|
||||
});
|
||||
|
||||
it("skips migration when new id already exists", () => {
|
||||
const cfg = {
|
||||
channels: {
|
||||
telegram: {
|
||||
groups: {
|
||||
"-123": { requireMention: true },
|
||||
"-100123": { requireMention: false },
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const result = migrateTelegramGroupConfig({
|
||||
cfg,
|
||||
accountId: "default",
|
||||
oldChatId: "-123",
|
||||
newChatId: "-100123",
|
||||
});
|
||||
|
||||
expect(result.migrated).toBe(false);
|
||||
expect(result.skippedExisting).toBe(true);
|
||||
expect(cfg.channels.telegram.groups).toEqual({
|
||||
"-123": { requireMention: true },
|
||||
"-100123": { requireMention: false },
|
||||
});
|
||||
});
|
||||
});
|
||||
84
src/telegram/group-migration.ts
Normal file
84
src/telegram/group-migration.ts
Normal file
@@ -0,0 +1,84 @@
|
||||
import type { ClawdbotConfig } from "../config/config.js";
|
||||
import type { TelegramGroupConfig } from "../config/types.telegram.js";
|
||||
import { normalizeAccountId } from "../routing/session-key.js";
|
||||
|
||||
type TelegramGroups = Record<string, TelegramGroupConfig>;
|
||||
|
||||
type MigrationScope = "account" | "global";
|
||||
|
||||
export type TelegramGroupMigrationResult = {
|
||||
migrated: boolean;
|
||||
skippedExisting: boolean;
|
||||
scopes: MigrationScope[];
|
||||
};
|
||||
|
||||
function resolveAccountGroups(
|
||||
cfg: ClawdbotConfig,
|
||||
accountId?: string | null,
|
||||
): { groups?: TelegramGroups } {
|
||||
if (!accountId) return {};
|
||||
const normalized = normalizeAccountId(accountId);
|
||||
const accounts = cfg.channels?.telegram?.accounts;
|
||||
if (!accounts || typeof accounts !== "object") return {};
|
||||
const exact = accounts[normalized];
|
||||
if (exact?.groups) return { groups: exact.groups };
|
||||
const matchKey = Object.keys(accounts).find(
|
||||
(key) => key.toLowerCase() === normalized.toLowerCase(),
|
||||
);
|
||||
return { groups: matchKey ? accounts[matchKey]?.groups : undefined };
|
||||
}
|
||||
|
||||
export function migrateTelegramGroupsInPlace(
|
||||
groups: TelegramGroups | undefined,
|
||||
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 };
|
||||
groups[newChatId] = groups[oldChatId];
|
||||
delete groups[oldChatId];
|
||||
return { migrated: true, skippedExisting: false };
|
||||
}
|
||||
|
||||
export function migrateTelegramGroupConfig(params: {
|
||||
cfg: ClawdbotConfig;
|
||||
accountId?: string | null;
|
||||
oldChatId: string;
|
||||
newChatId: string;
|
||||
}): TelegramGroupMigrationResult {
|
||||
const scopes: MigrationScope[] = [];
|
||||
let migrated = false;
|
||||
let skippedExisting = false;
|
||||
|
||||
const accountGroups = resolveAccountGroups(params.cfg, params.accountId).groups;
|
||||
if (accountGroups) {
|
||||
const result = migrateTelegramGroupsInPlace(
|
||||
accountGroups,
|
||||
params.oldChatId,
|
||||
params.newChatId,
|
||||
);
|
||||
if (result.migrated) {
|
||||
migrated = true;
|
||||
scopes.push("account");
|
||||
}
|
||||
if (result.skippedExisting) skippedExisting = true;
|
||||
}
|
||||
|
||||
const globalGroups = params.cfg.channels?.telegram?.groups;
|
||||
if (globalGroups) {
|
||||
const result = migrateTelegramGroupsInPlace(
|
||||
globalGroups,
|
||||
params.oldChatId,
|
||||
params.newChatId,
|
||||
);
|
||||
if (result.migrated) {
|
||||
migrated = true;
|
||||
scopes.push("global");
|
||||
}
|
||||
if (result.skippedExisting) skippedExisting = true;
|
||||
}
|
||||
|
||||
return { migrated, skippedExisting, scopes };
|
||||
}
|
||||
Reference in New Issue
Block a user