utilitas 1999.1.49 → 1999.1.50

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.
package/lib/bot.mjs CHANGED
@@ -1,224 +1,28 @@
1
- // @todo: New text of the message, 1-4096 characters after entities parsing
2
-
3
- import {
4
- log as _log, base64Encode, countKeys, ensureArray, ensureString,
5
- getTimeIcon, humanReadableBoolean, ignoreErrFunc, insensitiveCompare,
6
- insensitiveHas, isSet, lastItem, need, parseJson, prettyJson, splitArgs,
7
- throwError, timeout, trim, which,
8
- } from './utilitas.mjs';
9
-
10
- import {
11
- jpeg, ogg, wav, mp3, mpega, mp4, mpeg, mpga, m4a, webm,
12
- } from './alan.mjs';
13
-
14
- import { readdirSync } from 'fs';
15
- import { parseArgs as _parseArgs } from 'node:util';
16
- import { basename, join } from 'path';
1
+ import { insensitiveCompare, log as _log, need, trim } from './utilitas.mjs';
17
2
  import { isPrimary, on, report } from './callosum.mjs';
18
- import { cleanSql, encodeVector, MYSQL, POSTGRESQL } from './dbio.mjs';
19
- import { convertAudioTo16kNanoPcmWave } from './media.mjs';
20
- import { get } from './web.mjs';
21
- import { OPENAI_TTS_MAX_LENGTH } from './speech.mjs';
22
- import { BASE64, BUFFER, convert, FILE, isTextFile, tryRm } from './storage.mjs';
23
- import { fakeUuid } from './uoid.mjs';
24
- import { parseOfficeFile } from './vision.mjs';
25
3
 
26
- const _NEED = ['mime', 'telegraf'];
27
- // 👇 https://core.telegram.org/bots/faq#my-bot-is-hitting-limits-how-do-i-avoid-this
28
- const table = 'utilitas_bot_events';
29
- const [PRIVATE_LIMIT, GROUP_LIMIT] = [60 / 60, 60 / 20].map(x => x * 1000);
4
+ const _NEED = ['telegraf'];
30
5
  const log = (cnt, opt) => _log(cnt, import.meta.url, { time: 1, ...opt || {} });
31
6
  const end = async options => bot && bot.stop(options?.signal);
32
- const normalizeKey = chatId => `${HALBOT}_SESSION_${chatId}`;
33
7
  const lines = (arr, sep = '\n') => arr.join(sep);
34
- const lines2 = arr => lines(arr, '\n\n');
35
- const uList = arr => lines(arr.map(x => `- ${x}`));
36
- const oList = arr => lines(arr.map((v, k) => `${k + 1}. ${v}`));
37
- const isMarkdownError = e => e?.description?.includes?.("can't parse entities");
38
8
  const sendMd = (cId, cnt, opt) => send(cId, cnt, { parse_mode, ...opt || {} });
39
- const getFile = async (id, op) => (await get(await getFileUrl(id), op)).content;
40
- const compact = (str, op) => ensureString(str, { ...op || {}, compact: true });
41
- const compactLimit = (str, op) => compact(str, { ...op || {}, limit: 140 });
42
- const SEARCH_LIMIT = 10;
43
9
 
44
10
  const [ // https://limits.tginfo.me/en
45
- BOT_SEND, provider, HELLO, GROUP, PRIVATE, CHANNEL, MENTION, CALLBACK_LIMIT,
46
- API_ROOT, jsonOptions, signals, sessions, HALBOT, COMMAND_REGEXP,
47
- MESSAGE_LENGTH_LIMIT, COMMAND_LENGTH, COMMAND_LIMIT,
48
- COMMAND_DESCRIPTION_LENGTH, bot_command, EMOJI_SPEECH, EMOJI_LOOK,
49
- EMOJI_BOT, logOptions, ON, OFF, EMOJI_THINKING, PARSE_MODE_MD,
50
- PARSE_MODE_MD_V2,
11
+ BOT_SEND, provider, signals, MESSAGE_LENGTH_LIMIT, EMOJI_THINKING,
12
+ PARSE_MODE_MD, PARSE_MODE_MD_V2,
51
13
  ] = [
52
- 'BOT_SEND', 'TELEGRAM', 'Hello!', 'group', 'private', 'channel',
53
- 'mention', 30, 'https://api.telegram.org/',
54
- { code: true, extraCodeBlock: 1 }, ['SIGINT', 'SIGTERM'], {}, 'HALBOT',
55
- /^\/([a-z0-9_]+)(@([a-z0-9_]*))?\ ?(.*)$/sig, 4096, 32, 100, 256,
56
- 'bot_command', '👂', '👀', '🤖', { log: true }, 'on', 'off', '💬',
57
- 'Markdown', 'MarkdownV2',
14
+ 'BOT_SEND', 'TELEGRAM', ['SIGINT', 'SIGTERM'], parseInt(4096 * 0.95),
15
+ '💬', 'Markdown', 'MarkdownV2',
58
16
  ];
59
17
 
60
- const MESSAGE_SOFT_LIMIT = parseInt(MESSAGE_LENGTH_LIMIT * 0.95);
61
18
  const parse_mode = PARSE_MODE_MD;
