mirror of
https://github.com/Trollobot/Telemax.git
synced 2026-10-21 05:29:59 +00:00
1.3.11-beta.1: GitHub #3 — в группе MAX (type CHAT) имя автора «👤 Имя:» всегда: в списке чатов участники большой группы бывают неполными, и при двух видимых группа считалась личкой — имена пропадали (AlexHighTower, 1.3.7); GitHub #2 — сообщение, доставленное позже 5 минут (заливка истории, догрузка после простоя), получает в конце «🕓 10.09.2026, 11:27» — время отправки в MAX, по TZ (МСК по умолчанию); BACKFILL_TIME=off выключает (Mackey-way)
This commit is contained in:
parent
df6c4afa4e
commit
c25d43115d
6 changed files with 79 additions and 7 deletions
|
|
@ -14,6 +14,11 @@ MAX_SESSION_KEY=
|
|||
# Telegram пускает ~20 сообщений в минуту: чат на 5 000 сообщений иначе заливается около 4 часов.
|
||||
# HISTORY_LIMIT=300
|
||||
|
||||
# Сообщение, доставленное с опозданием больше 5 минут (заливка истории, догрузка после простоя),
|
||||
# получает в конце строку «🕓 10.09.2026, 11:27» — когда его на самом деле отправили в MAX.
|
||||
# Время — по TZ (по умолчанию московское). Выключить: BACKFILL_TIME=off
|
||||
# BACKFILL_TIME=off
|
||||
|
||||
# Опционально: версия десктопного MAX, которой представляется мост. Нужна, только если при /login
|
||||
# MAX отвечает «Приложение устарело» (см. README, «Если что-то не работает»). Пусто — встроенная.
|
||||
# MAX_APP_VERSION=26.35.2
|
||||
|
|
|
|||
|
|
@ -112,7 +112,7 @@ curl -fsSL https://raw.githubusercontent.com/Trollobot/Telemax/main/install.sh |
|
|||
|
||||
После успешной авторизации сессия сохраняется зашифрованной в `./data` — повторной авторизации при перезапуске контейнера не требуется. Повторный вход тем же путём (`/login`) нужен только после `/kill`, смены номера или если MAX отклонил сессию (бот сам напишет об этом в группу).
|
||||
|
||||
После входа мост переносит историю чатов в темы. Telegram разрешает боту писать в группу примерно 20 сообщений в минуту, поэтому большая история заливается долго: чат на 5 000 сообщений — около 4 часов (новые сообщения при этом приходят сразу). Установщик спрашивает, сколько последних сообщений каждого чата переносить; то же — `HISTORY_LIMIT=300` в `.env` (затем `docker compose up -d --force-recreate`). Тема, куда попала не вся история, начинается с пометки об этом.
|
||||
После входа мост переносит историю чатов в темы. Telegram разрешает боту писать в группу примерно 20 сообщений в минуту, поэтому большая история заливается долго: чат на 5 000 сообщений — около 4 часов (новые сообщения при этом приходят сразу). Установщик спрашивает, сколько последних сообщений каждого чата переносить; то же — `HISTORY_LIMIT=300` в `.env` (затем `docker compose up -d --force-recreate`). Тема, куда попала не вся история, начинается с пометки об этом. У сообщений из истории и у всех, что пришли с опозданием больше 5 минут, в конце стоит время отправки в MAX: «🕓 10.09.2026, 11:27» (выключить — `BACKFILL_TIME=off` в `.env`).
|
||||
|
||||
В личке бот работает только для входа в MAX (`/login`); все остальные команды — в группе моста. Если администратор напишет в личку что-то другое, бот подскажет, где пульт.
|
||||
|
||||
|
|
|
|||
4
package-lock.json
generated
4
package-lock.json
generated
|
|
@ -1,12 +1,12 @@
|
|||
{
|
||||
"name": "telemax",
|
||||
"version": "1.3.10",
|
||||
"version": "1.3.11-beta.1",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "telemax",
|
||||
"version": "1.3.10",
|
||||
"version": "1.3.11-beta.1",
|
||||
"license": "AGPL-3.0-only",
|
||||
"dependencies": {
|
||||
"dotenv": "^17.2.3",
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
"name": "telemax",
|
||||
"private": true,
|
||||
"license": "AGPL-3.0-only",
|
||||
"version": "1.3.10",
|
||||
"version": "1.3.11-beta.1",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "tsx watch src/server/app.ts",
|
||||
|
|
|
|||
|
|
@ -919,6 +919,36 @@ export function historyLimitNotice(limit: number, channel = false): string {
|
|||
return `📜 Здесь ${phrase} этого чата — так задано при установке (HISTORY_LIMIT в .env). Более ранняя переписка осталась в MAX.`;
|
||||
}
|
||||
|
||||
/** A catch-up also delivers the live message that set it off: only a message at least this old at delivery is «late». */
|
||||
const LATE_MIN_AGE_MS = 5 * 60_000;
|
||||
|
||||
/**
|
||||
* «🕓 10.09.2026, 11:27» — the MAX time of a message delivered late (a history fill, a catch-up
|
||||
* after downtime), in TZ (МСК by default); undefined for a fresh one or with BACKFILL_TIME=off.
|
||||
* Otherwise a filled history reads as all sent at the fill (GitHub #2, Mackey-way, 2026-10-13).
|
||||
* Pure + exported for unit testing.
|
||||
*/
|
||||
export function lateTimeStamp(time: unknown, env: NodeJS.ProcessEnv = process.env, now: number = Date.now()): string | undefined {
|
||||
if ((env.BACKFILL_TIME ?? '').trim().toLowerCase() === 'off') return undefined;
|
||||
const ms = Number(time);
|
||||
if (!Number.isFinite(ms) || ms <= 0 || now - ms < LATE_MIN_AGE_MS) return undefined;
|
||||
const when = new Date(ms).toLocaleString('ru-RU', { day: '2-digit', month: '2-digit', year: 'numeric', hour: '2-digit', minute: '2-digit', timeZone: env.TZ || 'Europe/Moscow' });
|
||||
return `🕓 ${when}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether a chat's messages carry «👤 Имя:». A group always, by MAX's own type: a big group's
|
||||
* `participants` in the chat list can be partial, and two in it hid every author (GitHub #3,
|
||||
* AlexHighTower, on 1.3.7). A 1:1 never — the topic IS the contact. Anything else (a channel, a
|
||||
* chat without a type) by its participants, as before. Pure + exported for unit testing.
|
||||
*/
|
||||
export function showsAuthor(chat: unknown): boolean {
|
||||
const c = chat as { type?: unknown; participants?: Record<string, unknown> } | null | undefined;
|
||||
if (c?.type === 'CHAT') return true;
|
||||
if (c?.type === 'DIALOG') return false;
|
||||
return !!c?.participants && Object.keys(c.participants).length > 2;
|
||||
}
|
||||
|
||||
/** HISTORY_LIMIT from .env: how many of a chat's newest messages a topic filled from scratch gets (0 = all). */
|
||||
export function historyLimitFromEnv(env: NodeJS.ProcessEnv = process.env): number {
|
||||
const n = Number((env.HISTORY_LIMIT ?? '').trim());
|
||||
|
|
@ -1038,8 +1068,7 @@ async function resolveAuthorPrefix(
|
|||
const id = typeof senderId === 'number' ? senderId : Number(senderId);
|
||||
if (Number.isNaN(id)) return '';
|
||||
if (myAccountId != null && id === myAccountId) return '🧑 Вы:\n';
|
||||
const participants = (chat as { participants?: Record<string, unknown> } | null)?.participants;
|
||||
if (!participants || Object.keys(participants).length <= 2) return ''; // 1:1 (or unknown) — contact's line stays plain
|
||||
if (!showsAuthor(chat)) return ''; // 1:1 (or unknown) — contact's line stays plain
|
||||
const profile = await resolveProfile(max, id, 'group sender', profiles);
|
||||
return `👤 ${resolveContactDisplayName(id, profile)}:\n`;
|
||||
}
|
||||
|
|
@ -1325,6 +1354,10 @@ export class ChatCatchUp {
|
|||
const authorPrefix = await resolveAuthorPrefix(chat, (msg as { sender?: unknown }).sender, myAccountId, max, profiles);
|
||||
if (authorPrefix) text = text ? `${authorPrefix}${text}` : authorPrefix;
|
||||
}
|
||||
// Delivered late: its MAX time at the end (lateTimeStamp). A bare photo of a 1:1 gets none — a
|
||||
// text message of its own for just the time would be noise.
|
||||
const stamp = lateTimeStamp(msg.time);
|
||||
if (stamp && text) text = `${text}\n${stamp}`;
|
||||
const abortKey = `${String(chatId)}:${String(msg.id ?? msg.time)}`;
|
||||
const lastTry = this.strikes.isLastTry(abortKey);
|
||||
const sent: number[] = [];
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import type { Telegraf } from 'telegraf';
|
|||
import type { MaxClient, MaxHistoryMessage } from '../src/max/client.js';
|
||||
import { ChatMapStore } from '../src/store/chatMapStore.js';
|
||||
import { ChatBannedError, StrikeCounter } from '../src/bridge/catchUp.js';
|
||||
import { ChatCatchUp, MessageLinkStore, historyLimitFromEnv, historyLimitNotice, isLeftChat, syncAllChatsToTelegram } from '../src/bridge/sync.js';
|
||||
import { ChatCatchUp, MessageLinkStore, historyLimitFromEnv, historyLimitNotice, isLeftChat, lateTimeStamp, showsAuthor, syncAllChatsToTelegram } from '../src/bridge/sync.js';
|
||||
|
||||
// One writer per chat, catch-up on first touch, and the cursor gate — see ChatCatchUp in sync.ts.
|
||||
|
||||
|
|
@ -16,9 +16,12 @@ let store: ChatMapStore;
|
|||
beforeEach(async () => {
|
||||
dir = await mkdtemp(path.join(tmpdir(), 'telemax-catchup-'));
|
||||
store = new ChatMapStore(path.join(dir, 'chat-map.json'));
|
||||
// The fixtures' MAX times are of 1970: every message would be «late» and get its time line (lateTimeStamp).
|
||||
process.env.BACKFILL_TIME = 'off';
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
delete process.env.BACKFILL_TIME;
|
||||
await rm(dir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
|
|
@ -407,6 +410,37 @@ describe('syncAllChatsToTelegram', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('late messages and authors', () => {
|
||||
it('a message delivered late gets its MAX time; a fresh one and BACKFILL_TIME=off get none', () => {
|
||||
const sent = Date.parse('2026-09-10T08:27:00Z');
|
||||
expect(lateTimeStamp(sent, { TZ: 'Europe/Moscow' }, sent + 6 * 60_000)).toBe('🕓 10.09.2026, 11:27');
|
||||
expect(lateTimeStamp(sent, {}, sent + 6 * 60_000)).toBe('🕓 10.09.2026, 11:27'); // МСК by default
|
||||
expect(lateTimeStamp(sent, {}, sent + 4 * 60_000)).toBeUndefined(); // the live message a catch-up delivers
|
||||
expect(lateTimeStamp(sent, { BACKFILL_TIME: 'off' }, sent + 60 * 60_000)).toBeUndefined();
|
||||
expect(lateTimeStamp('мусор', {}, sent)).toBeUndefined();
|
||||
});
|
||||
|
||||
it('a group shows its authors by its type, whatever part of its members the chat list carries', () => {
|
||||
expect(showsAuthor({ type: 'CHAT', participants: { '1': 0, '2': 0 } })).toBe(true); // GitHub #3: a big group listed with two
|
||||
expect(showsAuthor({ type: 'CHAT' })).toBe(true);
|
||||
expect(showsAuthor({ type: 'DIALOG', participants: { '1': 0, '2': 0 } })).toBe(false);
|
||||
expect(showsAuthor({ participants: { '1': 0, '2': 0, '3': 0 } })).toBe(true); // no type: as before
|
||||
expect(showsAuthor({ type: 'CHANNEL', participants: { '1': 0 } })).toBe(false);
|
||||
expect(showsAuthor(undefined)).toBe(false);
|
||||
});
|
||||
|
||||
it('a history fill writes the author of a group message and its time', async () => {
|
||||
delete process.env.BACKFILL_TIME;
|
||||
await store.upsert({ maxChatId: '7', telegramTopicId: 100, title: 'Дом', createdAt: 'x' });
|
||||
const old = Date.now() - 3 * 86_400_000;
|
||||
const w = fakeWorld([{ id: 1, time: old, text: 'лифт сломан', sender: 42 } as unknown as MaxHistoryMessage]);
|
||||
w.sync.deps.getChats = () => [{ id: 7, type: 'CHAT', participants: { '5': 0, '42': 0 } }];
|
||||
await w.sync.ensureCaughtUp(7);
|
||||
expect(w.sends).toHaveLength(1);
|
||||
expect(w.sends[0]!.text).toMatch(/^👤 [^\n]+:\nлифт сломан\n🕓 \d\d\.\d\d\.\d{4}, \d\d:\d\d$/);
|
||||
});
|
||||
});
|
||||
|
||||
describe('isLeftChat', () => {
|
||||
it('is MAX saying the account is out of a group or channel — never a dialog, never an unclear answer', () => {
|
||||
expect(isLeftChat({ type: 'CHANNEL', status: 'LEFT' })).toBe(true);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue