@will-17173/telegram-cli 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (82) hide show
  1. package/LICENSE +675 -0
  2. package/README.md +264 -0
  3. package/README.zh-CN.md +264 -0
  4. package/dist/account/account-context.js +22 -0
  5. package/dist/account/account-context.js.map +1 -0
  6. package/dist/account/account-presets.js +22 -0
  7. package/dist/account/account-presets.js.map +1 -0
  8. package/dist/account/account-store.js +225 -0
  9. package/dist/account/account-store.js.map +1 -0
  10. package/dist/cli/app.js +21 -0
  11. package/dist/cli/app.js.map +1 -0
  12. package/dist/cli/output.js +39 -0
  13. package/dist/cli/output.js.map +1 -0
  14. package/dist/commands/account-options.js +55 -0
  15. package/dist/commands/account-options.js.map +1 -0
  16. package/dist/commands/account.js +449 -0
  17. package/dist/commands/account.js.map +1 -0
  18. package/dist/commands/config.js +63 -0
  19. package/dist/commands/config.js.map +1 -0
  20. package/dist/commands/data.js +52 -0
  21. package/dist/commands/data.js.map +1 -0
  22. package/dist/commands/query.js +116 -0
  23. package/dist/commands/query.js.map +1 -0
  24. package/dist/commands/telegram.js +423 -0
  25. package/dist/commands/telegram.js.map +1 -0
  26. package/dist/commands/types.js +12 -0
  27. package/dist/commands/types.js.map +1 -0
  28. package/dist/config/credential-store.js +82 -0
  29. package/dist/config/credential-store.js.map +1 -0
  30. package/dist/config/env.js +78 -0
  31. package/dist/config/env.js.map +1 -0
  32. package/dist/index.js +4 -0
  33. package/dist/index.js.map +1 -0
  34. package/dist/presenters/human.js +218 -0
  35. package/dist/presenters/human.js.map +1 -0
  36. package/dist/presenters/ink/DetailView.js +14 -0
  37. package/dist/presenters/ink/DetailView.js.map +1 -0
  38. package/dist/presenters/ink/TableView.js +9 -0
  39. package/dist/presenters/ink/TableView.js.map +1 -0
  40. package/dist/presenters/ink/TimelineView.js +34 -0
  41. package/dist/presenters/ink/TimelineView.js.map +1 -0
  42. package/dist/presenters/ink/display-width.js +149 -0
  43. package/dist/presenters/ink/display-width.js.map +1 -0
  44. package/dist/presenters/ink/image-preview.js +47 -0
  45. package/dist/presenters/ink/image-preview.js.map +1 -0
  46. package/dist/presenters/ink/listen-scroll.js +37 -0
  47. package/dist/presenters/ink/listen-scroll.js.map +1 -0
  48. package/dist/presenters/ink/listen-scrollbar.js +49 -0
  49. package/dist/presenters/ink/listen-scrollbar.js.map +1 -0
  50. package/dist/presenters/ink/listen.js +542 -0
  51. package/dist/presenters/ink/listen.js.map +1 -0
  52. package/dist/presenters/ink/mouse-scroll.js +55 -0
  53. package/dist/presenters/ink/mouse-scroll.js.map +1 -0
  54. package/dist/presenters/ink/render.js +45 -0
  55. package/dist/presenters/ink/render.js.map +1 -0
  56. package/dist/presenters/listen-message.js +267 -0
  57. package/dist/presenters/listen-message.js.map +1 -0
  58. package/dist/presenters/structured.js +30 -0
  59. package/dist/presenters/structured.js.map +1 -0
  60. package/dist/services/attachment-download.js +21 -0
  61. package/dist/services/attachment-download.js.map +1 -0
  62. package/dist/services/data-service.js +76 -0
  63. package/dist/services/data-service.js.map +1 -0
  64. package/dist/services/listen-album-aggregator.js +72 -0
  65. package/dist/services/listen-album-aggregator.js.map +1 -0
  66. package/dist/services/message-service.js +102 -0
  67. package/dist/services/message-service.js.map +1 -0
  68. package/dist/services/query-service.js +116 -0
  69. package/dist/services/query-service.js.map +1 -0
  70. package/dist/services/sync-service.js +134 -0
  71. package/dist/services/sync-service.js.map +1 -0
  72. package/dist/storage/chat-resolver.js +9 -0
  73. package/dist/storage/chat-resolver.js.map +1 -0
  74. package/dist/storage/message-db.js +246 -0
  75. package/dist/storage/message-db.js.map +1 -0
  76. package/dist/telegram/client-factory.js +27 -0
  77. package/dist/telegram/client-factory.js.map +1 -0
  78. package/dist/telegram/mtcute-client.js +324 -0
  79. package/dist/telegram/mtcute-client.js.map +1 -0
  80. package/dist/telegram/types.js +2 -0
  81. package/dist/telegram/types.js.map +1 -0
  82. package/package.json +50 -0