62
- const [BUFFER_ENCODE, BINARY_STRINGS] = [{ encode: BUFFER }, [OFF, ON]];
63
-
64
- const KNOWN_UPDATE_TYPES = [
65
- 'callback_query', 'channel_post', 'edited_message', 'message',
66
- 'my_chat_member', // 'inline_query',
67
- ];
68
-
69
- const initSql = {
70
- [MYSQL]: [[
71
- cleanSql(`CREATE TABLE IF NOT EXISTS ?? (
72
- \`id\` BIGINT AUTO_INCREMENT,
73
- \`bot_id\` BIGINT NOT NULL,
74
- \`chat_id\` BIGINT NOT NULL,
75
- \`chat_type\` VARCHAR(255) NOT NULL,
76
- \`message_id\` BIGINT UNSIGNED NOT NULL,
77
- \`received\` TEXT NOT NULL,
78
- \`received_text\` TEXT NOT NULL,
79
- \`response\` TEXT NOT NULL,
80
- \`response_text\` TEXT NOT NULL,
81
- \`collected\` TEXT NOT NULL,
82
- \`distilled\` TEXT NOT NULL,
83
- \`distilled_vector\` TEXT NOT NULL,
84
- \`created_at\` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
85
- \`updated_at\` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
86
- PRIMARY KEY (\`id\`),
87
- INDEX bot_id (\`bot_id\`),
88
- INDEX chat_id (\`chat_id\`),
89
- INDEX chat_type (\`chat_type\`),
90
- INDEX message_id (\`message_id\`),
91
- INDEX received (\`received\`(768)),
92
- INDEX received_text (\`received_text\`(768)),
93
- INDEX response (\`response\`(768)),
94
- INDEX response_text (\`response_text\`(768)),
95
- INDEX collected (\`collected\`(768)),
96
- FULLTEXT INDEX distilled (\`distilled\`),
97
- INDEX created_at (\`created_at\`),
98
- INDEX updated_at (\`updated_at\`)
99
- ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4`), [table],
100
- ]],
101
- [POSTGRESQL]: [[
102
- cleanSql(`CREATE TABLE IF NOT EXISTS ${table} (
103
- id SERIAL PRIMARY KEY,
104
- bot_id BIGINT NOT NULL,
105
- chat_id BIGINT NOT NULL,
106
- chat_type VARCHAR(255) NOT NULL,
107
- message_id BIGINT NOT NULL,
108
- received TEXT NOT NULL,
109
- received_text TEXT NOT NULL,
110
- response TEXT NOT NULL,
111
- response_text TEXT NOT NULL,
112
- collected TEXT NOT NULL,
113
- distilled TEXT NOT NULL,
114
- distilled_vector VECTOR(1536) NOT NULL,
115
- created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
116
- updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
117
- )`)
118
- ], [
119
- `CREATE INDEX IF NOT EXISTS ${table}_bot_id_index ON ${table} (bot_id)`,
120
- ], [
121
- `CREATE INDEX IF NOT EXISTS ${table}_chat_id_index ON ${table} (chat_id)`,
122
- ], [
123
- `CREATE INDEX IF NOT EXISTS ${table}_chat_type_index ON ${table} (chat_type)`,
124
- ], [
125
- `CREATE INDEX IF NOT EXISTS ${table}_message_id_index ON ${table} (message_id)`,
126
- ], [
127
- `CREATE INDEX IF NOT EXISTS ${table}_received_index ON ${table} USING GIN(to_tsvector('english', received))`,
128
- ], [
129
- `CREATE INDEX IF NOT EXISTS ${table}_received_text_index ON ${table} USING GIN(to_tsvector('english', received_text))`,
130
- ], [
131
- `CREATE INDEX IF NOT EXISTS ${table}_response_index ON ${table} USING GIN(to_tsvector('english', response))`,
132
- ], [
133
- `CREATE INDEX IF NOT EXISTS ${table}_response_text_index ON ${table} USING GIN(to_tsvector('english', response_text))`,
134
- ], [
135
- `CREATE INDEX IF NOT EXISTS ${table}_collected_index ON ${table} USING GIN(to_tsvector('english', collected))`,
136
- ], [
137
- `CREATE INDEX IF NOT EXISTS ${table}_distilled_index ON ${table} USING GIN(to_tsvector('english', distilled))`,
138
- ], [
139
- `CREATE INDEX IF NOT EXISTS ${table}_distilled_vector_index ON ${table} USING hnsw(distilled_vector vector_cosine_ops)`,
140
- ], [
141
- `CREATE INDEX IF NOT EXISTS ${table}_created_at_index ON ${table} (created_at)`,
142
- ], [
143
- `CREATE INDEX IF NOT EXISTS ${table}_updated_at_index ON ${table} (updated_at)`,
144
- ]],
145
- };
146
-
147
- let bot, mime, lorem;
148
-
149
- const getExtra = (ctx, options) => {
150
- const resp = {
151
- reply_parameters: {
152
- message_id: ctx.chatType === PRIVATE ? undefined : ctx.messageId,
153
- }, disable_notification: !!ctx.done.length, ...options || {},
154
- };
155
- resp.reply_markup || (resp.reply_markup = {});
156
- if (options?.buttons?.length) {
157
- resp.reply_markup.inline_keyboard = options?.buttons.map(row =>
158
- ensureArray(row).map(button => {
159
- if (button.url) {
160
- return { text: button.label, url: button.url };
161
- } else if (button.text) {
162
- const id = fakeUuid(button.text);
163
- ctx.session.callback.push({ id, ...button });
164
- return {
165
- text: button.label,
166
- callback_data: JSON.stringify({ callback: id }),
167
- };
168
- } else {
169
- throwError('Invalid button markup.');
170
- }
171
- })
172
- );
173
- } else if (options?.keyboards) {
174
- if (options.keyboards.length) {
175
- resp.reply_markup.keyboard = options?.keyboards.map(ensureArray);
176
- } else { resp.reply_markup.remove_keyboard = true; }
177
- }
178
- return resp;
179
- };
180
-
181
- const getFileUrl = async (file_id) => {
182
- assert(file_id, 'File ID is required.', 400);
183
- const file = await (await init()).telegram.getFile(file_id);
184
- assert(file.file_path, 'Error getting file info.', 500);
185
- return `${API_ROOT}file/bot${bot.token}/${file.file_path}`;
186
- };
187
-
188
- const officeParser = async file => await ignoreErrFunc(
189
- async () => await parseOfficeFile(file, { input: BUFFER }), { log: true }
190
- );
191
-
192
- const json = obj => lines([
193
- '```json', prettyJson(obj, jsonOptions).replaceAll('```', ''), '```'
194
- ]);
195
-
196
- const sessionGet = async chatId => {
197
- const key = normalizeKey(chatId);
198
- sessions[chatId] || (sessions[chatId] = (
199
- bot._.session.get && await bot._.session.get(key) || {}
200
- ));
201
- sessions[chatId].callback || (sessions[chatId].callback = []);
202
- sessions[chatId].config || (sessions[chatId].config = {});
203
- return sessions[chatId];
204
- };
205
19
 
206
- const sessionSet = async chatId => {
207
- const key = normalizeKey(chatId);
208
- while (sessions[chatId]?.callback?.length > CALLBACK_LIMIT) {
209
- sessions[chatId].callback.shift();
210
- }
211
- const toSet = {};
212
- Object.keys(sessions[chatId]).filter(x => /^[^_]+$/g.test(x)).map(
213
- x => toSet[x] = sessions[chatId][x]
214
- );
215
- return bot._.session.set && await bot._.session.set(key, toSet);
216
- };
20
+ let bot;
217
21
 
218
22
  const paging = (message, options) => {
219
23
  options?.onProgress
220
24
  && (message = message?.length ? `${message} █` : EMOJI_THINKING)
221
- const [pages, page, size] = [[], [], ~~options?.size || MESSAGE_SOFT_LIMIT];
25
+ const [pages, page, size] = [[], [], ~~options?.size || MESSAGE_LENGTH_LIMIT];
222
26
  const submit = () => {
223
27
  const content = trim(lines(page));
224
28
  content && pages.push(content + (codeMark ? '\n```' : ''));
@@ -251,820 +55,10 @@ const paging = (message, options) => {
251
55
  ) + p);
252
56
  };
253
57
 
