fix: complete v2.4.0 security isolation
This commit is contained in:
1 parent
81b482ce1e
commit
20fdcd433f
7 files changed
+108
-162
No files matched your search
Vendored
+36
-85
@@ -4431,17 +4431,6 @@ async function buildConsolidatedStatus(singleFiles, batches) {
|
||||
}
|
||||
return lines.join("\n");
|
||||
}
|
||||
function buildCleanupNotice(deletedCount, freedSpace) {
|
||||
return [
|
||||
`\u{1F9F9} **\u7CFB\u7EDF\u542F\u52A8\u6E05\u7406\u5B8C\u6210**`,
|
||||
``,
|
||||
`\u{1F4CA} \u6E05\u7406\u7EDF\u8BA1\uFF1A`,
|
||||
` \u5220\u9664\u5B64\u513F\u6587\u4EF6: ${deletedCount} \u4E2A`,
|
||||
` \u91CA\u653E\u7A7A\u95F4: ${freedSpace}`,
|
||||
``,
|
||||
`\u{1F4A1} \u8FD9\u4E9B\u662F\u4E4B\u524D\u4E0A\u4F20\u5931\u8D25\u6B8B\u7559\u7684\u6587\u4EF6`
|
||||
].join("\n");
|
||||
}
|
||||
|
||||
// src/services/telegramCommands.ts
|
||||
init_telegramState();
|
||||
@@ -4892,9 +4881,6 @@ async function initializeTelegramUserClientPool(credentials) {
|
||||
currentCredentials = credentials;
|
||||
await telegramUserClientPool.initialize(credentials);
|
||||
}
|
||||
async function reloadTelegramUserClientPool() {
|
||||
if (currentCredentials) await telegramUserClientPool.initialize(currentCredentials);
|
||||
}
|
||||
async function listTelegramUserAccounts() {
|
||||
const accounts = await telegramAccountRepository.listAccounts();
|
||||
const ready = new Set(telegramUserClientPool.getReadyAccountIds());
|
||||
@@ -4904,11 +4890,14 @@ async function listTelegramUserAccounts() {
|
||||
activeConnections: telegramUserClientPool.getActiveConnections(account.id)
|
||||
}));
|
||||
}
|
||||
async function upsertTelegramUserAccount(input) {
|
||||
async function upsertTelegramUserAccountWithoutRuntimeRefresh(input) {
|
||||
const account = await telegramAccountRepository.upsertAccount(input);
|
||||
await reloadTelegramUserClientPool();
|
||||
const { session: _session, ...publicAccount } = account;
|
||||
return { ...publicAccount, connected: telegramUserClientPool.getReadyAccountIds().includes(account.id), activeConnections: 0 };
|
||||
return {
|
||||
...publicAccount,
|
||||
connected: telegramUserClientPool.getReadyAccountIds().includes(account.id),
|
||||
activeConnections: telegramUserClientPool.getActiveConnections(account.id)
|
||||
};
|
||||
}
|
||||
async function deleteTelegramUserAccount(accountId) {
|
||||
await telegramUserClientPool.expireAccount(accountId);
|
||||
@@ -6317,7 +6306,7 @@ var TelegramUserWebLoginFlows = class {
|
||||
const sent = await client2.sendCode(credentials, phone);
|
||||
const flowId = crypto10.randomBytes(24).toString("base64url");
|
||||
const expiresAt = this.now() + this.ttlMs;
|
||||
this.flows.set(flowId, { id: flowId, owner, phone, phoneCodeHash: sent.phoneCodeHash, expiresAt, errors: 0, step: "code", client: client2 });
|
||||
this.flows.set(flowId, { id: flowId, owner, phone, phoneCodeHash: sent.phoneCodeHash, expiresAt, errors: 0, step: "code", client: client2, credentials });
|
||||
return { flowId, delivery: sent.isCodeViaApp ? "app" : "sms", expiresAt: new Date(expiresAt).toISOString() };
|
||||
} catch (error) {
|
||||
await this.closeClient(client2);
|
||||
@@ -6365,7 +6354,7 @@ var TelegramUserWebLoginFlows = class {
|
||||
async complete(flow) {
|
||||
try {
|
||||
const account = normalizeAccount2(await flow.client.getMe());
|
||||
await this.deps.persistAndActivate(flow.client.saveSession(), account);
|
||||
await this.deps.persistAndActivate(flow.client.saveSession(), account, flow.credentials);
|
||||
this.flows.delete(flow.id);
|
||||
return { step: "complete", account };
|
||||
} finally {
|
||||
@@ -6576,14 +6565,14 @@ async function activateTelegramUserAccount(accountId) {
|
||||
await telegramUserClientPool.activateAccount(accountId, "explicit_enable", credentials);
|
||||
if (!telegramUserClientPool.getAccountClient(accountId)) throw new Error("Telegram \u7528\u6237\u8D26\u53F7\u8FDE\u63A5\u5931\u8D25");
|
||||
}
|
||||
async function persistAndActivate(session, account) {
|
||||
async function persistAndActivate(session, account, credentials) {
|
||||
await setSettings([
|
||||
[TELEGRAM_USER_SESSION_SETTING, session],
|
||||
[TELEGRAM_USER_ENABLED_SETTING, "true"],
|
||||
[TELEGRAM_USER_ID_SETTING, account.userId],
|
||||
[TELEGRAM_USER_USERNAME_SETTING, account.username || ""]
|
||||
]);
|
||||
await upsertTelegramUserAccount({
|
||||
const persisted = await upsertTelegramUserAccountWithoutRuntimeRefresh({
|
||||
telegramUserId: account.userId,
|
||||
username: account.username,
|
||||
displayName: account.displayName,
|
||||
@@ -6591,7 +6580,8 @@ async function persistAndActivate(session, account) {
|
||||
enabled: true,
|
||||
isLegacy: true
|
||||
});
|
||||
await initTelegramUserClient();
|
||||
await telegramUserClientPool.activateAccount(persisted.id, "login_complete", credentials);
|
||||
if (!telegramUserClientPool.getAccountClient(persisted.id)) throw new Error("Telegram \u7528\u6237\u8D26\u53F7\u8FDE\u63A5\u5931\u8D25");
|
||||
}
|
||||
var GramJsWebLoginClient = class {
|
||||
constructor(client2, credentials) {
|
||||
@@ -17369,7 +17359,6 @@ async function handleBotHomeCallback(update, data) {
|
||||
\u8BF7\u6309\u63D0\u793A\u8F93\u5165\uFF0C\u6216\u8FD4\u56DE\u201C\u66F4\u591A\u529F\u80FD\u201D\u9009\u62E9\u5176\u5B83\u5165\u53E3\u3002` : "\u8FD9\u4E2A\u5165\u53E3\u6682\u65F6\u4E0D\u53EF\u7528\u3002"
|
||||
});
|
||||
}
|
||||
var SESSION_FILE = process.env.TELEGRAM_SESSION_FILE || "./data/telegram_session.txt";
|
||||
var client = null;
|
||||
var digestTimer = null;
|
||||
var botLifecycle = Promise.resolve();
|
||||
@@ -18570,10 +18559,6 @@ async function initTelegramBot(credentialsOverride) {
|
||||
console.error("\u{1F916} Telegram Bot \u540C\u6B65\u5B58\u50A8\u914D\u7F6E\u5931\u8D25:", e);
|
||||
}
|
||||
try {
|
||||
const sessionDir = path16.dirname(SESSION_FILE);
|
||||
if (!fs13.existsSync(sessionDir)) {
|
||||
fs13.mkdirSync(sessionDir, { recursive: true, mode: 448 });
|
||||
}
|
||||
client = new TelegramClient8(new StringSession5(""), apiId, apiHash, {
|
||||
connectionRetries: 5,
|
||||
reconnectRetries: 5,
|
||||
@@ -18588,35 +18573,7 @@ async function initTelegramBot(credentialsOverride) {
|
||||
console.log("\u{1F916} Telegram Bot \u6B63\u5728\u542F\u52A8...");
|
||||
await client.start({ botAuthToken: botToken });
|
||||
await client.getMe();
|
||||
const newSession = client.session.save();
|
||||
fs13.writeFileSync(SESSION_FILE, newSession, { mode: 384 });
|
||||
try {
|
||||
fs13.chmodSync(SESSION_FILE, 384);
|
||||
} catch (e) {
|
||||
console.warn("\u{1F916} \u4FEE\u6B63 Telegram Bot session \u6587\u4EF6\u6743\u9650\u5931\u8D25:", e);
|
||||
}
|
||||
console.log("\u{1F916} Telegram Bot \u5DF2\u8FDE\u63A5!");
|
||||
setYtDlpNotifier(async (task, text) => {
|
||||
if (!client || !task.chatId) return;
|
||||
const kind = /^✅/.test(text) ? "success" : /^❌|^⚠️/.test(text) ? "failure" : "success";
|
||||
const userId = resolveNotificationOwnerUserId(task.ownerUserId, task.chatId);
|
||||
await enqueueTelegramNotification({ userId, chatId: task.chatId, kind, message: text }, {
|
||||
send: async (targetChat, message) => {
|
||||
await client.sendMessage(targetChat, { message });
|
||||
}
|
||||
});
|
||||
});
|
||||
digestTimer = setInterval(() => {
|
||||
if (!client) return;
|
||||
void listTelegramNotificationDigestScopes().then((scopes) => Promise.all(scopes.map(
|
||||
(scope) => flushTelegramNotificationDigest(scope.userId, scope.chatId, {
|
||||
send: async (chatId, message) => {
|
||||
await client.sendMessage(chatId, { message });
|
||||
}
|
||||
}).catch(() => 0)
|
||||
))).catch(() => void 0);
|
||||
}, 6e4);
|
||||
digestTimer.unref?.();
|
||||
try {
|
||||
await query(`
|
||||
CREATE TABLE IF NOT EXISTS telegram_auth (
|
||||
@@ -18634,42 +18591,12 @@ async function initTelegramBot(credentialsOverride) {
|
||||
commands: buildBotCommandMenu().map((command) => new Api9.BotCommand(command))
|
||||
})), 1e4, "Telegram Bot \u547D\u4EE4\u83DC\u5355\u6CE8\u518C\u8D85\u65F6\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5");
|
||||
console.log("\u{1F916} Bot \u547D\u4EE4\u83DC\u5355\u5DF2\u66F4\u65B0");
|
||||
try {
|
||||
const cleanupSetting = await query("SELECT value FROM system_settings WHERE key = $1", ["auto_cleanup_orphans"]);
|
||||
if (cleanupSetting.rows[0]?.value !== void 0) {
|
||||
process.env.AUTO_CLEANUP_ORPHANS = String(cleanupSetting.rows[0].value);
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn("\u{1F9F9} \u8BFB\u53D6\u81EA\u52A8\u6E05\u7406\u8BBE\u7F6E\u5931\u8D25\uFF0C\u4F7F\u7528\u73AF\u5883\u53D8\u91CF\u9ED8\u8BA4\u503C:", e);
|
||||
}
|
||||
try {
|
||||
const fileConcurrency = await loadFileDownloadConcurrencySetting();
|
||||
console.log(`\u{1F916} Telegram \u6587\u4EF6\u7EA7\u5E76\u53D1: ${fileConcurrency}`);
|
||||
} catch (e) {
|
||||
console.warn("\u{1F916} \u8BFB\u53D6\u6587\u4EF6\u7EA7\u5E76\u53D1\u8BBE\u7F6E\u5931\u8D25\uFF0C\u4F7F\u7528\u73AF\u5883\u53D8\u91CF\u9ED8\u8BA4\u503C:", e);
|
||||
}
|
||||
if (isAutoCleanupEnabled()) {
|
||||
try {
|
||||
const stats = await cleanupOrphanFiles();
|
||||
if (stats.deletedCount > 0) {
|
||||
console.log(`\u{1F9F9} \u542F\u52A8\u6E05\u7406: \u5220\u9664\u4E86 ${stats.deletedCount} \u4E2A\u5B64\u513F\u6587\u4EF6\uFF0C\u91CA\u653E ${stats.freedSpace}`);
|
||||
const allowedUsers = await getConfiguredTelegramAllowedUsers();
|
||||
const recipients = allowedUsers.length > 0 ? (await reconcileTelegramAllowedUsers(allowedUsers)).recipients : [];
|
||||
for (const userId of recipients) {
|
||||
try {
|
||||
await client.sendMessage(userId, {
|
||||
message: buildCleanupNotice(stats.deletedCount, stats.freedSpace)
|
||||
});
|
||||
} catch (e) {
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("\u{1F9F9} \u542F\u52A8\u6E05\u7406\u5931\u8D25:", e);
|
||||
}
|
||||
} else {
|
||||
console.log("\u{1F9F9} \u542F\u52A8\u5B64\u513F\u6E05\u7406\u5DF2\u8DF3\u8FC7\uFF1AAUTO_CLEANUP_ORPHANS=false");
|
||||
}
|
||||
client.addEventHandler(async (event) => {
|
||||
if (!client) return;
|
||||
try {
|
||||
@@ -19314,8 +19241,32 @@ ${buildPathPreviewLine(appliedPath.folder)}
|
||||
displayName: [me?.firstName, me?.lastName].filter(Boolean).join(" ") || null
|
||||
});
|
||||
markTelegramBotReady();
|
||||
setYtDlpNotifier(async (task, text) => {
|
||||
if (!client || !task.chatId) return;
|
||||
const kind = /^✅/.test(text) ? "success" : /^❌|^⚠️/.test(text) ? "failure" : "success";
|
||||
const userId = resolveNotificationOwnerUserId(task.ownerUserId, task.chatId);
|
||||
await enqueueTelegramNotification({ userId, chatId: task.chatId, kind, message: text }, {
|
||||
send: async (targetChat, message) => {
|
||||
await client.sendMessage(targetChat, { message });
|
||||
}
|
||||
});
|
||||
});
|
||||
digestTimer = setInterval(() => {
|
||||
if (!client) return;
|
||||
void listTelegramNotificationDigestScopes().then((scopes) => Promise.all(scopes.map(
|
||||
(scope) => flushTelegramNotificationDigest(scope.userId, scope.chatId, {
|
||||
send: async (chatId, message) => {
|
||||
await client.sendMessage(chatId, { message });
|
||||
}
|
||||
}).catch(() => 0)
|
||||
))).catch(() => void 0);
|
||||
}, 6e4);
|
||||
digestTimer.unref?.();
|
||||
console.log("\u{1F916} Telegram Bot \u542F\u52A8\u6210\u529F! (\u6700\u5927 2GB\uFF0C\u8D26\u53F7\u7EA7\u4E0B\u8F7D\u5668\u4E0D\u53D7\u6B64\u9650\u5236)");
|
||||
} catch (error) {
|
||||
if (digestTimer) clearInterval(digestTimer);
|
||||
digestTimer = null;
|
||||
setYtDlpNotifier(null);
|
||||
const failedClient = client;
|
||||
client = null;
|
||||
if (failedClient) {
|
||||
|
||||
@@ -31,8 +31,7 @@ import {
|
||||
retryTelegramBackgroundJob,
|
||||
TELEGRAM_COMMENTS_MAX_PER_POST,
|
||||
} from './telegramChannelJobs.js';
|
||||
import { cleanupOrphanFiles, isAutoCleanupEnabled } from './orphanCleanup.js';
|
||||
import { MSG, buildStartPrompt, buildAuthSuccess, build2FASetupCaption, buildCleanupNotice } from '../utils/telegramMessages.js';
|
||||
import { MSG, buildStartPrompt, buildAuthSuccess, build2FASetupCaption } from '../utils/telegramMessages.js';
|
||||
import { query } from '../db/index.js';
|
||||
import { getConfiguredTelegramAllowedUsers, addTelegramAllowedUser, countAuthenticatedTelegramUsers, shouldAutoAllowFirstTelegramUser, verifyTelegramPin } from '../utils/authSettings.js';
|
||||
import { assertPublicHttpUrl } from '../utils/networkSecurity.js';
|
||||
@@ -161,9 +160,6 @@ async function handleBotHomeCallback(update: Api.UpdateBotCallbackQuery, data: s
|
||||
});
|
||||
}
|
||||
|
||||
// Session File Path
|
||||
const SESSION_FILE = process.env.TELEGRAM_SESSION_FILE || './data/telegram_session.txt';
|
||||
|
||||
// GramJS Client
|
||||
let client: TelegramClient | null = null;
|
||||
let digestTimer: NodeJS.Timeout | null = null;
|
||||
@@ -1541,11 +1537,6 @@ export async function initTelegramBot(credentialsOverride?: TelegramBotCredentia
|
||||
}
|
||||
|
||||
try {
|
||||
const sessionDir = path.dirname(SESSION_FILE);
|
||||
if (!fs.existsSync(sessionDir)) {
|
||||
fs.mkdirSync(sessionDir, { recursive: true, mode: 0o700 });
|
||||
}
|
||||
|
||||
client = new TelegramClient(new StringSession(''), apiId, apiHash, {
|
||||
connectionRetries: 5,
|
||||
reconnectRetries: 5,
|
||||
@@ -1562,28 +1553,7 @@ export async function initTelegramBot(credentialsOverride?: TelegramBotCredentia
|
||||
await client.start({ botAuthToken: botToken });
|
||||
await client.getMe();
|
||||
|
||||
const newSession = client.session.save() as unknown as string;
|
||||
fs.writeFileSync(SESSION_FILE, newSession, { mode: 0o600 });
|
||||
try { fs.chmodSync(SESSION_FILE, 0o600); } catch (e) { console.warn('🤖 修正 Telegram Bot session 文件权限失败:', e); }
|
||||
|
||||
console.log('🤖 Telegram Bot 已连接!');
|
||||
setYtDlpNotifier(async (task, text) => {
|
||||
if (!client || !task.chatId) return;
|
||||
const kind = /^✅/.test(text) ? 'success' : /^❌|^⚠️/.test(text) ? 'failure' : 'success';
|
||||
const userId = resolveNotificationOwnerUserId(task.ownerUserId, task.chatId);
|
||||
await enqueueTelegramNotification({ userId, chatId: task.chatId, kind, message: text }, {
|
||||
send: async (targetChat, message) => { await client!.sendMessage(targetChat, { message }); },
|
||||
});
|
||||
});
|
||||
digestTimer = setInterval(() => {
|
||||
if (!client) return;
|
||||
void listTelegramNotificationDigestScopes().then(scopes => Promise.all(scopes.map(scope =>
|
||||
flushTelegramNotificationDigest(scope.userId, scope.chatId, {
|
||||
send: async (chatId, message) => { await client!.sendMessage(chatId, { message }); },
|
||||
}).catch(() => 0),
|
||||
))).catch(() => undefined);
|
||||
}, 60_000);
|
||||
digestTimer.unref?.();
|
||||
|
||||
// Ensure database table exists
|
||||
try {
|
||||
@@ -1606,15 +1576,6 @@ export async function initTelegramBot(credentialsOverride?: TelegramBotCredentia
|
||||
})), 10_000, 'Telegram Bot 命令菜单注册超时,请稍后重试');
|
||||
console.log('🤖 Bot 命令菜单已更新');
|
||||
|
||||
try {
|
||||
const cleanupSetting = await query('SELECT value FROM system_settings WHERE key = $1', ['auto_cleanup_orphans']);
|
||||
if (cleanupSetting.rows[0]?.value !== undefined) {
|
||||
process.env.AUTO_CLEANUP_ORPHANS = String(cleanupSetting.rows[0].value);
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('🧹 读取自动清理设置失败,使用环境变量默认值:', e);
|
||||
}
|
||||
|
||||
try {
|
||||
const fileConcurrency = await loadFileDownloadConcurrencySetting();
|
||||
console.log(`🤖 Telegram 文件级并发: ${fileConcurrency}`);
|
||||
@@ -1622,35 +1583,6 @@ export async function initTelegramBot(credentialsOverride?: TelegramBotCredentia
|
||||
console.warn('🤖 读取文件级并发设置失败,使用环境变量默认值:', e);
|
||||
}
|
||||
|
||||
// 启动时清理孤儿文件(默认开启,可通过 /cleanup_settings 关闭)
|
||||
if (isAutoCleanupEnabled()) {
|
||||
try {
|
||||
const stats = await cleanupOrphanFiles();
|
||||
if (stats.deletedCount > 0) {
|
||||
console.log(`🧹 启动清理: 删除了 ${stats.deletedCount} 个孤儿文件,释放 ${stats.freedSpace}`);
|
||||
|
||||
// 广播收件人始终与当前 allowlist 收敛。
|
||||
const allowedUsers = await getConfiguredTelegramAllowedUsers();
|
||||
const recipients = allowedUsers.length > 0
|
||||
? (await reconcileTelegramAllowedUsers(allowedUsers)).recipients
|
||||
: [];
|
||||
for (const userId of recipients) {
|
||||
try {
|
||||
await client.sendMessage(userId, {
|
||||
message: buildCleanupNotice(stats.deletedCount, stats.freedSpace)
|
||||
});
|
||||
} catch (e) {
|
||||
// 用户可能已删除对话或阻止了 Bot
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('🧹 启动清理失败:', e);
|
||||
}
|
||||
} else {
|
||||
console.log('🧹 启动孤儿清理已跳过:AUTO_CLEANUP_ORPHANS=false');
|
||||
}
|
||||
|
||||
// 后台任务由应用级编排显式启动;Bot 凭证绑定只安装交互处理器,
|
||||
// 避免仅保存凭证就触发订阅扫描、任务恢复或主动消息。
|
||||
|
||||
@@ -2403,9 +2335,29 @@ export async function initTelegramBot(credentialsOverride?: TelegramBotCredentia
|
||||
displayName: [me?.firstName, me?.lastName].filter(Boolean).join(' ') || null,
|
||||
});
|
||||
markTelegramBotReady();
|
||||
setYtDlpNotifier(async (task, text) => {
|
||||
if (!client || !task.chatId) return;
|
||||
const kind = /^✅/.test(text) ? 'success' : /^❌|^⚠️/.test(text) ? 'failure' : 'success';
|
||||
const userId = resolveNotificationOwnerUserId(task.ownerUserId, task.chatId);
|
||||
await enqueueTelegramNotification({ userId, chatId: task.chatId, kind, message: text }, {
|
||||
send: async (targetChat, message) => { await client!.sendMessage(targetChat, { message }); },
|
||||
});
|
||||
});
|
||||
digestTimer = setInterval(() => {
|
||||
if (!client) return;
|
||||
void listTelegramNotificationDigestScopes().then(scopes => Promise.all(scopes.map(scope =>
|
||||
flushTelegramNotificationDigest(scope.userId, scope.chatId, {
|
||||
send: async (chatId, message) => { await client!.sendMessage(chatId, { message }); },
|
||||
}).catch(() => 0),
|
||||
))).catch(() => undefined);
|
||||
}, 60_000);
|
||||
digestTimer.unref?.();
|
||||
console.log('🤖 Telegram Bot 启动成功! (最大 2GB,账号级下载器不受此限制)');
|
||||
|
||||
} catch (error) {
|
||||
if (digestTimer) clearInterval(digestTimer);
|
||||
digestTimer = null;
|
||||
setYtDlpNotifier(null);
|
||||
const failedClient = client;
|
||||
client = null;
|
||||
if (failedClient) {
|
||||
|
||||
@@ -46,7 +46,8 @@ test('Bot migrate, disable and delete routes remain isolated from Telegram user-
|
||||
|
||||
test('Bot replacement avoids stale saved sessions and requires full activation before ready', () => {
|
||||
assert.doesNotMatch(bot, /fs\.readFileSync\(SESSION_FILE/);
|
||||
const startupSection = bot.slice(bot.indexOf("console.log('🤖 Telegram Bot 正在启动...')"), bot.indexOf('const newSession = client.session.save()'));
|
||||
assert.doesNotMatch(bot, /session\.save\(\)[\s\S]*writeFileSync\(SESSION_FILE/);
|
||||
const startupSection = bot.slice(bot.indexOf("console.log('🤖 Telegram Bot 正在启动...')"), bot.indexOf("console.log('🤖 Telegram Bot 已连接!')"));
|
||||
assert.doesNotMatch(startupSection, /Promise\.race|withTelegramClientDeadline/);
|
||||
assert.match(bot, /new TelegramClient\(new StringSession\(''\)/);
|
||||
assert.match(bot, /await client\.start[\s\S]*SetBotCommands[\s\S]*addEventHandler[\s\S]*markTelegramBotReady\(\)/);
|
||||
@@ -55,6 +56,20 @@ test('Bot replacement avoids stale saved sessions and requires full activation b
|
||||
assert.match(bot, /catch \(error\) \{[\s\S]*const failedClient = client;[\s\S]*client = null;[\s\S]*failedClient\.disconnect/);
|
||||
});
|
||||
|
||||
test('Bot startup defers global notifier and timer effects until ready and rolls them back on failure', () => {
|
||||
const startup = bot.slice(bot.indexOf('export async function initTelegramBot'), bot.indexOf('async function withTelegramClientDeadline'));
|
||||
const readyIndex = startup.indexOf('markTelegramBotReady()');
|
||||
assert.ok(readyIndex >= 0);
|
||||
assert.ok(startup.indexOf('setYtDlpNotifier(', readyIndex) > readyIndex);
|
||||
assert.ok(startup.indexOf('digestTimer = setInterval(', readyIndex) > readyIndex);
|
||||
assert.match(startup, /catch \(error\) \{[\s\S]*clearInterval\(digestTimer\)[\s\S]*setYtDlpNotifier\(null\)/);
|
||||
});
|
||||
|
||||
test('Bot startup never performs orphan cleanup or sends cleanup notifications', () => {
|
||||
const startup = bot.slice(bot.indexOf('export async function initTelegramBot'), bot.indexOf('async function withTelegramClientDeadline'));
|
||||
assert.doesNotMatch(startup, /cleanupOrphanFiles\(|buildCleanupNotice|启动清理/);
|
||||
});
|
||||
|
||||
test('client teardown waits for in-flight subscription and recovery work', () => {
|
||||
assert.match(jobs, /while \(subscriptionScanRunning \|\| recoveryRunning\)/);
|
||||
assert.match(jobs, /拒绝切换 Bot 客户端/);
|
||||
|
||||
@@ -11,7 +11,7 @@ import {
|
||||
deleteTelegramUserAccount,
|
||||
listTelegramUserAccounts,
|
||||
telegramUserClientPool,
|
||||
upsertTelegramUserAccount,
|
||||
upsertTelegramUserAccountWithoutRuntimeRefresh,
|
||||
} from './telegramUserClientPool.js';
|
||||
import { initializeTelegramMultiAccountRuntime } from './telegramMultiAccountRuntime.js';
|
||||
|
||||
@@ -142,14 +142,18 @@ export async function activateTelegramUserAccount(accountId: string): Promise<vo
|
||||
if (!telegramUserClientPool.getAccountClient(accountId)) throw new Error('Telegram 用户账号连接失败');
|
||||
}
|
||||
|
||||
async function persistAndActivate(session: string, account: TelegramUserLoginAccount): Promise<void> {
|
||||
async function persistAndActivate(
|
||||
session: string,
|
||||
account: TelegramUserLoginAccount,
|
||||
credentials: { apiId: number; apiHash: string },
|
||||
): Promise<void> {
|
||||
await setSettings([
|
||||
[TELEGRAM_USER_SESSION_SETTING, session],
|
||||
[TELEGRAM_USER_ENABLED_SETTING, 'true'],
|
||||
[TELEGRAM_USER_ID_SETTING, account.userId],
|
||||
[TELEGRAM_USER_USERNAME_SETTING, account.username || ''],
|
||||
]);
|
||||
await upsertTelegramUserAccount({
|
||||
const persisted = await upsertTelegramUserAccountWithoutRuntimeRefresh({
|
||||
telegramUserId: account.userId,
|
||||
username: account.username,
|
||||
displayName: account.displayName,
|
||||
@@ -157,7 +161,8 @@ async function persistAndActivate(session: string, account: TelegramUserLoginAcc
|
||||
enabled: true,
|
||||
isLegacy: true,
|
||||
});
|
||||
await initTelegramUserClient();
|
||||
await telegramUserClientPool.activateAccount(persisted.id, 'login_complete', credentials);
|
||||
if (!telegramUserClientPool.getAccountClient(persisted.id)) throw new Error('Telegram 用户账号连接失败');
|
||||
}
|
||||
|
||||
class GramJsWebLoginClient implements TelegramUserLoginClient {
|
||||
|
||||
@@ -307,6 +307,18 @@ export async function upsertTelegramUserAccount(input: Parameters<TelegramAccoun
|
||||
return { ...publicAccount, connected: telegramUserClientPool.getReadyAccountIds().includes(account.id), activeConnections: 0 };
|
||||
}
|
||||
|
||||
export async function upsertTelegramUserAccountWithoutRuntimeRefresh(
|
||||
input: Parameters<TelegramAccountRepository['upsertAccount']>[0],
|
||||
): Promise<PublicTelegramUserAccount> {
|
||||
const account = await telegramAccountRepository.upsertAccount(input);
|
||||
const { session: _session, ...publicAccount } = account;
|
||||
return {
|
||||
...publicAccount,
|
||||
connected: telegramUserClientPool.getReadyAccountIds().includes(account.id),
|
||||
activeConnections: telegramUserClientPool.getActiveConnections(account.id),
|
||||
};
|
||||
}
|
||||
|
||||
export async function setTelegramUserAccountEnabled(accountId: string, enabled: boolean): Promise<boolean> {
|
||||
const changed = await telegramAccountRepository.setEnabled(accountId, enabled);
|
||||
if (changed) await reloadTelegramUserClientPool();
|
||||
|
||||
@@ -26,6 +26,7 @@ interface Flow<C> {
|
||||
errors: number;
|
||||
step: 'code' | 'password';
|
||||
client: C;
|
||||
credentials: { apiId: number; apiHash: string };
|
||||
}
|
||||
|
||||
export type TelegramUserLoginFlowErrorCode =
|
||||
@@ -64,7 +65,7 @@ export class TelegramUserWebLoginFlows<C extends TelegramUserLoginClient> {
|
||||
constructor(private readonly deps: {
|
||||
credentials(): Promise<{ apiId: number; apiHash: string } | null>;
|
||||
createClient(credentials: { apiId: number; apiHash: string }): C;
|
||||
persistAndActivate(session: string, account: TelegramUserLoginAccount): Promise<void>;
|
||||
persistAndActivate(session: string, account: TelegramUserLoginAccount, credentials: { apiId: number; apiHash: string }): Promise<void>;
|
||||
now?: () => number;
|
||||
ttlMs?: number;
|
||||
maxErrors?: number;
|
||||
@@ -86,7 +87,7 @@ export class TelegramUserWebLoginFlows<C extends TelegramUserLoginClient> {
|
||||
const sent = await client.sendCode(credentials, phone);
|
||||
const flowId = crypto.randomBytes(24).toString('base64url');
|
||||
const expiresAt = this.now() + this.ttlMs;
|
||||
this.flows.set(flowId, { id: flowId, owner, phone, phoneCodeHash: sent.phoneCodeHash, expiresAt, errors: 0, step: 'code', client });
|
||||
this.flows.set(flowId, { id: flowId, owner, phone, phoneCodeHash: sent.phoneCodeHash, expiresAt, errors: 0, step: 'code', client, credentials });
|
||||
return { flowId, delivery: sent.isCodeViaApp ? 'app' : 'sms', expiresAt: new Date(expiresAt).toISOString() };
|
||||
} catch (error) {
|
||||
await this.closeClient(client);
|
||||
@@ -138,7 +139,7 @@ export class TelegramUserWebLoginFlows<C extends TelegramUserLoginClient> {
|
||||
private async complete(flow: Flow<C>): Promise<{ step: 'complete'; account: TelegramUserLoginAccount }> {
|
||||
try {
|
||||
const account = normalizeAccount(await flow.client.getMe());
|
||||
await this.deps.persistAndActivate(flow.client.saveSession(), account);
|
||||
await this.deps.persistAndActivate(flow.client.saveSession(), account, flow.credentials);
|
||||
this.flows.delete(flow.id);
|
||||
return { step: 'complete', account };
|
||||
} finally {
|
||||
|
||||
@@ -39,6 +39,16 @@ test('explicit legacy enable requires a saved user session before initializing t
|
||||
assert.match(runtimeSource, /installTelegramMultiAccountRuntimeAdapters[\s\S]*initializeTelegramUserClientPool/);
|
||||
});
|
||||
|
||||
test('legacy login completion activates only the just-authorized account', () => {
|
||||
const persist = client.slice(
|
||||
client.indexOf('async function persistAndActivate'),
|
||||
client.indexOf('class GramJsWebLoginClient'),
|
||||
);
|
||||
assert.match(persist, /activateAccount\(persisted\.id, 'login_complete', credentials\)/);
|
||||
assert.match(persist, /upsertTelegramUserAccountWithoutRuntimeRefresh/);
|
||||
assert.doesNotMatch(persist, /initTelegramUserClient\(|initializeTelegramMultiAccountRuntime\(|reloadTelegramUserClientPool/);
|
||||
});
|
||||
|
||||
test('disable retains the session, unlink deletes it, and both apply without a restart', () => {
|
||||
assert.match(route, /router\.post\('\/config\/telegram-user\/disable'/);
|
||||
assert.match(route, /disableTelegramUserAccount/);
|
||||
|
||||
Reference in new issue
Block a user