Архив: выбор чата в личке, отрицательные номера, стирание всего архива
- /archive в личке с ботом показывает список чатов с архивом (название и количество), дальше — листание и удаление - Номера сообщений могут быть отрицательными (история группы до перехода в супергруппу) — кнопки и /archive_del их понимают - /archive_del all — стереть весь архив чата с подтверждением; в личке первым аргументом ID чата - В callback_data кнопок передаётся ID чата, поэтому архив группы можно чистить из лички Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -12,7 +12,7 @@ Telegram-бот для групповых чатов с AI-личностями.
|
|||||||
- **Память — две части**:
|
- **Память — две части**:
|
||||||
- *Контекст диалога* — вопросы к боту и его ответы в этом чате, хранит [Mastra Memory](https://mastra.ai/docs/memory/message-history) (`memory.db` рядом с `DB_PATH`). Подкладывается в каждый запрос, поэтому можно уточнять «а подробнее?». Самые старые реплики Mastra срезает, когда промпт не влезает в `CONTEXT_TOKEN_LIMIT`; результаты инструментов из прошлых ответов (`ToolCallFilter`) и фото в историю не попадают. `/clear` сбрасывает только контекст.
|
- *Контекст диалога* — вопросы к боту и его ответы в этом чате, хранит [Mastra Memory](https://mastra.ai/docs/memory/message-history) (`memory.db` рядом с `DB_PATH`). Подкладывается в каждый запрос, поэтому можно уточнять «а подробнее?». Самые старые реплики Mastra срезает, когда промпт не влезает в `CONTEXT_TOKEN_LIMIT`; результаты инструментов из прошлых ответов (`ToolCallFilter`) и фото в историю не попадают. `/clear` сбрасывает только контекст.
|
||||||
- *Архив чата* — вся переписка (живая и импортированная). Модель читает его инструментами по необходимости; `/clear` его не трогает.
|
- *Архив чата* — вся переписка (живая и импортированная). Модель читает его инструментами по необходимости; `/clear` его не трогает.
|
||||||
- `/archive` — архив чата с кнопками 🗑 для удаления сообщений и «⬅️ Раньше» для листания; `/archive_del 120-130 135` — удалить пачку по номерам. Служебные команды боту в архив не пишутся.
|
- `/archive` — архив чата с кнопками 🗑 для удаления сообщений и «⬅️ Раньше» для листания. В группе — её архив, в личке с ботом — список всех чатов с архивом на выбор. `/archive_del 120-130 135` — удалить пачку по номерам (в личке первым аргументом ID чата, он есть в заголовке списка), `/archive_del all` — стереть весь архив с подтверждением. Номера бывают отрицательными — так Telegram нумерует историю группы до перехода в супергруппу. Служебные команды боту в архив не пишутся.
|
||||||
- `/context` — что сейчас в контексте и в архиве, ID чата, видит ли бот всю переписку; `/import` — загрузить старую переписку из экспорта (см. ниже).
|
- `/context` — что сейчас в контексте и в архиве, ID чата, видит ли бот всю переписку; `/import` — загрузить старую переписку из экспорта (см. ниже).
|
||||||
- **Личности**: задаются в коде, из чата не редактируются. Сейчас одна — `boltun` (Dirty D).
|
- **Личности**: задаются в коде, из чата не редактируются. Сейчас одна — `boltun` (Dirty D).
|
||||||
- `/personas` — список и выбор кнопками
|
- `/personas` — список и выбор кнопками
|
||||||
|
|||||||
@@ -29,8 +29,8 @@ export const helpText = (username: string) => `Я отвечаю только а
|
|||||||
• /context — что сейчас в контексте и в архиве, вижу ли я всю переписку
|
• /context — что сейчас в контексте и в архиве, вижу ли я всю переписку
|
||||||
• /clear — сбросить контекст диалога (архив не трогаю)
|
• /clear — сбросить контекст диалога (архив не трогаю)
|
||||||
• /import — загрузить старую переписку из экспорта Telegram Desktop (JSON)
|
• /import — загрузить старую переписку из экспорта Telegram Desktop (JSON)
|
||||||
• /archive — архив чата с кнопками удаления, листать кнопкой «Раньше»
|
• /archive — архив чата с кнопками удаления (в личке — выбор чата), листать кнопкой «Раньше»
|
||||||
• /archive_del 120-130 — удалить пачку сообщений из архива по номерам
|
• /archive_del 120-130 — удалить пачку по номерам, /archive_del all — весь архив (в личке сначала ID чата)
|
||||||
|
|
||||||
Личности:
|
Личности:
|
||||||
• /personas — список и выбор кнопками
|
• /personas — список и выбор кнопками
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Composer, InlineKeyboard } from "grammy";
|
import { Composer, InlineKeyboard, type Context } from "grammy";
|
||||||
import { formatUtc } from "../../libs/time.js";
|
import { formatUtc } from "../../libs/time.js";
|
||||||
import type { ChatHistoryService } from "../../services/chat-history-service.js";
|
import type { ChatHistoryService } from "../../services/chat-history-service.js";
|
||||||
|
|
||||||
@@ -8,21 +8,22 @@ const PREVIEW_LENGTH = 150;
|
|||||||
|
|
||||||
const BUTTONS_PER_ROW = 5;
|
const BUTTONS_PER_ROW = 5;
|
||||||
|
|
||||||
/** Страница «самые свежие» — в callback_data кодируем нулём. */
|
/** Страница «самые свежие» в callback_data. Номер сообщения так не выглядит: номера бывают и отрицательными. */
|
||||||
const LATEST = 0;
|
const LATEST = "L";
|
||||||
|
|
||||||
const DEL_USAGE =
|
/** Номер сообщения или ID чата: у импортированной истории старых групп и у групп они отрицательные. */
|
||||||
"Формат: /archive_del 120-130 135 — номера сообщений из /archive (можно диапазоны через дефис).";
|
const NUM = "(-?\\d+)";
|
||||||
|
|
||||||
const preview = (text: string): string =>
|
const preview = (text: string): string =>
|
||||||
text.length > PREVIEW_LENGTH ? `${text.slice(0, PREVIEW_LENGTH)}…` : text;
|
text.length > PREVIEW_LENGTH ? `${text.slice(0, PREVIEW_LENGTH)}…` : text;
|
||||||
|
|
||||||
/** «120-130 135» → [{120..130}, {135..135}]; undefined — если что-то не разобралось. */
|
const chunk = <T>(items: T[], size: number): T[][] =>
|
||||||
const parseRanges = (args: string): { from: number; to: number }[] | undefined => {
|
items.length === 0 ? [] : [items.slice(0, size), ...chunk(items.slice(size), size)];
|
||||||
const tokens = args.split(/[\s,]+/).filter(Boolean);
|
|
||||||
|
|
||||||
|
/** «120-130 135 -999307862» → диапазоны; undefined — если что-то не разобралось. */
|
||||||
|
const parseRanges = (tokens: string[]): { from: number; to: number }[] | undefined => {
|
||||||
const ranges = tokens.map((token) => {
|
const ranges = tokens.map((token) => {
|
||||||
const match = token.match(/^(\d+)(?:-(\d+))?$/);
|
const match = token.match(/^(-?\d+)(?:-(-?\d+))?$/);
|
||||||
|
|
||||||
if (!match) return undefined;
|
if (!match) return undefined;
|
||||||
|
|
||||||
@@ -35,22 +36,53 @@ const parseRanges = (args: string): { from: number; to: number }[] | undefined =
|
|||||||
return tokens.length > 0 && ranges.every((range) => range !== undefined) ? ranges : undefined;
|
return tokens.length > 0 && ranges.every((range) => range !== undefined) ? ranges : undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
const chunk = <T>(items: T[], size: number): T[][] =>
|
const isPrivate = (ctx: Context): boolean => ctx.chat?.type === "private";
|
||||||
items.length === 0 ? [] : [items.slice(0, size), ...chunk(items.slice(size), size)];
|
|
||||||
|
|
||||||
/** Просмотр и удаление сообщений архива чата. Подключать только за фильтром администратора. */
|
/** Как называть чат в списке: название группы или имя в личке; если Telegram не ответил — ID. */
|
||||||
|
const chatTitle = async (ctx: Context, chatId: number): Promise<string> => {
|
||||||
|
const chat = await ctx.api.getChat(chatId).catch(() => undefined);
|
||||||
|
|
||||||
|
if (!chat) return String(chatId);
|
||||||
|
|
||||||
|
return "title" in chat && chat.title ? chat.title : `личка ${chat.first_name ?? chatId}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Просмотр и удаление сообщений архива. В группе — её архив, в личке с ботом — любой чат на выбор. Только для админов. */
|
||||||
export const createArchiveHandlers = (chatHistoryService: ChatHistoryService) => {
|
export const createArchiveHandlers = (chatHistoryService: ChatHistoryService) => {
|
||||||
const composer = new Composer();
|
const composer = new Composer();
|
||||||
|
|
||||||
/** Страница архива: сообщения до `before` (или самые свежие) с кнопками удаления и листания. */
|
/** Команда удаления пачки для заголовка: в личке к ней нужен ID чата. */
|
||||||
const renderPage = (chatId: number, before: number) => {
|
const delCommand = (chatId: number, inPrivate: boolean) => `/archive_del ${inPrivate ? `${chatId} ` : ""}`;
|
||||||
const messages = chatHistoryService.page(chatId, PAGE_SIZE, before === LATEST ? undefined : before);
|
|
||||||
|
const renderChats = async (ctx: Context) => {
|
||||||
|
const chats = chatHistoryService.chats();
|
||||||
|
|
||||||
|
if (chats.length === 0) return { text: "Архив пуст — бот ещё ничего не сохранил.", reply_markup: new InlineKeyboard() };
|
||||||
|
|
||||||
|
const titles = await Promise.all(chats.map((chat) => chatTitle(ctx, chat.chatId)));
|
||||||
|
|
||||||
|
const keyboard = chats.reduce(
|
||||||
|
(kb, chat, i) => kb.text(`${titles[i]} — ${chat.count}`, `arch:chat:${chat.chatId}`).row(),
|
||||||
|
new InlineKeyboard(),
|
||||||
|
);
|
||||||
|
|
||||||
|
return { text: "Какой архив открыть?", reply_markup: keyboard };
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Страница архива чата: сообщения до `before` (или самые свежие) с кнопками удаления и листания. */
|
||||||
|
const renderPage = (chatId: number, before: string, inPrivate: boolean) => {
|
||||||
|
const messages = chatHistoryService.page(chatId, PAGE_SIZE, before === LATEST ? undefined : Number(before));
|
||||||
const { count } = chatHistoryService.stats(chatId);
|
const { count } = chatHistoryService.stats(chatId);
|
||||||
|
|
||||||
|
const backToChats = inPrivate ? [InlineKeyboard.text("📂 Чаты", "arch:list")] : [];
|
||||||
|
|
||||||
if (messages.length === 0) {
|
if (messages.length === 0) {
|
||||||
const text = before === LATEST ? "Архив этого чата пуст." : "Раньше сообщений нет.";
|
const text = before === LATEST ? "Архив этого чата пуст." : "Раньше сообщений нет.";
|
||||||
|
|
||||||
return { text, reply_markup: new InlineKeyboard().text("⏭ К последним", `arch:page:${LATEST}`) };
|
return {
|
||||||
|
text,
|
||||||
|
reply_markup: InlineKeyboard.from([[InlineKeyboard.text("⏭ К последним", `arch:page:${chatId}:${LATEST}`), ...backToChats]]),
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const lines = messages.map((m) => `#${m.messageId} [${formatUtc(m.date)}] ${m.author}: ${preview(m.text)}`);
|
const lines = messages.map((m) => `#${m.messageId} [${formatUtc(m.date)}] ${m.author}: ${preview(m.text)}`);
|
||||||
@@ -59,61 +91,112 @@ export const createArchiveHandlers = (chatHistoryService: ChatHistoryService) =>
|
|||||||
const hasOlder = chatHistoryService.page(chatId, 1, oldest).length > 0;
|
const hasOlder = chatHistoryService.page(chatId, 1, oldest).length > 0;
|
||||||
|
|
||||||
const deleteRows = chunk(messages, BUTTONS_PER_ROW).map((row) =>
|
const deleteRows = chunk(messages, BUTTONS_PER_ROW).map((row) =>
|
||||||
row.map((m) => InlineKeyboard.text(`🗑 ${m.messageId}`, `arch:del:${m.messageId}:${before}`)),
|
row.map((m) => InlineKeyboard.text(`🗑 ${m.messageId}`, `arch:del:${chatId}:${m.messageId}:${before}`)),
|
||||||
);
|
);
|
||||||
|
|
||||||
const navRow = [
|
const navRow = [
|
||||||
...(hasOlder ? [InlineKeyboard.text("⬅️ Раньше", `arch:page:${oldest}`)] : []),
|
...(hasOlder ? [InlineKeyboard.text("⬅️ Раньше", `arch:page:${chatId}:${oldest}`)] : []),
|
||||||
...(before !== LATEST ? [InlineKeyboard.text("⏭ К последним", `arch:page:${LATEST}`)] : []),
|
...(before !== LATEST ? [InlineKeyboard.text("⏭ К последним", `arch:page:${chatId}:${LATEST}`)] : []),
|
||||||
|
...backToChats,
|
||||||
];
|
];
|
||||||
|
|
||||||
const text = [`Архив чата: ${count} сообщений. Удалить пачку: /archive_del 120-130`, "", ...lines].join("\n");
|
const cmd = delCommand(chatId, inPrivate);
|
||||||
|
|
||||||
|
const text = [
|
||||||
|
`Архив чата: ${count} сообщений.`,
|
||||||
|
`Удалить пачку: ${cmd}120-130 · весь архив: ${cmd}all`,
|
||||||
|
"",
|
||||||
|
...lines,
|
||||||
|
].join("\n");
|
||||||
|
|
||||||
return { text, reply_markup: InlineKeyboard.from([...deleteRows, ...(navRow.length > 0 ? [navRow] : [])]) };
|
return { text, reply_markup: InlineKeyboard.from([...deleteRows, ...(navRow.length > 0 ? [navRow] : [])]) };
|
||||||
};
|
};
|
||||||
|
|
||||||
composer.command("archive", async (ctx) => {
|
const edit = async (ctx: Context, view: { text: string; reply_markup: InlineKeyboard }) =>
|
||||||
const { text, reply_markup } = renderPage(ctx.chat.id, LATEST);
|
ctx.editMessageText(view.text, { reply_markup: view.reply_markup }).catch(() => {});
|
||||||
|
|
||||||
await ctx.reply(text, { reply_markup });
|
composer.command("archive", async (ctx) => {
|
||||||
|
const view = isPrivate(ctx) ? await renderChats(ctx) : renderPage(ctx.chat.id, LATEST, false);
|
||||||
|
|
||||||
|
await ctx.reply(view.text, { reply_markup: view.reply_markup });
|
||||||
});
|
});
|
||||||
|
|
||||||
composer.command("archive_del", async (ctx) => {
|
composer.command("archive_del", async (ctx) => {
|
||||||
const ranges = parseRanges(ctx.match);
|
const tokens = ctx.match.split(/[\s,]+/).filter(Boolean);
|
||||||
|
const inPrivate = isPrivate(ctx);
|
||||||
|
|
||||||
if (!ranges) {
|
const [chatToken, ...rest] = inPrivate ? tokens : [String(ctx.chat.id), ...tokens];
|
||||||
await ctx.reply(DEL_USAGE);
|
const chatId = Number(chatToken);
|
||||||
|
|
||||||
|
const usage =
|
||||||
|
`Формат: ${inPrivate ? "/archive_del <ID чата> 120-130 135" : "/archive_del 120-130 135"} — номера из /archive, ` +
|
||||||
|
`можно диапазоны через дефис; весь архив: ${inPrivate ? "/archive_del <ID чата> all" : "/archive_del all"}.`;
|
||||||
|
|
||||||
|
if (!Number.isSafeInteger(chatId) || rest.length === 0) {
|
||||||
|
await ctx.reply(usage);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const removed = chatHistoryService.remove(ctx.chat.id, ranges);
|
if (rest.length === 1 && rest[0]!.toLowerCase() === "all") {
|
||||||
|
const { count } = chatHistoryService.stats(chatId);
|
||||||
|
|
||||||
|
const keyboard = new InlineKeyboard()
|
||||||
|
.text(`🗑 Да, стереть все ${count}`, `arch:wipe:${chatId}`)
|
||||||
|
.text("Отмена", "arch:wipe_no");
|
||||||
|
|
||||||
|
await ctx.reply(`Стереть весь архив чата ${chatId} (${count} сообщений)? Вернуть можно только повторным /import.`, {
|
||||||
|
reply_markup: keyboard,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ranges = parseRanges(rest);
|
||||||
|
|
||||||
|
if (!ranges) {
|
||||||
|
await ctx.reply(usage);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const removed = chatHistoryService.remove(chatId, ranges);
|
||||||
|
|
||||||
await ctx.reply(`Удалил из архива ${removed} сообщений. Посмотреть, что осталось: /archive`);
|
await ctx.reply(`Удалил из архива ${removed} сообщений. Посмотреть, что осталось: /archive`);
|
||||||
});
|
});
|
||||||
|
|
||||||
composer.callbackQuery(/^arch:del:(\d+):(\d+)$/, async (ctx) => {
|
composer.callbackQuery("arch:list", async (ctx) => {
|
||||||
const chatId = ctx.chat?.id;
|
await ctx.answerCallbackQuery();
|
||||||
|
await edit(ctx, await renderChats(ctx));
|
||||||
if (chatId === undefined) return ctx.answerCallbackQuery();
|
|
||||||
|
|
||||||
const messageId = Number(ctx.match[1]);
|
|
||||||
const removed = chatHistoryService.remove(chatId, [{ from: messageId, to: messageId }]);
|
|
||||||
|
|
||||||
const { text, reply_markup } = renderPage(chatId, Number(ctx.match[2]));
|
|
||||||
|
|
||||||
await ctx.answerCallbackQuery({ text: removed > 0 ? `Удалено #${messageId}` : "Уже удалено" });
|
|
||||||
await ctx.editMessageText(text, { reply_markup }).catch(() => {});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
composer.callbackQuery(/^arch:page:(\d+)$/, async (ctx) => {
|
composer.callbackQuery(new RegExp(`^arch:chat:${NUM}$`), async (ctx) => {
|
||||||
const chatId = ctx.chat?.id;
|
|
||||||
|
|
||||||
if (chatId === undefined) return ctx.answerCallbackQuery();
|
|
||||||
|
|
||||||
const { text, reply_markup } = renderPage(chatId, Number(ctx.match[1]));
|
|
||||||
|
|
||||||
await ctx.answerCallbackQuery();
|
await ctx.answerCallbackQuery();
|
||||||
await ctx.editMessageText(text, { reply_markup }).catch(() => {});
|
await edit(ctx, renderPage(Number(ctx.match[1]), LATEST, isPrivate(ctx)));
|
||||||
|
});
|
||||||
|
|
||||||
|
composer.callbackQuery(new RegExp(`^arch:page:${NUM}:(-?\\d+|${LATEST})$`), async (ctx) => {
|
||||||
|
await ctx.answerCallbackQuery();
|
||||||
|
await edit(ctx, renderPage(Number(ctx.match[1]), ctx.match[2]!, isPrivate(ctx)));
|
||||||
|
});
|
||||||
|
|
||||||
|
composer.callbackQuery(new RegExp(`^arch:del:${NUM}:${NUM}:(-?\\d+|${LATEST})$`), async (ctx) => {
|
||||||
|
const chatId = Number(ctx.match[1]);
|
||||||
|
const messageId = Number(ctx.match[2]);
|
||||||
|
|
||||||
|
const removed = chatHistoryService.remove(chatId, [{ from: messageId, to: messageId }]);
|
||||||
|
|
||||||
|
await ctx.answerCallbackQuery({ text: removed > 0 ? `Удалено #${messageId}` : "Уже удалено" });
|
||||||
|
await edit(ctx, renderPage(chatId, ctx.match[3]!, isPrivate(ctx)));
|
||||||
|
});
|
||||||
|
|
||||||
|
composer.callbackQuery(new RegExp(`^arch:wipe:${NUM}$`), async (ctx) => {
|
||||||
|
const removed = chatHistoryService.removeAll(Number(ctx.match[1]));
|
||||||
|
|
||||||
|
await ctx.answerCallbackQuery({ text: "Архив стёрт" });
|
||||||
|
await ctx.editMessageText(`Готово: стёр весь архив чата (${removed} сообщений).`).catch(() => {});
|
||||||
|
});
|
||||||
|
|
||||||
|
composer.callbackQuery("arch:wipe_no", async (ctx) => {
|
||||||
|
await ctx.answerCallbackQuery({ text: "Отменено" });
|
||||||
|
await ctx.editMessageText("Стирание архива отменено.").catch(() => {});
|
||||||
});
|
});
|
||||||
|
|
||||||
return composer;
|
return composer;
|
||||||
|
|||||||
@@ -48,6 +48,8 @@ export const createMessageRepository = (db: Db) => {
|
|||||||
"SELECT * FROM messages WHERE chat_id = ? AND message_id < ? ORDER BY message_id DESC LIMIT ?",
|
"SELECT * FROM messages WHERE chat_id = ? AND message_id < ? ORDER BY message_id DESC LIMIT ?",
|
||||||
),
|
),
|
||||||
deleteRange: db.prepare("DELETE FROM messages WHERE chat_id = ? AND message_id BETWEEN ? AND ?"),
|
deleteRange: db.prepare("DELETE FROM messages WHERE chat_id = ? AND message_id BETWEEN ? AND ?"),
|
||||||
|
deleteByChat: db.prepare("DELETE FROM messages WHERE chat_id = ?"),
|
||||||
|
countsByChat: db.prepare("SELECT chat_id AS chatId, COUNT(*) AS count FROM messages GROUP BY chat_id ORDER BY count DESC"),
|
||||||
};
|
};
|
||||||
|
|
||||||
const toChronological = (rows: unknown[]): ChatMessage[] => (rows as MessageRow[]).reverse().map(toMessage);
|
const toChronological = (rows: unknown[]): ChatMessage[] => (rows as MessageRow[]).reverse().map(toMessage);
|
||||||
@@ -96,7 +98,25 @@ export const createMessageRepository = (db: Db) => {
|
|||||||
const deleteRange = (chatId: number, from: number, to: number): number =>
|
const deleteRange = (chatId: number, from: number, to: number): number =>
|
||||||
statements.deleteRange.run(chatId, from, to).changes;
|
statements.deleteRange.run(chatId, from, to).changes;
|
||||||
|
|
||||||
return { upsertAndPrune, insertManyAndPrune, latest, latestByUser, findById, statsByChat, pageBefore, deleteRange };
|
/** Удаляет весь архив чата; возвращает, сколько сообщений удалено. */
|
||||||
|
const deleteByChat = (chatId: number): number => statements.deleteByChat.run(chatId).changes;
|
||||||
|
|
||||||
|
/** Чаты, у которых есть архив, и сколько в каждом сообщений — самые большие первыми. */
|
||||||
|
const countsByChat = (): { chatId: number; count: number }[] =>
|
||||||
|
statements.countsByChat.all() as { chatId: number; count: number }[];
|
||||||
|
|
||||||
|
return {
|
||||||
|
upsertAndPrune,
|
||||||
|
insertManyAndPrune,
|
||||||
|
latest,
|
||||||
|
latestByUser,
|
||||||
|
findById,
|
||||||
|
statsByChat,
|
||||||
|
pageBefore,
|
||||||
|
deleteRange,
|
||||||
|
deleteByChat,
|
||||||
|
countsByChat,
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export type MessageRepository = ReturnType<typeof createMessageRepository>;
|
export type MessageRepository = ReturnType<typeof createMessageRepository>;
|
||||||
|
|||||||
@@ -29,7 +29,13 @@ export const createChatHistoryService = ({ messageRepository, historyLimit }: Ch
|
|||||||
const remove = (chatId: number, ranges: { from: number; to: number }[]): number =>
|
const remove = (chatId: number, ranges: { from: number; to: number }[]): number =>
|
||||||
ranges.reduce((sum, { from, to }) => sum + messageRepository.deleteRange(chatId, from, to), 0);
|
ranges.reduce((sum, { from, to }) => sum + messageRepository.deleteRange(chatId, from, to), 0);
|
||||||
|
|
||||||
return { record, latest, latestByUser, find, stats, page, remove };
|
/** Стереть весь архив чата. */
|
||||||
|
const removeAll = (chatId: number): number => messageRepository.deleteByChat(chatId);
|
||||||
|
|
||||||
|
/** Чаты с архивом — для выбора в личке с ботом. */
|
||||||
|
const chats = (): { chatId: number; count: number }[] => messageRepository.countsByChat();
|
||||||
|
|
||||||
|
return { record, latest, latestByUser, find, stats, page, remove, removeAll, chats };
|
||||||
};
|
};
|
||||||
|
|
||||||
export type ChatHistoryService = ReturnType<typeof createChatHistoryService>;
|
export type ChatHistoryService = ReturnType<typeof createChatHistoryService>;
|
||||||
|
|||||||
Reference in New Issue
Block a user