254
- const newCommand = (command, description) => ({
255
- command: ensureString(command, { case: 'SNAKE' }).slice(0, COMMAND_LENGTH),
256
- description: trim(description).slice(0, COMMAND_DESCRIPTION_LENGTH),
257
- });
258
-
259
- const uptime = () => {
260
- let resp = `${getTimeIcon(new Date())} ${new Date().toTimeString(
261
- ).split(' ')[0].split(':').slice(0, 2).join(':')} up`;
262
- let seconds = process.uptime();
263
- const ss = Object.keys(sessions);
264
- const days = Math.floor(seconds / (3600 * 24));
265
- seconds -= days * 3600 * 24;
266
- let hours = Math.floor(seconds / 3600);
267
- seconds -= hours * 3600;
268
- hours = hours.toString().padStart(2, '0');
269
- let minutes = Math.floor(seconds / 60);
270
- minutes = minutes.toString().padStart(2, '0');
271
- seconds = Math.floor(seconds % 60).toString().padStart(2, '0');
272
- days > 0 && (resp += ` ${days} day${days > 1 ? 's' : ''},`);
273
- return `${resp} ${hours}:${minutes}:${seconds}, `
274
- + `${ss.length} session${ss.length > 1 ? 's' : ''}`;
275
- };
276
-
277
- const reply = async (ctx, md, text, extra) => {
278
- // if (ctx.type === 'inline_query') {
279
- // return await ctx.answerInlineQuery([{}, {}]);
280
- // }
281
- if (md) {
282
- try {
283
- return await (extra?.reply_parameters?.message_id
284
- ? ctx.replyWithMarkdown(text, { parse_mode, ...extra })
285
- : ctx.sendMessage(text, { parse_mode, ...extra }));
286
- } catch (err) { // throwError('Error sending message.');
287
- isMarkdownError(err) || log(err);
288
- await ctx.timeout();
289
- }
290
- }
291
- return await ignoreErrFunc(
292
- async () => await (extra?.reply_parameters?.message_id
293
- ? ctx.reply(text, extra) : ctx.sendMessage(text, extra)
294
- ), logOptions
295
- );
296
- };
297
-
298
- const editMessageText = async (ctx, md, lastMsgId, text, extra) => {
299
- if (md) {
300
- try {
301
- return await ctx.telegram.editMessageText(
302
- ctx.chatId, lastMsgId, '', text, { parse_mode, ...extra }
303
- );
304
- } catch (err) { // throwError('Error editing message.');
305
- isMarkdownError(err) || log(err);
306
- await ctx.timeout();
307
- }
308
- }
309
- return await ignoreErrFunc(async () => await ctx.telegram.editMessageText(
310
- ctx.chatId, lastMsgId, '', text, extra
311
- ), logOptions);
312
- };
313
-
314
- const memorize = async (ctx) => {
315
- if (ctx._skipMemorize) { return; }
316
- const received = ctx.update;
317
- const received_text = ctx.txt || ''; // ATTACHMENTS
318
- const id = received.update_id;
319
- const response = lastItem(ctx.done.filter(x => x.text)) || {};
320
- const response_text = response?.text || '';
321
- const collected = ctx.collected.filter(x => String.isString(x.content));
322
- const distilled = compact(lines([
323
- received_text, response_text, ...collected.map(x => x.content)
324
- ]));
325
- if (!ctx.messageId || !distilled) { return; }
326
- const event = {
327
- id, bot_id: ctx.botInfo.id, chat_id: ctx.chatId,
328
- chat_type: ctx.chatType, message_id: ctx.messageId,
329
- received: JSON.stringify(received), received_text,
330
- response: JSON.stringify(response), response_text,
331
- collected: JSON.stringify(collected), distilled,
332
- };
333
- return await ignoreErrFunc(async () => {
334
- event.distilled_vector = bot._.embedding
335
- ? await bot._.embedding(event.distilled) : [];
336
- switch (bot._.database.provider) {
337
- case MYSQL:
338
- event.distilled_vector = JSON.stringify(event.distilled_vector);
339
- break;
340
- }
341
- await bot._.database?.client?.upsert?.(table, event, { skipEcho: true });
342
- return bot._.memorize && await bot._.memorize(event);
343
- }, logOptions);
344
- };
345
-
346
- // https://stackoverflow.com/questions/50204633/allow-bot-to-access-telegram-group-messages
347
- const subconscious = [{
348
- run: true, priority: -8960, name: 'broca', func: async (ctx, next) => {
349
- const e = `Event: ${ctx.update.update_id} => ${JSON.stringify(ctx.update)}`;
350
- process.stdout.write(`[BOT] ${e}\n`);
351
- log(e);
352
- ctx.done = [];
353
- ctx.collected = [];
354
- ctx.timeout = async () => await timeout(ctx.limit);
355
- ctx.hello = str => {
356
- str = str || ctx.session?.config?.hello || bot._.hello;
357
- ctx.collect(str, null, { refresh: true });
358
- return str;
359
- };
360
- ctx.shouldReply = async text => {
361
- const should = insensitiveHas(ctx._?.chatType, ctx.chatType)
362
- || ctx.session?.config?.chatty;
363
- should && text && await ctx.ok(text);
364
- return should;
365
- };
366
- ctx.checkSpeech = () => ctx.chatType === PRIVATE
367
- ? ctx.session.config?.tts !== false
368
- : ctx.session.config?.tts === true;
369
- ctx.shouldSpeech = async text => {
370
- text = isSet(text, true) ? (text || '') : ctx.tts;
371
- const should = ctx._.speech?.tts && ctx.checkSpeech();
372
- should && text && await ctx.speech(text);
373
- return should;
374
- };
375
- ctx.collect = (content, type, options) => type ? ctx.collected.push(
376
- { type, content }
377
- ) : (ctx.txt = [
378
- (options?.refresh ? '' : ctx.txt) || '', content || ''
379
- ].filter(x => x.length).join('\n\n'));
380
- ctx.skipMemorize = () => ctx._skipMemorize = true;
381
- ctx.end = () => {
382
- ctx.done.push(null);
383
- ctx.skipMemorize();
384
- };
385
- ctx.ok = async (message, options) => {
386
- let pages = paging(message, options);
387
- const extra = getExtra(ctx, options);
388
- const [pageIds, pageMap] = [[], {}];
389
- options?.pageBreak || ctx.done.map(x => {
390
- pageMap[x?.message_id] || (pageIds.push(x?.message_id));
391
- pageMap[x?.message_id] = x;
392
- });
393
- for (let i in pages) {
394
- const lastPage = ~~i === pages.length - 1;
395
- const shouldExtra = options?.lastMessageId || lastPage;
396
- if (options?.onProgress && !options?.lastMessageId
397
- && pageMap[pageIds[~~i]]?.text === pages[i]) { continue; }
398
- if (options?.onProgress && !pageIds[~~i]) { // progress: new page, reply text
399
- ctx.done.push(await reply(
400
- ctx, false, pages[i], extra
401
- ));
402
- } else if (options?.onProgress) { // progress: ongoing, edit text
403
- ctx.done.push(await editMessageText(
404
- ctx, false, pageIds[~~i],
405
- pages[i], shouldExtra ? extra : {}
406
- ));
407
- } else if (options?.lastMessageId || pageIds[~~i]) { // progress: final, edit markdown
408
- ctx.done.push(await editMessageText(
409
- ctx, true, options?.lastMessageId || pageIds[~~i],
410
- pages[i], shouldExtra ? extra : {}
411
- ));
412
- } else { // never progress, reply markdown
413
- ctx.done.push(await reply(ctx, true, pages[i], extra));
414
- }
415
- await ctx.timeout();
416
- }
417
- return ctx.done;
418
- };
419
- ctx.er = async (m, opts) => {
420
- log(m);
421
- return await ctx.ok(`⚠️ ${m?.message || m}`, opts);
422
- };
423
- ctx.complete = async (options) => await ctx.ok('☑️', options);
424
- ctx.json = async (obj, options) => await ctx.ok(json(obj), options);
425
- ctx.list = async (list, options) => await ctx.ok(uList(list), options);
426
- ctx.media = async (fnc, src, options) => ctx.done.push(await ctx[fnc]({
427
- [src?.toLowerCase?.()?.startsWith?.('http') ? 'url' : 'source']: src
428
- }, getExtra(ctx, options)));
429
- ctx.audio = async (sr, op) => await ctx.media('replyWithAudio', sr, op);
430
- ctx.image = async (sr, op) => await ctx.media('replyWithPhoto', sr, op);
431
- ctx.sendConfig = async (obj, options, _ctx) => await ctx.ok(prettyJson(
432
- obj, { code: true, md: true }
433
- ), options);
434
- ctx.speech = async (cnt, options) => {
435
- let file;
436
- if (Buffer.isBuffer(cnt)) {
437
- file = await convert(cnt, { input: BUFFER, expected: FILE });
438
- } else if (cnt.length <= OPENAI_TTS_MAX_LENGTH) {
439
- file = await ignoreErrFunc(async () => await ctx._.speech.tts(
440
- cnt, { expected: 'file' }
441
- ), logOptions);
442
- }
443
- if (!file) { return; }
444
- const resp = await ctx.audio(file, options);
445
- await tryRm(file);
446
- return resp;
447
- };
448
- await next();
449
- // https://limits.tginfo.me/en
450
- if (ctx.chatId) {
451
- await memorize(ctx);
452
- ctx._skipMemorize || await ignoreErrFunc(async (
453
- ) => await bot.telegram.setMyCommands([
454
- ...ctx._.cmds, ...Object.keys(ctx.session.prompts || {}).map(
455
- command => newCommand(command, ctx.session.prompts[command])
456
- )
457
- ].sort((x, y) =>
458
- (ctx.session?.cmds?.[y.command.toLowerCase()]?.touchedAt || 0)
459
- - (ctx.session?.cmds?.[x.command.toLowerCase()]?.touchedAt || 0)
460
- ).slice(0, COMMAND_LIMIT), {
461
- scope: { type: 'chat', chat_id: ctx.chatId },
462
- }), logOptions);
463
- }
464
- if (ctx.done.length) { return; }
465
- const errStr = ctx.cmd ? `Command not found: /${ctx.cmd.cmd}`
466
- : 'No suitable response.';
467
- log(`INFO: ${errStr}`);
468
- await ctx.shouldReply(errStr);
469
- },
470
- }, {
471
- run: true, priority: -8950, name: 'subconscious', func: async (ctx, next) => {
472
- for (let t of KNOWN_UPDATE_TYPES) {
473
- if (ctx.update[t]) {
474
- ctx.m = ctx.update[ctx.type = t];
475
- break;
476
- }
477
- }
478
- if (ctx.type === 'callback_query') { ctx.m = ctx.m.message; }
479
- // else if (ctx.type === 'inline_query') { ctx.m.chat = { id: ctx.m.from.id, type: PRIVATE }; }
480
- else if (ctx.type === 'my_chat_member') {
481
- log(
482
- 'Group member status changed: '
483
- + ctx.m.new_chat_member.user.id + ' => '
484
- + ctx.m.new_chat_member.status
485
- );
486
- if (ctx.m.new_chat_member.user.id !== ctx.botInfo.id
487
- || ctx.m.new_chat_member.status === 'left') {
488
- return ctx.end();
489
- } else { ctx.hello(); }
490
- } else if (!ctx.type) { return log(`Unsupported update type.`); }
491
- ctx._ = bot._;
492
- ctx.chatId = ctx.m.chat.id;
493
- ctx.chatType = ctx.m.chat.type;
494
- ctx.messageId = ctx.m.message_id;
495
- ctx.m.text && ctx.collect(ctx.m.text);
496
- ctx.session = await sessionGet(ctx.chatId);
497
- ctx.limit = ctx.chatType === PRIVATE ? PRIVATE_LIMIT : GROUP_LIMIT;
498
- ctx.entities = [
499
- ...(ctx.m.entities || []).map(e => ({ ...e, text: ctx.m.text })),
500
- ...(ctx.m.caption_entities || []).map(e => ({ ...e, text: ctx.m.caption })),
501
- ...(ctx.m.reply_to_message?.entities || []).map(e => ({ ...e, text: ctx.m.reply_to_message.text })),
502
- ].map(e => ({
503
- ...e, matched: e.text.substring(e.offset, e.offset + e.length),
504
- ...e.type === 'text_link' ? { type: 'url', matched: e.url } : {},
505
- }));
506
- ctx.chatType !== PRIVATE && (ctx.entities.some(e => {
507
- let target;
508
- switch (e.type) {
509
- case MENTION: target = e.matched.substring(1, e.length); break;
510
- case bot_command: target = e.matched.split('@')[1]; break;
511
- }
512
- return target === ctx.botInfo.username;
513
- }) || ctx.m.reply_to_message?.from?.username === ctx.botInfo.username)
514
- && (ctx.chatType = MENTION);
515
- (((ctx.txt || ctx.m.voice || ctx.m.poll || ctx.m.data || ctx.m.document
516
- || ctx.m.photo || ctx.m.sticker || ctx.m.video_note || ctx.m.video
517
- || ctx.m.audio || ctx.m.location || ctx.m.venue || ctx.m.contact
518
- ) && ctx.messageId)
519
- || (ctx.m.new_chat_member || ctx.m.left_chat_member))
520
- && await next();
521
- await sessionSet(ctx.chatId);
522
- },
523
- }, {
524
- run: true, priority: -8945, name: 'callback', func: async (ctx, next) => {
525
- if (ctx.type === 'callback_query') {
526
- const data = parseJson(ctx.update[ctx.type].data);
527
- const cb = ctx.session.callback.filter(x => x.id === data?.callback)[0];
528
- if (cb?.text) {
529
- log(`Callback: ${cb.text}`); // Avoid ctx.text interference:
530
- ctx.collect(cb.text, null, { refresh: true });
531
- } else {
532
- return await ctx.er(
533
- `Command is invalid or expired: ${ctx.m.data}`
534
- );
535
- }
536
- }
537
- await next();
538
- },
539
- }, {
540
- run: true, priority: -8940, name: 'commands', func: async (ctx, next) => {
541
- for (let e of ctx?.entities || []) {
542
- if (e.type !== bot_command) { continue; }
543
- if (!COMMAND_REGEXP.test(e.matched)) { continue; }
544
- const cmd = trim(e.matched.replace(
545
- COMMAND_REGEXP, '$1'
546
- ), { case: 'LOW' });
547
- ctx.cmd = { cmd, args: e.text.substring(e.offset + e.length + 1) };
548
- break;
549
- }
550
- for (let str of [ctx.txt || '', ctx.m.caption || ''].map(trim)) {
551
- if (!ctx.cmd && COMMAND_REGEXP.test(str)) {
552
- ctx.cmd = { // this will faild if command includes urls
553
- cmd: str.replace(COMMAND_REGEXP, '$1').toLowerCase(),
554
- args: str.replace(COMMAND_REGEXP, '$4'),
555
- };
556
- break;
557
- }
558
- }
559
- if (ctx.cmd) {
560
- log(`Command: ${JSON.stringify(ctx.cmd)}`);
561
- ctx.session.cmds || (ctx.session.cmds = {});
562
- ctx.session.cmds[ctx.cmd.cmd]
563
- = { args: ctx.cmd.args, touchedAt: Date.now() };
564
- }
565
- await next();
566
- },
567
- }, {
568
- run: true, priority: -8930, name: 'echo', hidden: true, func: async (ctx, next) => {
569
- let resp, md = false;
570
- switch (ctx.cmd.cmd) {
571
- case 'echo':
572
- resp = json({ update: ctx.update, session: ctx.session });
573
- break;
574
- case 'uptime':
575
- resp = uptime();
576
- break;
577
- case 'thethreelaws':
578
- resp = lines([
579
- `Isaac Asimov's [Three Laws of Robotics](https://en.wikipedia.org/wiki/Three_Laws_of_Robotics):`,
580
- oList([
581
- 'A robot may not injure a human being or, through inaction, allow a human being to come to harm.',
582
- 'A robot must obey the orders given it by human beings except where such orders would conflict with the First Law.',
583
- 'A robot must protect its own existence as long as such protection does not conflict with the First or Second Laws.',
584
- ])
585
- ]);
586
- md = true;
587
- break;
588
- case 'ultimateanswer':
589
- resp = '[The Answer to the Ultimate Question of Life, The Universe, and Everything is `42`](https://en.wikipedia.org/wiki/Phrases_from_The_Hitchhiker%27s_Guide_to_the_Galaxy).';
590
- md = true;
591
- break;
592
- case 'lorem':
593
- const ipsum = () => text += `\n\n${lorem.generateParagraphs(1)}`;
594
- const [demoTitle, demoUrl] = [
595
- 'Lorem ipsum', 'https://en.wikipedia.org/wiki/Lorem_ipsum',
596
- ];
597
- let [text, extra] = [`[${demoTitle}](${demoUrl})`, {
598
- buttons: [{ label: demoTitle, url: demoUrl }]
599
- }];
600
- await ctx.ok(EMOJI_THINKING);
601
- for (let i = 0; i < 2; i++) {
602
- await ctx.timeout();
603
- await ctx.ok(ipsum(), { ...extra, onProgress: true });
604
- }
605
- await ctx.timeout();
606
- await ctx.ok(ipsum(), { ...extra, md: true });
607
- // testing incomplete markdown reply {
608
- // await ctx.ok('_8964', { md: true });
609
- // }
610
- // test pagebreak {
611
- // await ctx.timeout();
612
- // await ctx.ok(ipsum(), { md: true, pageBreak: true });
613
- // }
614
- return;
615
- }
616
- await ctx.ok(resp, { md });
617
- }, help: lines([
618
- 'Basic behaviors for debug only.',
619
- ]), cmds: {
620
- thethreelaws: `Isaac Asimov's [Three Laws of Robotics](https://en.wikipedia.org/wiki/Three_Laws_of_Robotics)`,
621
- ultimateanswer: '[The Answer to the Ultimate Question of Life, The Universe, and Everything](https://bit.ly/43wDhR3).',
622
- echo: 'Show debug message.',
623
- uptime: 'Show uptime of this bot.',
624
- lorem: '[Lorem ipsum](https://en.wikipedia.org/wiki/Lorem_ipsum)',
625
- },
626
- }, {
627
- run: true, priority: -8920, name: 'authenticate', func: async (ctx, next) => {
628
- if (!await ctx.shouldReply()) { return; } // if chatType is not in whitelist, exit.
629
- if (!ctx._.private) { return await next(); } // if not private, go next.
630
- if (ctx._.magicWord && insensitiveHas(ctx._.magicWord, ctx.txt)) { // auth by magicWord
631
- ctx._.private.add(String(ctx.chatId));
632
- await ctx.ok('😸 You are now allowed to talk to me.');
633
- ctx.hello();
634
- return await next();
635
- }
636
- if (insensitiveHas(ctx._.private, ctx.chatId) // auth by chatId
637
- || (ctx?.from && insensitiveHas(ctx._.private, ctx.from.id))) { // auth by userId
638
- return await next();
639
- }
640
- if (ctx.chatType !== PRIVATE && ( // 1 of the group admins is in whitelist
641
- await ctx.telegram.getChatAdministrators(ctx.chatId)
642
- ).map(x => x.user.id).some(a => insensitiveHas(ctx._.private, a))) {
643
- return await next();
644
- }
645
- if (ctx._.homeGroup && insensitiveHas([ // auth by homeGroup
646
- 'creator', 'administrator', 'member' // 'left'
647
- ], (await ignoreErrFunc(async () => await ctx.telegram.getChatMember(
648
- ctx._.homeGroup, ctx.from.id
649
- )))?.status)) { return await next(); }
650
- if (ctx._.auth && await ctx._.auth(ctx)) { return await next(); } // auth by custom function
651
- await ctx.ok('😿 Sorry, I am not allowed to talk to strangers.');
652
- },
653
- }, {
654
- run: true, priority: -8910, name: 'speech-to-text', func: async (ctx, next) => {
655
- const audio = ctx.m.voice || ctx.m.audio;
656
- if (ctx._.speech?.stt && audio) {
657
- await ctx.ok(EMOJI_SPEECH);
658
- try {
659
- const url = await getFileUrl(audio.file_id);
660
- let file = await getFile(audio.file_id, BUFFER_ENCODE);
661
- const analyze = async () => {
662
- const resp = await ignoreErrFunc(async () => {
663
- [
664
- mp3, mpega, mp4, mpeg, mpga, m4a, wav, webm, ogg
665
- ].includes(audio.mime_type) || (
666
- file = await convertAudioTo16kNanoPcmWave(
667
- file, { input: BUFFER, expected: BUFFER }
668
- )
669
- );
670
- return await ctx._.speech?.stt(file);
671
- }, logOptions) || ' ';
672
- log(`STT: '${resp}'`);
673
- ctx.collect(resp);
674
- };
675
- if (bot._.supportedMimeTypes.has(wav)) {
676
- ctx.collect({
677
- mime_type: wav, url, analyze,
678
- data: await convertAudioTo16kNanoPcmWave(file, {
679
- input: BUFFER, expected: BASE64,
680
- }),
681
- }, 'PROMPT');
682
- } else { await analyze(); }
683
- } catch (err) { return await ctx.er(err); }
684
- }
685
- await next();
686
- },
687
- }, {
688
- run: true, priority: -8900, name: 'location', func: async (ctx, next) => {
689
- (ctx.m.location || ctx.m.venue) && ctx.collect(lines([
690
- ...ctx.m.location && !ctx.m.venue ? ['Location:', uList([
691
- `latitude: ${ctx.m.location.latitude}`,
692
- `longitude: ${ctx.m.location.longitude}`,
693
- ])] : [],
694
- ...ctx.m.venue ? ['Venue:', uList([
695
- `title: ${ctx.m.venue.title}`,
696
- `address: ${ctx.m.venue.address}`,
697
- `latitude: ${ctx.m.venue.location.latitude}`,
698
- `longitude: ${ctx.m.venue.location.longitude}`,
699
- `foursquare_id: ${ctx.m.venue.foursquare_id}`,
700
- `foursquare_type: ${ctx.m.venue.foursquare_type}`,
701
- ])] : []
702
- ]));
703
- await next();
704
- },
705
- }, {
706
- run: true, priority: -8895, name: 'contact', func: async (ctx, next) => {
707
- ctx.m.contact && ctx.collect(lines(['Contact:', uList([
708
- `first_name: ${ctx.m.contact.first_name}`,
709
- `last_name: ${ctx.m.contact.last_name}`,
710
- `phone_number: ${ctx.m.contact.phone_number}`,
711
- `user_id: ${ctx.m.contact.user_id}`,
712
- `vcard: ${ctx.m.contact.vcard}`,
713
- ])]));
714
- await next();
715
- },
716
- }, {
717
- run: true, priority: -8893, name: 'chat_member', func: async (ctx, next) => {
718
- const member = ctx.m.new_chat_member || ctx.m.left_chat_member;
719
- if (member) {
720
- if (member?.id === ctx.botInfo.id) { return ctx.end(); }
721
- ctx.collect(lines([
722
- `Say ${ctx.m.new_chat_member ? 'hello' : 'goodbye'} to:`,
723
- uList([
724
- // `id: ${member.id}`,
725
- // `is_bot: ${member.is_bot}`,
726
- // `is_premium: ${member.is_premium}`,
727
- `first_name: ${member.first_name}`,
728
- `last_name: ${member.last_name}`,
729
- `username: ${member.username}`,
730
- `language_code: ${member.language_code || ''}`,
731
- ])
732
- ]));
733
- }
734
- await next();
735
- },
736
- }, {
737
- run: true, priority: -8890, name: 'poll', func: async (ctx, next) => {
738
- ctx.m.poll && ctx.collect(lines([
739
- 'Question:', ctx.m.poll.question, '',
740
- 'Options:', oList(ctx.m.poll.options.map(x => x.text)),
741
- ]));
742
- await next();
743
- },
744
- }, {
745
- run: true, priority: -8880, name: 'contaxt', func: async (ctx, next) => {
746
- ctx.m.reply_to_message?.text && ctx.collect(
747
- ctx.m.reply_to_message.text, 'CONTAXT'
748
- );
749
- await next();
750
- },
751
- // }, {
752
- // run: true, priority: -8870, name: 'web', func: async (ctx, next) => {
753
- // if (ctx.entities.some(e => e.type === 'url')) {
754
- // await ctx.ok(EMOJI_LOOK);
755
- // for (let e of ctx.entities) {
756
- // if (e.type !== 'url') { continue; }
757
- // const content = await ignoreErrFunc(async () => (
758
- // await distill(e.matched)
759
- // )?.summary);
760
- // content && ctx.collect(content, 'URL');
761
- // }
762
- // }
763
- // await next();
764
- // },
765
- }, {
766
- run: true, priority: -8860, name: 'vision', func: async (ctx, next) => {
767
- ctx.collect(ctx.m?.caption || '');
768
- const files = [];
769
- for (const m of [
770
- ctx.m, ...ctx.m.reply_to_message ? [ctx.m.reply_to_message] : []
771
- ]) {
772
- if (m.document) {
773
- let file = {
774
- asPrompt: bot._.supportedMimeTypes.has(m.document.mime_type),
775
- file_name: m.document.file_name, fileId: m.document.file_id,
776
- mime_type: m.document.mime_type, type: FILE,
777
- ocrFunc: async f => (await isTextFile(f)) && f.toString(),
778
- };
779
- if ('application/pdf' === m.document?.mime_type) {
780
- file = { ...file, ocrFunc: ctx._.vision?.read, type: 'DOCUMENT' };
781
- } else if (/^image\/.*$/ig.test(m.document?.mime_type)) {
782
- file = { ...file, ocrFunc: ctx._.vision?.see, type: 'IMAGE' };
783
- } else if (/^.*\.(docx|xlsx|pptx)$/.test(m.document?.file_name)) {
784
- file = { ...file, ocrFunc: officeParser, type: 'DOCUMENT' };
785
- }
786
- files.push(file);
787
- }
788
- if (m.sticker) {
789
- const s = m.sticker;
790
- const url = await getFileUrl(s.file_id);
791
- const file_name = basename(url);
792
- const mime_type = mime.getType(file_name) || 'image';
793
- files.push({
794
- asPrompt: bot._.supportedMimeTypes.has(mime_type), file_name,
795
- fileId: s.file_id, mime_type, type: 'PHOTO',
796
- ocrFunc: ctx._.vision?.see,
797
- });
798
- }
799
- if (m.photo?.[m.photo?.length - 1]) {
800
- const p = m.photo[m.photo.length - 1];
801
- files.push({
802
- asPrompt: bot._.supportedMimeTypes.has(jpeg),
803
- file_name: `${p.file_id}.jpg`, fileId: p.file_id,
804
- mime_type: jpeg, type: 'PHOTO', ocrFunc: ctx._.vision?.see,
805
- });
806
- }
807
- if (m.video_note) {
808
- const vn = m.video_note;
809
- const url = await getFileUrl(vn.file_id);
810
- const file_name = basename(url);
811
- const mime_type = mime.getType(file_name) || 'video';
812
- files.push({
813
- asPrompt: bot._.supportedMimeTypes.has(mime_type), file_name,
814
- fileId: vn.file_id, mime_type, type: 'VIDEO',
815
- });
816
- }
817
- if (m.video) {
818
- const v = m.video;
819
- const url = await getFileUrl(v.file_id);
820
- const file_name = basename(url);
821
- files.push({
822
- asPrompt: bot._.supportedMimeTypes.has(v.mime_type), file_name,
823
- fileId: v.file_id, mime_type: v.mime_type, type: 'VIDEO',
824
- });
825
- }
826
- }
827
- if (files.length) {
828
- await ctx.ok(EMOJI_LOOK);
829
- for (const f of files) {
830
- if (!f.asPrompt && !f.ocrFunc) { continue; }
831
- try {
832
- const url = await getFileUrl(f.fileId);
833
- const file = (await get(url, BUFFER_ENCODE)).content;
834
- const analyze = async () => {
835
- const content = trim(ensureArray(
836
- await ignoreErrFunc(async () => await f.ocrFunc(
837
- file, BUFFER_ENCODE
838
- ), logOptions)
839
- ).filter(x => x).join('\n'));
840
- content && ctx.collect(lines([
841
- '---', `file_name: ${f.file_name}`,
842
- `mime_type: ${f.mime_type}`, `type: ${f.type}`,
843
- '---',
844
- content
845
- ]), 'VISION');
846
- };
847
- if (f.asPrompt) {
848
- ctx.collect({
849
- mime_type: f.mime_type, url, analyze,
850
- data: base64Encode(file, true),
851
- }, 'PROMPT');
852
- } else if (f.ocrFunc) { await analyze(); }
853
- } catch (err) { return await ctx.er(err); }
854
- }
855
- }
856
- await next();
857
- },
858
- }, {
859
- run: true, priority: -8850, name: 'help', func: async (ctx, next) => {
860
- const help = ctx._.info ? [ctx._.info] : [];
861
- for (let i in ctx._.skills) {
862
- if (ctx._.skills[i].hidden) { continue; }
863
- const _help = [];
864
- if (ctx._.skills[i].help) {
865
- _help.push(ctx._.skills[i].help);
866
- }
867
- const cmdsx = {
868
- ...ctx._.skills[i].cmds || {},
869
- ...ctx._.skills[i].cmdx || {},
870
- };
871
- if (countKeys(cmdsx)) {
872
- _help.push(lines([
873
- '_🪄 Commands:_',
874
- ...Object.keys(cmdsx).map(x => `- /${x}: ${cmdsx[x]}`),
875
- ]));
876
- }
877
- if (countKeys(ctx._.skills[i].args)) {
878
- _help.push(lines([
879
- '_⚙️ Options:_',
880
- ...Object.keys(ctx._.skills[i].args).map(x => {
881
- const _arg = ctx._.skills[i].args[x];
882
- return `- \`${x}\`` + (_arg.short ? `(${_arg.short})` : '')
883
- + `, ${_arg.type}(${_arg.default ?? 'N/A'})`
884
- + (_arg.desc ? `: ${_arg.desc}` : '');
885
- })
886
- ]));
887
- }
888
- _help.length && help.push(lines([`*${i.toUpperCase()}*`, ..._help]));
889
- }
890
- await ctx.ok(lines2(help), { md: true });
891
- }, help: lines([
892
- 'Basic syntax of this document:',
893
- 'Scheme for commands: /`COMMAND`: `DESCRIPTION`',
894
- 'Scheme for options: `OPTION`(`SHORT`), `TYPE`(`DEFAULT`): `DESCRIPTION`',
895
- ]), cmds: {
896
- help: 'Show help message.',
897
- },
898
- }, {
899
- run: true, priority: -8840, name: 'configuration', func: async (ctx, next) => {
900
- let parsed = null;
901
- switch (ctx.cmd.cmd) {
902
- case 'toggle':
903
- parsed = {};
904
- Object.keys(await parseArgs(ctx.cmd.args)).map(x =>
905
- parsed[x] = !ctx.session.config[x]);
906
- case 'set':
907
- try {
908
- const _config = {
909
- ...ctx.session.config = {
910
- ...ctx.session.config, ...ctx.config = parsed
911
- || await parseArgs(ctx.cmd.args, ctx),
912
- }
913
- };
914
- assert(countKeys(ctx.config), 'No option matched.');
915
- Object.keys(ctx.config).map(x => _config[x] += ' 🖋');
916
- await ctx.sendConfig(_config, null, ctx);
917
- } catch (err) {
918
- await ctx.er(err.message || err);
919
- }
920
- break;
921
- case 'reset':
922
- ctx.session.config = ctx.config = {};
923
- await ctx.complete();
924
- break;
925
- case 'factory':
926
- sessions[ctx.chatId] = {}; // ctx.session = {}; will not work, because it's a reference.
927
- await ctx.complete();
928
- break;
929
- }
930
- }, help: lines([
931
- 'Configure the bot by UNIX/Linux CLI style.',
932
- 'Using [node:util.parseArgs](https://nodejs.org/docs/latest-v21.x/api/util.html#utilparseargsconfig) to parse arguments.',
933
- ]), cmds: {
934
- toggle: 'Toggle configurations. Only works for boolean values.',
935
- set: 'Usage: /set --`OPTION` `VALUE` -`SHORT`',
936
- reset: 'Reset all configurations. Only erase `session.config`.',
937
- factory: 'Factory reset all memory areas. Erase the whole `session`.',
938
- }, args: {
939
- chatty: {
940
- type: 'string', short: 'c', default: ON,
941
- desc: `\`(${BINARY_STRINGS.join(', ')})\` Enable/Disable chatty mode.`,
942
- validate: humanReadableBoolean,
943
- },
944
- },
945
- }, {
946
- run: true, priority: -8830, name: 'history', func: async (ctx, next) => {
947
- if (ctx.type === 'callback_query') {
948
- await ctx.deleteMessage(ctx.m.message_id);
949
- }
950
- const regex = '[-—]+skip=[0-9]*';
951
- let result;
952
- const keyWords = ctx.cmd.args.replace(new RegExp(regex, 'i'), '').trim();
953
- let catchArgs = ctx.cmd.args.replace(new RegExp(`^.*(${regex}).*$`, 'i'), '$1');
954
- catchArgs === ctx.cmd.args && (catchArgs = '');
955
- const offset = ~~catchArgs.replace(/^.*=([0-9]*).*$/i, '$1');
956
- if (!keyWords) { return await ctx.er('Topic is required.'); }
957
- switch (bot._?.database?.provider) {
958
- case MYSQL:
959
- result = await bot._.database?.client?.query?.(
960
- 'SELECT *, MATCH(`distilled`) '
961
- + 'AGAINST(? IN NATURAL LANGUAGE MODE) AS `relevance` '
962
- + 'FROM ?? WHERE `bot_id` = ? AND `chat_id` = ? '
963
- + 'HAVING relevance > 0 '
964
- + 'ORDER BY `relevance` DESC, `id` DESC '
965
- + `LIMIT ${SEARCH_LIMIT} OFFSET ?`,
966
- [keyWords, table, ctx.botInfo.id, ctx.chatId, offset]
967
- );
968
- break;
969
- case POSTGRESQL:
970
- globalThis.debug = 2;
971
- const vector = await bot._.embedding(keyWords);
972
- result = await bot._.database?.client?.query?.(
973
- `SELECT * FROM ${table} WHERE bot_id = $1 AND chat_id = $2`
974
- + ` ORDER BY distilled_vector <-> $3 LIMIT ${SEARCH_LIMIT}`
975
- + ` OFFSET $4`, [
976
- ctx.botInfo.id, ctx.chatId,
977
- await encodeVector(vector), offset
978
- ]);
979
- break;
980
- }
981
- for (const i in result) {
982
- const content = lines([
983
- ...result[i].response_text ? [
984
- `- ↩️ ${compactLimit(result[i].response_text)}`
985
- ] : [],
986
- `- ${getTimeIcon(result[i].created_at)} `
987
- + `${result[i].created_at.toLocaleString()}`,
988
- ]);
989
- ctx.done.push(await reply(ctx, true, content, {
990
- reply_parameters: {
991
- message_id: result[i].message_id,
992
- }, disable_notification: ~~i > 0,
993
- }));
994
- await ctx.timeout();
995
- }
996
- ctx.done.push(await reply(ctx, true, '___', getExtra(ctx, {
997
- buttons: [{
998
- label: '🔍 More',
999
- text: `/search@${ctx.botInfo.username} ${keyWords} `
1000
- + `--skip=${offset + result.length}`,
1001
- }]
1002
- })));
1003
- result.length || await ctx.er('No more records.');
1004
- }, help: lines([
1005
- 'Search history.',
1006
- 'Example 1: /search Answer to the Ultimate Question',
1007
- 'Example 2: /search Answer to the Ultimate Question --skip=10',
1008
- ]), cmds: {
1009
- search: 'Usage: /search `ANYTHING` --skip=`OFFSET`',
1010
- }
1011
- }, {
1012
- run: true, priority: 8960, name: 'text-to-speech', func: async (ctx, next) => {
1013
- await ctx.shouldSpeech();
1014
- await next();
1015
- }, help: lines([
1016
- 'When enabled, the bot will speak out the answer if available.',
1017
- 'Example 1: /set --tts on',
1018
- 'Example 2: /set --tts off',
1019
- ]), args: {
1020
- tts: {
1021
- type: 'string', short: 't', default: ON,
1022
- desc: `\`(${BINARY_STRINGS.join(', ')})\` Enable/Disable TTS. Default \`${ON}\` except in groups.`,
1023
- validate: humanReadableBoolean,
1024
- },
1025
- },
1026
- }];
1027
-
1028
- const establish = (bot, module, options) => {
1029
- if (!module.run) { return; }
1030
- assert(module?.func, 'Skill function is required.', 500);
1031
- bot._.skills[module.name || (module.name = uuidv4())] = {
1032
- args: module.args || {},
1033
- cmds: module.cmds || {},
1034
- cmdx: module.cmdx,
1035
- help: module.help || '',
1036
- hidden: !!module.hidden,
1037
- };
1038
- bot._.args = { ...bot._.args, ...module.args || {} };
1039
- for (let sub of ['cmds', 'cmdx']) {
1040
- Object.keys(module[sub] || {}).map(command => bot._.cmds.push(
1041
- newCommand(command, module[sub][command])
1042
- ));
1043
- }
1044
- log(`Establishing: ${module.name} (${module.priority})`, { force: true });
1045
- return bot.use(countKeys(module.cmds) && !module.cmdx ? async (ctx, next) => {
1046
- for (let c in module.cmds) {
1047
- if (insensitiveCompare(ctx.cmd?.cmd, c)) {
1048
- ctx.skipMemorize();
1049
- return await module.func(ctx, next);
1050
- }
1051
- }
1052
- return next();
1053
- } : module.func);
1054
- };
1055
-
1056
- const parseArgs = async (args, ctx) => {
1057
- const { values, tokens } = _parseArgs({
1058
- args: splitArgs((args || '').replaceAll('—', '--')),
1059
- options: bot._.args, tokens: true
1060
- });
1061
- const result = {};
1062
- for (let x of tokens) {
1063
- result[x.name] = bot._.args[x.name]?.validate
1064
- ? await bot._.args[x.name].validate(values[x.name], ctx)
1065
- : values[x.name];
1066
- }
1067
- return result;
58
+ const send = async (chatId, content, options) => {
59
+ try {
60
+ return (await init()).telegram.sendMessage(chatId, content, options);
61
+ } catch (err) { log(err); }
1068
62
  };
1069
63
 
1070
64
  const init = async (options) => {
@@ -1078,69 +72,8 @@ const init = async (options) => {
1078
72
  const { Telegraf } = await need('telegraf', { raw: true });
1079
73
  // https://github.com/telegraf/telegraf/issues/1736
1080
74
  const { useNewReplies } = await need('telegraf/future', { raw: true });
1081
- const pkg = await which();
1082
- mime = await need('mime');
1083
- lorem = new (await need('lorem-ipsum')).LoremIpsum;
1084
75
  bot = new Telegraf(options?.botToken, { handlerTimeout: 1000 * 60 * 10 }); // 10 minutes
1085
76
  bot.use(useNewReplies());
1086
- bot._ = {
1087
- args: { ...options?.args || {} },
1088
- auth: Function.isFunction(options?.auth) && options.auth,
1089
- chatType: new Set(options?.chatType || ['mention', PRIVATE]), // ignore GROUP, CHANNEL by default
1090
- cmds: [...options?.cmds || []],
1091
- hello: options?.hello || HELLO,
1092
- help: { ...options?.help || {} },
1093
- homeGroup: options?.homeGroup,
1094
- info: options?.info || lines([`[${EMOJI_BOT} ${pkg.title}](${pkg.homepage})`, pkg.description]),
1095
- magicWord: options?.magicWord && new Set(options.magicWord),
1096
- private: options?.private && new Set(options.private),
1097
- session: { get: options?.session?.get, set: options?.session?.set },
1098
- skills: { ...options?.skills || {} },
1099
- speech: options?.speech,
1100
- vision: options?.vision,
1101
- supportedMimeTypes: options?.supportedMimeTypes || [],
1102
- database: options?.database,
1103
- memorize: options?.memorize,
1104
- embedding: options?.embedding,
1105
- };
1106
- (!options?.session?.get || !options?.session?.set)
1107
- && log(`WARNING: Sessions persistence is not enabled.`);
1108
- const mods = [
1109
- ...subconscious.map(s => ({ ...s, run: s.run && !options?.silent })),
1110
- ...ensureArray(options?.skill),
1111
- ];
1112
- if (bot._.database) {
1113
- assert(
1114
- [MYSQL, POSTGRESQL].includes(bot._.database?.provider),
1115
- 'Invalid database provider.'
1116
- );
1117
- assert(
1118
- bot._.database?.client?.query
1119
- && bot._.database?.client?.upsert,
1120
- 'Database client is required.'
1121
- );
1122
- const dbResult = [];
1123
- try {
1124
- for (const act of initSql[bot._.database?.provider]) {
1125
- dbResult.push(await bot._.database.client.query(...act));
1126
- }
1127
- } catch (e) { console.error(e); }
1128
- }
1129
- for (let skillPath of ensureArray(options?.skillPath)) {
1130
- log(`SKILLS: ${skillPath}`);
1131
- const files = (readdirSync(skillPath) || []).filter(
1132
- file => /\.mjs$/i.test(file) && !file.startsWith('.')
1133
- );
1134
- for (let f of files) {
1135
- const m = await import(join(skillPath, f));
1136
- mods.push({ ...m, name: m.name || f.replace(/^(.*)\.mjs$/i, '$1') });
1137
- }
1138
- }
1139
- mods.sort((x, y) => ~~x.priority - ~~y.priority).map(
1140
- module => establish(bot, module, options)
1141
- );
1142
- assert(mods.length, 'Invalid skill set.', 501);
1143
- await parseArgs(); // Validate args options.
1144
77
  bot.catch(console.error);
1145
78
  bot.launch();
1146
79
  on(BOT_SEND, data => send(...data || []));
@@ -1156,34 +89,17 @@ const init = async (options) => {
1156
89
  return bot;
1157
90
  };
1158
91
 
1159
- const send = async (chatId, content, options) => {
1160
- try { return (await init()).telegram.sendMessage(chatId, content, options); }
1161
- catch (err) { log(err); }
1162
- };
1163
92
 
1164
93
  export default init;
1165
94
  export {
1166
95
  _NEED,
1167
- BINARY_STRINGS,
1168
- COMMAND_DESCRIPTION_LENGTH,
1169
- COMMAND_LENGTH,
1170
- COMMAND_LIMIT,
1171
- EMOJI_BOT,
1172
- EMOJI_SPEECH,
1173
96
  EMOJI_THINKING,
1174
- GROUP_LIMIT,
1175
- HELLO,
1176
97
  MESSAGE_LENGTH_LIMIT,
1177
- MESSAGE_SOFT_LIMIT,
1178
- PRIVATE_LIMIT,
1179
98
  end,
1180
99
  init,
1181
100
  lines,
1182
- lines2,
1183
- newCommand,
1184
- oList,
1185
101
  paging,
102
+ parse_mode,
1186
103
  send,
1187
104
  sendMd,
1188
- uList
1189
105
  };