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