@@ -0,0 +1,102 @@
1
+ import { actionDetail } from '../presenters/human.js';
2
+ export class MessageService {
3
+ tg;
4
+ constructor(tg) {
5
+ this.tg = tg;
6
+ }
7
+ async send(options) {
8
+ const invalid = validateSend(options);
9
+ if (invalid)
10
+ return invalid;
11
+ try {
12
+ const result = await this.tg.sendMessage(options);
13
+ const payload = {
14
+ sent: true,
15
+ msg_id: result.msg_id,
16
+ chat: options.chat,
17
+ };
18
+ if (options.reply != null)
19
+ payload.reply_to = options.reply;
20
+ return { ok: true, data: payload, human: actionDetail('Message Sent', payload) };
21
+ }
22
+ catch (error) {
23
+ return telegramFailure(error);
24
+ }
25
+ }
26
+ async edit(options) {
27
+ const invalid = validateEdit(options);
28
+ if (invalid)
29
+ return invalid;
30
+ try {
31
+ await this.tg.editMessage({ chat: options.chat, msgId: options.msgId, text: options.text, linkPreview: options.linkPreview });
32
+ const data = { edited: true, msg_id: options.msgId, chat: options.chat };
33
+ return { ok: true, data, human: actionDetail('Message Edited', data) };
34
+ }
35
+ catch (error) {
36
+ return telegramFailure(error);
37
+ }
38
+ }
39
+ async delete(options) {
40
+ const invalid = validateDelete(options);
41
+ if (invalid)
42
+ return invalid;
43
+ try {
44
+ await this.tg.deleteMessages({ chat: options.chat, msgIds: options.msgIds });
45
+ const data = { deleted: true, msg_ids: options.msgIds, chat: options.chat };
46
+ return { ok: true, data, human: actionDetail('Messages Deleted', data) };
47
+ }
48
+ catch (error) {
49
+ return telegramFailure(error);
50
+ }
51
+ }
52
+ }
53
+ function validateSend(options) {
54
+ if (!options.chat.trim())
55
+ return invalidOption('chat must be a non-empty string.');
56
+ if (!options.message.trim())
57
+ return invalidOption('message must not be empty.');
58
+ if (options.reply != null && !isPositiveInteger(options.reply))
59
+ return invalidOption('reply must be a positive integer.');
60
+ return undefined;
61
+ }
62
+ function validateEdit(options) {
63
+ if (!options.chat.trim())
64
+ return invalidOption('chat must be a non-empty string.');
65
+ if (!isPositiveInteger(options.msgId))
66
+ return invalidOption('msg_id must be a positive integer.');
67
+ if (!options.text.trim())
68
+ return invalidOption('text must not be empty.');
69
+ return undefined;
70
+ }
71
+ function validateDelete(options) {
72
+ if (!options.chat.trim())
73
+ return invalidOption('chat must be a non-empty string.');
74
+ if (!Array.isArray(options.msgIds) || options.msgIds.length === 0) {
75
+ return invalidOption('msg_ids must be a non-empty array.');
76
+ }
77
+ if (!options.msgIds.every(isPositiveInteger))
78
+ return invalidOption('All msg_ids must be positive integers.');
79
+ return undefined;
80
+ }
81
+ function isPositiveInteger(value) {
82
+ return Number.isInteger(value) && value > 0;
83
+ }
84
+ function invalidOption(message) {
85
+ return { ok: false, error: { code: 'invalid_option', message } };
86
+ }
87
+ function telegramFailure(error) {
88
+ const details = errorDetails(error);
89
+ return {
90
+ ok: false,
91
+ error: details == null
92
+ ? { code: 'telegram_error', message: errorMessage(error) }
93
+ : { code: 'telegram_error', message: errorMessage(error), details },
94
+ };
95
+ }
96
+ function errorMessage(error) {
97
+ return error instanceof Error ? error.message : String(error);
98
+ }
99
+ function errorDetails(error) {
100
+ return error instanceof Error ? { name: error.name } : undefined;
101
+ }
102
+ //# sourceMappingURL=message-service.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"message-service.js","sourceRoot":"","sources":["../../src/services/message-service.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,116 @@
1
+ import { messageTable, statsSummary, timelineView, topTable } from '../presenters/human.js';
2
+ import { MessageDB } from '../storage/message-db.js';
3
+ export class QueryService {
4
+ db;
5
+ constructor(db = new MessageDB()) {
6
+ this.db = db;
7
+ }
8
+ close() {
9
+ this.db.close();
10
+ }
11
+ search(options) {
12
+ const valid = validateQueryOptions(options);
13
+ if (!valid.ok)
14
+ return valid;
15
+ const chatId = this.resolveChat(options.chat);
16
+ if (!chatId.ok)
17
+ return chatId;
18
+ try {
19
+ const data = options.regex
20
+ ? this.db.searchRegex(options.keyword, { chatId: chatId.data, sender: options.sender, hours: options.hours, limit: options.limit })
21
+ : this.db.search(options.keyword, { chatId: chatId.data, sender: options.sender, hours: options.hours, limit: options.limit });
22
+ return { ok: true, data, human: messageTable(data, 'Search Results') };
23
+ }
24
+ catch (error) {
25
+ if (error instanceof SyntaxError) {
26
+ return { ok: false, error: { code: 'invalid_regex', message: error.message } };
27
+ }
28
+ throw error;
29
+ }
30
+ }
31
+ recent(options) {
32
+ const normalized = { ...options, hours: options.hours ?? 24, limit: options.limit ?? 50 };
33
+ const valid = validateQueryOptions(normalized);
34
+ if (!valid.ok)
35
+ return valid;
36
+ const chatId = this.resolveChat(options.chat);
37
+ if (!chatId.ok)
38
+ return chatId;
39
+ const data = this.db.getRecent({ chatId: chatId.data, sender: options.sender, hours: normalized.hours, limit: normalized.limit });
40
+ return { ok: true, data, human: messageTable(data, 'Recent Messages', 'No recent messages found.') };
41
+ }
42
+ stats() {
43
+ const data = { total: this.db.count(), chats: this.db.getChats() };
44
+ return { ok: true, data, human: statsSummary({ total: data.total }, 'Stats', data.chats) };
45
+ }
46
+ top(options) {
47
+ const valid = validateQueryOptions(options);
48
+ if (!valid.ok)
49
+ return valid;
50
+ const chatId = this.resolveChat(options.chat);
51
+ if (!chatId.ok)
52
+ return chatId;
53
+ const data = this.db.topSenders({ chatId: chatId.data, hours: options.hours, limit: options.limit });
54
+ return { ok: true, data, human: topTable('Top Senders', data) };
55
+ }
56
+ timeline(options) {
57
+ const valid = validateQueryOptions(options);
58
+ if (!valid.ok)
59
+ return valid;
60
+ if (options.granularity != null && options.granularity !== 'day' && options.granularity !== 'hour') {
61
+ return invalidOption('granularity', 'Use day or hour for --by.');
62
+ }
63
+ const chatId = this.resolveChat(options.chat);
64
+ if (!chatId.ok)
65
+ return chatId;
66
+ const data = this.db.timeline({ chatId: chatId.data, hours: options.hours, granularity: options.granularity ?? 'day' });
67
+ return { ok: true, data, human: timelineView('Timeline', data) };
68
+ }
69
+ today(options) {
70
+ const chatId = this.resolveChat(options.chat);
71
+ if (!chatId.ok)
72
+ return chatId;
73
+ const data = this.db.getToday({ chatId: chatId.data });
74
+ return { ok: true, data, human: messageTable(data, 'Today', 'No messages found today.') };
75
+ }
76
+ filter(options) {
77
+ const valid = validateQueryOptions(options);
78
+ if (!valid.ok)
79
+ return valid;
80
+ const words = options.keywords.split(',').map((word) => word.trim()).filter(Boolean);
81
+ if (words.length === 0)
82
+ return { ok: false, error: { code: 'invalid_keywords', message: 'Please provide at least one keyword.' } };
83
+ const source = options.hours == null ? this.today({ chat: options.chat }) : this.recent({ chat: options.chat, hours: options.hours, limit: 100000 });
84
+ if (!source.ok)
85
+ return source;
86
+ const regex = new RegExp(words.map((word) => escapeRegex(word)).join('|'), 'i');
87
+ const data = source.data.filter((row) => row.content && regex.test(row.content));
88
+ return { ok: true, data, human: messageTable(data, 'Filtered Messages', 'No filtered messages found.') };
89
+ }
90
+ resolveChat(chat) {
91
+ if (!chat)
92
+ return { ok: true, data: undefined };
93
+ const matches = this.db.findChats(chat);
94
+ if (matches.length === 1)
95
+ return { ok: true, data: matches[0].chat_id };
96
+ if (matches.length === 0)
97
+ return { ok: false, error: { code: 'chat_not_found', message: `Chat '${chat}' not found in database.` } };
98
+ return { ok: false, error: { code: 'ambiguous_chat', message: `Chat '${chat}' is ambiguous. Matches: ${matches.map((m) => m.chat_name ?? m.chat_id).join(', ')}` } };
99
+ }
100
+ }
101
+ function validateQueryOptions(options) {
102
+ if (options.limit != null && (!Number.isInteger(options.limit) || options.limit <= 0)) {
103
+ return invalidOption('limit', 'Limit must be a positive integer.');
104
+ }
105
+ if (options.hours != null && (!Number.isFinite(options.hours) || options.hours <= 0)) {
106
+ return invalidOption('hours', 'Hours must be a positive number.');
107
+ }
108
+ return { ok: true, data: undefined };
109
+ }
110
+ function invalidOption(option, message) {
111
+ return { ok: false, error: { code: 'invalid_option', message, details: { option } } };
112
+ }
113
+ function escapeRegex(value) {
114
+ return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
115
+ }
116
+ //# sourceMappingURL=query-service.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"query-service.js","sourceRoot":"","sources":["../../src/services/query-service.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,134 @@
1
+ import { setTimeout as delayMs } from 'node:timers/promises';
2
+ import { actionDetail, syncSummary } from '../presenters/human.js';
3
+ import { MessageDB } from '../storage/message-db.js';
4
+ const FIRST_SYNC_LIMIT = 500;
5
+ export class SyncService {
6
+ tg;
7
+ db;
8
+ constructor(tg, db) {
9
+ this.tg = tg;
10
+ this.db = db ?? new MessageDB();
11
+ }
12
+ async history(options) {
13
+ const invalid = validateLimit(options.limit);
14
+ if (invalid)
15
+ return invalid;
16
+ try {
17
+ const messages = await this.tg.fetchHistory({ chat: parseChat(options.chat), limit: options.limit });
18
+ const stored = this.db.insertBatch(messages);
19
+ const data = { stored, chat: options.chat };
20
+ return { ok: true, data, human: actionDetail('History Synced', { chat: data.chat, stored: data.stored }) };
21
+ }
22
+ catch (error) {
23
+ return telegramFailure(error);
24
+ }
25
+ }
26
+ async sync(options) {
27
+ const invalid = validateLimit(options.limit);
28
+ if (invalid)
29
+ return invalid;
30
+ const chatId = this.db.resolveChatId(options.chat);
31
+ const minId = chatId == null ? 0 : this.db.getLastMsgId(chatId) ?? 0;
32
+ const limit = minId === 0 && options.limit > FIRST_SYNC_LIMIT ? FIRST_SYNC_LIMIT : options.limit;
33
+ try {
34
+ const messages = await this.tg.fetchHistory({ chat: parseChat(options.chat), limit, minId });
35
+ const stored = this.db.insertBatch(messages);
36
+ const data = { synced: stored, chat: options.chat };
37
+ return { ok: true, data, human: actionDetail('Sync Complete', { chat: data.chat, synced: data.synced }) };
38
+ }
39
+ catch (error) {
40
+ return telegramFailure(error);
41
+ }
42
+ }
43
+ async refresh(options) {
44
+ const invalid = validateRefreshOptions(options);
45
+ if (invalid)
46
+ return invalid;
47
+ let dialogs;
48
+ try {
49
+ dialogs = await this.tg.listChats();
50
+ }
51
+ catch (error) {
52
+ return telegramFailure(error);
53
+ }
54
+ const selected = options.maxChats == null ? dialogs : dialogs.slice(0, options.maxChats);
55
+ const results = {};
56
+ const failures = {};
57
+ for (let index = 0; index < selected.length; index += 1) {
58
+ const dialog = selected[index];
59
+ const lastId = this.db.getLastMsgId(dialog.id) ?? 0;
60
+ const limit = lastId === 0 && options.limit > FIRST_SYNC_LIMIT ? FIRST_SYNC_LIMIT : options.limit;
61
+ try {
62
+ const messages = await this.tg.fetchHistory({ chat: dialog.id, limit, minId: lastId });
63
+ results[dialog.name] = this.db.insertBatch(messages);
64
+ }
65
+ catch (error) {
66
+ results[dialog.name] = 0;
67
+ failures[dialog.name] = errorMessage(error);
68
+ }
69
+ if (options.delay > 0 && index < selected.length - 1) {
70
+ const jitter = options.delay * (Math.random() * 0.4 - 0.2);
71
+ await delayMs((options.delay + jitter) * 1000);
72
+ }
73
+ }
74
+ const updated_chats = Object.entries(results)
75
+ .filter(([, count]) => count > 0)
76
+ .sort((a, b) => b[1] - a[1])
77
+ .map(([name]) => name);
78
+ const data = {
79
+ new_messages: Object.values(results).reduce((sum, count) => sum + count, 0),
80
+ chats: selected.length,
81
+ updated_chats,
82
+ results,
83
+ failures,
84
+ };
85
+ return {
86
+ ok: true,
87
+ data,
88
+ human: syncSummary(data),
89
+ };
90
+ }
91
+ close() {
92
+ this.db.close();
93
+ }
94
+ }
95
+ function validateRefreshOptions(options) {
96
+ const invalidLimit = validateLimit(options.limit);
97
+ if (invalidLimit)
98
+ return invalidLimit;
99
+ if (!Number.isFinite(options.delay) || options.delay < 0)
100
+ return invalidOption('delay must be a non-negative number.');
101
+ if (options.maxChats != null && (!isPositiveInteger(options.maxChats))) {
102
+ return invalidOption('maxChats must be a positive integer.');
103
+ }
104
+ return undefined;
105
+ }
106
+ function validateLimit(limit) {
107
+ return isPositiveInteger(limit) ? undefined : invalidOption('limit must be a positive integer.');
108
+ }
109
+ function invalidOption(message) {
110
+ return { ok: false, error: { code: 'invalid_option', message } };
111
+ }
112
+ function telegramFailure(error) {
113
+ const details = errorDetails(error);
114
+ return {
115
+ ok: false,
116
+ error: details == null
117
+ ? { code: 'telegram_error', message: errorMessage(error) }
118
+ : { code: 'telegram_error', message: errorMessage(error), details },
119
+ };
120
+ }
121
+ function errorMessage(error) {
122
+ return error instanceof Error ? error.message : String(error);
123
+ }
124
+ function errorDetails(error) {
125
+ return error instanceof Error ? { name: error.name } : undefined;
126
+ }
127
+ function isPositiveInteger(value) {
128
+ return Number.isInteger(value) && value > 0;
129
+ }
130
+ function parseChat(chat) {
131
+ const parsed = Number.parseInt(chat, 10);
132
+ return !Number.isNaN(parsed) && String(parsed) === chat.trim() ? parsed : chat;
133
+ }
134
+ //# sourceMappingURL=sync-service.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sync-service.js","sourceRoot":"","sources":["../../src/services/sync-service.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,9 @@
1
+ export function canonicalChatId(chatId) {
2
+ if (chatId < 0) {
3
+ const digits = String(Math.abs(chatId));
4
+ if (digits.startsWith('100') && digits.length > 3)
5
+ return Number.parseInt(digits.slice(3), 10);
6
+ }
7
+ return chatId;
8
+ }
9
+ //# sourceMappingURL=chat-resolver.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"chat-resolver.js","sourceRoot":"","sources":["../../src/storage/chat-resolver.ts"],"names":[],"mappings":"AAAA,MAAM,UAAU,eAAe,CAAC,MAAc;IAC5C,IAAI,MAAM,GAAG,CAAC,EAAE,CAAC;QACf,MAAM,MAAM,GAAG,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,CAAA;QACvC,IAAI,MAAM,CAAC,UAAU,CAAC,KAAK,CAAC,IAAI,MAAM,CAAC,MAAM,GAAG,CAAC;YAAE,OAAO,MAAM,CAAC,QAAQ,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAA;IAChG,CAAC;IACD,OAAO,MAAM,CAAA;AACf,CAAC"}
@@ -0,0 +1,246 @@
1
+ import Database from 'better-sqlite3';
2
+ import { mkdirSync } from 'node:fs';
3
+ import { dirname } from 'node:path';
4
+ import { getDbPath } from '../config/env.js';
5
+ import { canonicalChatId } from './chat-resolver.js';
6
+ export class MessageDB {
7
+ db;
8
+ constructor(path = getDbPath()) {
9
+ mkdirSync(dirname(path), { recursive: true });
10
+ this.db = new Database(path);
11
+ this.db.pragma('journal_mode = WAL');
12
+ this.db.exec(`
13
+ CREATE TABLE IF NOT EXISTS messages (
14
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
15
+ platform TEXT NOT NULL DEFAULT 'telegram',
16
+ chat_id INTEGER NOT NULL,
17
+ chat_name TEXT,
18
+ msg_id INTEGER NOT NULL,
19
+ sender_id INTEGER,
20
+ sender_name TEXT,
21
+ content TEXT,
22
+ timestamp TEXT NOT NULL,
23
+ raw_json TEXT,
24
+ UNIQUE(platform, chat_id, msg_id)
25
+ );
26
+ CREATE INDEX IF NOT EXISTS idx_messages_chat_ts ON messages(chat_id, timestamp);
27
+ CREATE INDEX IF NOT EXISTS idx_messages_content ON messages(content);
28
+ CREATE INDEX IF NOT EXISTS idx_messages_sender ON messages(sender_name);
29
+ `);
30
+ }
31
+ insertBatch(messages) {
32
+ if (messages.length === 0)
33
+ return 0;
34
+ const stmt = this.db.prepare(`
35
+ INSERT OR IGNORE INTO messages
36
+ (platform, chat_id, chat_name, msg_id, sender_id, sender_name, content, timestamp, raw_json)
37
+ VALUES (@platform, @chat_id, @chat_name, @msg_id, @sender_id, @sender_name, @content, @timestamp, @raw_json)
38
+ `);
39
+ let inserted = 0;
40
+ const tx = this.db.transaction((rows) => {
41
+ for (const row of rows) {
42
+ inserted += this.insertPrepared(stmt, row);
43
+ }
44
+ });
45
+ tx(messages);
46
+ return inserted;
47
+ }
48
+ insertMessage(message) {
49
+ const stmt = this.db.prepare(`
50
+ INSERT OR IGNORE INTO messages
51
+ (platform, chat_id, chat_name, msg_id, sender_id, sender_name, content, timestamp, raw_json)
52
+ VALUES (@platform, @chat_id, @chat_name, @msg_id, @sender_id, @sender_name, @content, @timestamp, @raw_json)
53
+ `);
54
+ return this.insertPrepared(stmt, message) === 1;
55
+ }
56
+ search(keyword, options = {}) {
57
+ const query = this.filteredQuery('content LIKE ?', [`%${keyword}%`], options);
58
+ return this.db.prepare(`${query.sql} ORDER BY timestamp DESC LIMIT ?`).all(...query.params, options.limit ?? 50);
59
+ }
60
+ searchRegex(pattern, options = {}) {
61
+ const regex = new RegExp(pattern, 'i');
62
+ const limit = options.limit ?? 50;
63
+ const query = this.filteredQuery('content IS NOT NULL', [], options);
64
+ const matches = [];
65
+ const rows = this.db.prepare(`${query.sql} ORDER BY timestamp DESC`).iterate(...query.params);
66
+ for (const row of rows) {
67
+ if (regex.test(row.content ?? ''))
68
+ matches.push(row);
69
+ if (matches.length >= limit)
70
+ break;
71
+ }
72
+ return matches;
73
+ }
74
+ getRecent(options = {}) {
75
+ const params = [];
76
+ const conditions = ['1=1'];
77
+ this.addFilters(conditions, params, options);
78
+ const limit = options.limit ?? 500;
79
+ return this.db.prepare(`
80
+ SELECT * FROM (
81
+ SELECT * FROM messages WHERE ${conditions.join(' AND ')} ORDER BY timestamp DESC LIMIT ?
82
+ ) ORDER BY timestamp ASC
83
+ `).all(...params, limit);
84
+ }
85
+ getToday(options = {}) {
86
+ const [start, nextStart] = this.todayRange(options.tzOffsetHours);
87
+ const params = [start, nextStart];
88
+ const conditions = ['timestamp >= ?', 'timestamp < ?'];
89
+ this.addFilters(conditions, params, { chatId: options.chatId });
90
+ return this.db.prepare(`
91
+ SELECT * FROM messages
92
+ WHERE ${conditions.join(' AND ')}
93
+ ORDER BY chat_name COLLATE NOCASE ASC, timestamp ASC
94
+ LIMIT ?
95
+ `).all(...params, options.limit ?? 500);
96
+ }
97
+ findChats(chat) {
98
+ const chats = this.getChats();
99
+ const numeric = Number.parseInt(chat, 10);
100
+ if (!Number.isNaN(numeric) && String(numeric) === chat.trim()) {
101
+ const id = canonicalChatId(numeric);
102
+ const matches = chats.filter((row) => row.chat_id === id);
103
+ if (matches.length > 0)
104
+ return matches;
105
+ }
106
+ const exact = chats.filter((row) => row.chat_name?.toLocaleLowerCase() === chat.toLocaleLowerCase());
107
+ if (exact.length > 0)
108
+ return exact;
109
+ return chats.filter((row) => row.chat_name?.toLocaleLowerCase().includes(chat.toLocaleLowerCase()));
110
+ }
111
+ resolveChatId(chat) {
112
+ const matches = this.findChats(chat);
113
+ return matches.length === 1 ? matches[0].chat_id : null;
114
+ }
115
+ getChats() {
116
+ return this.db.prepare(`
117
+ SELECT
118
+ m.chat_id,
119
+ (
120
+ SELECT latest.chat_name
121
+ FROM messages latest
122
+ WHERE latest.chat_id = m.chat_id
123
+ AND latest.chat_name IS NOT NULL
124
+ ORDER BY latest.timestamp DESC, latest.id DESC
125
+ LIMIT 1
126
+ ) as chat_name,
127
+ COUNT(*) as msg_count,
128
+ MIN(m.timestamp) as first_msg,
129
+ MAX(m.timestamp) as last_msg
130
+ FROM messages m GROUP BY m.chat_id ORDER BY msg_count DESC
131
+ `).all();
132
+ }
133
+ topSenders(options = {}) {
134
+ const params = [];
135
+ const conditions = ['(sender_id IS NOT NULL OR sender_name IS NOT NULL)'];
136
+ const since = options.hours == null ? undefined : new Date(Date.now() - options.hours * 60 * 60 * 1000).toISOString();
137
+ const filterOptions = { chatId: options.chatId, hours: options.hours, since };
138
+ this.addFilters(conditions, params, filterOptions);
139
+ const latestParams = [];
140
+ const latestConditions = [this.senderGroupPredicate('latest')];
141
+ this.addFilters(latestConditions, latestParams, filterOptions, 'latest');
142
+ return this.db.prepare(`
143
+ SELECT
144
+ (
145
+ SELECT latest.sender_name
146
+ FROM messages latest
147
+ WHERE ${latestConditions.join(' AND ')}
148
+ ORDER BY latest.timestamp DESC, latest.id DESC
149
+ LIMIT 1
150
+ ) as sender_name,
151
+ sender_id,
152
+ COUNT(*) as msg_count,
153
+ MIN(timestamp) as first_msg,
154
+ MAX(timestamp) as last_msg
155
+ FROM messages WHERE ${conditions.join(' AND ')}
156
+ GROUP BY CASE WHEN sender_id IS NULL THEN 'name:' || COALESCE(sender_name, '') ELSE 'id:' || CAST(sender_id AS TEXT) END
157
+ ORDER BY msg_count DESC LIMIT ?
158
+ `).all(...latestParams, ...params, options.limit ?? 20);
159
+ }
160
+ timeline(options = {}) {
161
+ const params = [];
162
+ const conditions = ['1=1'];
163
+ this.addFilters(conditions, params, { chatId: options.chatId, hours: options.hours });
164
+ const expr = options.granularity === 'hour' ? 'substr(timestamp, 1, 13)' : 'substr(timestamp, 1, 10)';
165
+ return this.db.prepare(`
166
+ SELECT ${expr} as period, COUNT(*) as msg_count
167
+ FROM messages WHERE ${conditions.join(' AND ')}
168
+ GROUP BY period ORDER BY period ASC
169
+ `).all(...params);
170
+ }
171
+ getLastMsgId(chatId) {
172
+ const row = this.db.prepare('SELECT MAX(msg_id) as value FROM messages WHERE chat_id = ?').get(canonicalChatId(chatId));
173
+ return row.value;
174
+ }
175
+ count(chatId) {
176
+ const row = chatId == null
177
+ ? this.db.prepare('SELECT COUNT(*) as value FROM messages').get()
178
+ : this.db.prepare('SELECT COUNT(*) as value FROM messages WHERE chat_id = ?').get(canonicalChatId(chatId));
179
+ return row.value;
180
+ }
181
+ getLatestTimestamp(chatId) {
182
+ const row = chatId == null
183
+ ? this.db.prepare('SELECT MAX(timestamp) as value FROM messages').get()
184
+ : this.db.prepare('SELECT MAX(timestamp) as value FROM messages WHERE chat_id = ?').get(canonicalChatId(chatId));
185
+ return row.value;
186
+ }
187
+ deleteChat(chatId) {
188
+ return this.db.prepare('DELETE FROM messages WHERE chat_id = ?').run(canonicalChatId(chatId)).changes;
189
+ }
190
+ close() {
191
+ this.db.close();
192
+ }
193
+ filteredQuery(firstCondition, firstParams, options) {
194
+ const conditions = [firstCondition];
195
+ const params = [...firstParams];
196
+ this.addFilters(conditions, params, options);
197
+ return { sql: `SELECT * FROM messages WHERE ${conditions.join(' AND ')}`, params };
198
+ }
199
+ insertPrepared(stmt, row) {
200
+ const { preview_jpeg_base64, ...persisted } = row;
201
+ void preview_jpeg_base64;
202
+ return stmt.run({
203
+ ...persisted,
204
+ chat_id: canonicalChatId(row.chat_id),
205
+ raw_json: row.raw_json == null ? null : JSON.stringify(row.raw_json),
206
+ }).changes;
207
+ }
208
+ addFilters(conditions, params, options, tableAlias) {
209
+ const prefix = tableAlias ? `${tableAlias}.` : '';
210
+ if (options.chatId != null) {
211
+ conditions.push(`${prefix}chat_id = ?`);
212
+ params.push(canonicalChatId(options.chatId));
213
+ }
214
+ if (options.sender) {
215
+ conditions.push(`${prefix}sender_name LIKE ?`);
216
+ params.push(`%${options.sender}%`);
217
+ }
218
+ if (options.hours != null) {
219
+ conditions.push(`${prefix}timestamp >= ?`);
220
+ params.push(options.since ?? new Date(Date.now() - options.hours * 60 * 60 * 1000).toISOString());
221
+ }
222
+ }
223
+ todayRange(tzOffsetHours) {
224
+ if (tzOffsetHours == null) {
225
+ const now = new Date();
226
+ const start = new Date(now.getFullYear(), now.getMonth(), now.getDate());
227
+ const nextStart = new Date(now.getFullYear(), now.getMonth(), now.getDate() + 1);
228
+ return [start.toISOString(), nextStart.toISOString()];
229
+ }
230
+ const offsetMs = tzOffsetHours * 60 * 60 * 1000;
231
+ const shifted = new Date(Date.now() + offsetMs);
232
+ const startMs = Date.UTC(shifted.getUTCFullYear(), shifted.getUTCMonth(), shifted.getUTCDate()) - offsetMs;
233
+ return [new Date(startMs).toISOString(), new Date(startMs + 24 * 60 * 60 * 1000).toISOString()];
234
+ }
235
+ senderGroupPredicate(alias) {
236
+ return `(
237
+ (${alias}.sender_id IS NOT NULL AND ${alias}.sender_id = messages.sender_id)
238
+ OR (
239
+ ${alias}.sender_id IS NULL
240
+ AND messages.sender_id IS NULL
241
+ AND COALESCE(${alias}.sender_name, '') = COALESCE(messages.sender_name, '')
242
+ )
243
+ )`;
244
+ }
245
+ }
246
+ //# sourceMappingURL=message-db.js.map