chz-telegram-bot 0.0.1
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/build.ps1 +34 -0
- package/bun.lock +506 -0
- package/dist/entities/actions/commandAction.js +79 -0
- package/dist/entities/actions/scheduledAction.js +65 -0
- package/dist/entities/bot.js +75 -0
- package/dist/entities/cachedStateFactory.js +9 -0
- package/dist/entities/commandTriggerCheckResult.js +22 -0
- package/dist/entities/context/chatContext.js +30 -0
- package/dist/entities/context/messageContext.js +46 -0
- package/dist/entities/incomingMessage.js +20 -0
- package/dist/entities/responses/imageMessage.js +11 -0
- package/dist/entities/responses/reaction.js +11 -0
- package/dist/entities/responses/textMessage.js +11 -0
- package/dist/entities/responses/videoMessage.js +11 -0
- package/dist/entities/states/actionStateBase.js +8 -0
- package/dist/entities/states/potuzhnoState.js +13 -0
- package/dist/entities/taskRecord.js +10 -0
- package/dist/entities/transactionResult.js +9 -0
- package/dist/helpers/builders/commandActionBuilder.js +61 -0
- package/dist/helpers/builders/scheduledActionBuilder.js +42 -0
- package/dist/helpers/escapeMarkdown.js +17 -0
- package/dist/helpers/getWeek.js +12 -0
- package/dist/helpers/noop.js +13 -0
- package/dist/helpers/randomInt.js +8 -0
- package/dist/helpers/reverseMap.js +6 -0
- package/dist/helpers/timeConvertions.js +14 -0
- package/dist/helpers/toArray.js +6 -0
- package/dist/index.js +33 -0
- package/dist/services/logger.js +17 -0
- package/dist/services/storage.js +79 -0
- package/dist/services/taskScheduler.js +37 -0
- package/dist/services/telegramApi.js +94 -0
- package/dist/types/actionState.js +2 -0
- package/dist/types/actionWithState.js +2 -0
- package/dist/types/cachedValueAccessor.js +2 -0
- package/dist/types/commandCondition.js +2 -0
- package/dist/types/daysOfTheWeek.js +13 -0
- package/dist/types/handlers.js +2 -0
- package/dist/types/replyMessage.js +2 -0
- package/dist/types/scheduledItem.js +2 -0
- package/dist/types/timeValues.js +2 -0
- package/entities/actions/commandAction.ts +143 -0
- package/entities/actions/scheduledAction.ts +121 -0
- package/entities/bot.ts +143 -0
- package/entities/cachedStateFactory.ts +14 -0
- package/entities/commandTriggerCheckResult.ts +30 -0
- package/entities/context/chatContext.ts +57 -0
- package/entities/context/messageContext.ts +94 -0
- package/entities/incomingMessage.ts +27 -0
- package/entities/responses/imageMessage.ts +21 -0
- package/entities/responses/reaction.ts +20 -0
- package/entities/responses/textMessage.ts +20 -0
- package/entities/responses/videoMessage.ts +21 -0
- package/entities/states/actionStateBase.ts +5 -0
- package/entities/states/potuzhnoState.ts +5 -0
- package/entities/taskRecord.ts +13 -0
- package/entities/transactionResult.ts +11 -0
- package/eslint.config.js +10 -0
- package/helpers/builders/commandActionBuilder.ts +88 -0
- package/helpers/builders/scheduledActionBuilder.ts +67 -0
- package/helpers/escapeMarkdown.ts +12 -0
- package/helpers/getWeek.ts +8 -0
- package/helpers/noop.ts +13 -0
- package/helpers/randomInt.ts +7 -0
- package/helpers/reverseMap.ts +3 -0
- package/helpers/timeConvertions.ts +13 -0
- package/helpers/toArray.ts +3 -0
- package/index.ts +47 -0
- package/package.json +30 -0
- package/services/logger.ts +30 -0
- package/services/storage.ts +111 -0
- package/services/taskScheduler.ts +66 -0
- package/services/telegramApi.ts +172 -0
- package/tsconfig.json +110 -0
- package/types/actionState.ts +3 -0
- package/types/actionWithState.ts +6 -0
- package/types/cachedValueAccessor.ts +1 -0
- package/types/commandCondition.ts +6 -0
- package/types/daysOfTheWeek.ts +9 -0
- package/types/handlers.ts +14 -0
- package/types/replyMessage.ts +6 -0
- package/types/scheduledItem.ts +6 -0
- package/types/timeValues.ts +29 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.startBot = startBot;
|
|
7
|
+
exports.stopBots = stopBots;
|
|
8
|
+
const promises_1 = require("fs/promises");
|
|
9
|
+
const bot_js_1 = __importDefault(require("./entities/bot.js"));
|
|
10
|
+
const taskScheduler_js_1 = __importDefault(require("./services/taskScheduler.js"));
|
|
11
|
+
const storage_js_1 = __importDefault(require("./services/storage.js"));
|
|
12
|
+
const logger_js_1 = __importDefault(require("./services/logger.js"));
|
|
13
|
+
const bots = [];
|
|
14
|
+
function log(text) {
|
|
15
|
+
logger_js_1.default.logWithTraceId('ALL BOTS', 'System:Bot', 'System', text);
|
|
16
|
+
}
|
|
17
|
+
async function startBot(name, tokenFile, commands, scheduled, chats) {
|
|
18
|
+
const token = await (0, promises_1.readFile)(tokenFile, 'utf8');
|
|
19
|
+
const bot = new bot_js_1.default(name, token, commands, scheduled, chats);
|
|
20
|
+
bots.push(bot);
|
|
21
|
+
return bot;
|
|
22
|
+
}
|
|
23
|
+
async function stopBots(reason) {
|
|
24
|
+
log(`Recieved termination code: ${reason}`);
|
|
25
|
+
taskScheduler_js_1.default.stopAll();
|
|
26
|
+
log('Acquiring storage semaphore...');
|
|
27
|
+
await storage_js_1.default.semaphoreInstance.acquire();
|
|
28
|
+
log('Stopping bots...');
|
|
29
|
+
for (const bot of bots) {
|
|
30
|
+
bot.stop(reason);
|
|
31
|
+
}
|
|
32
|
+
process.exit(0);
|
|
33
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
class Logger {
|
|
4
|
+
logWithTraceId(botName, traceId, chatName, text) {
|
|
5
|
+
console.log(JSON.stringify({ botName, traceId, chatName, text }));
|
|
6
|
+
}
|
|
7
|
+
errorWithTraceId(botName, traceId, chatName, errorObj, extraData) {
|
|
8
|
+
console.error(JSON.stringify({
|
|
9
|
+
botName,
|
|
10
|
+
traceId,
|
|
11
|
+
chatName,
|
|
12
|
+
errorObj,
|
|
13
|
+
extraData
|
|
14
|
+
}));
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
exports.default = new Logger();
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const fs_1 = require("fs");
|
|
4
|
+
const path_1 = require("path");
|
|
5
|
+
const promises_1 = require("fs/promises");
|
|
6
|
+
const async_sema_1 = require("async-sema");
|
|
7
|
+
class Storage {
|
|
8
|
+
get semaphoreInstance() {
|
|
9
|
+
return Storage.semaphore;
|
|
10
|
+
}
|
|
11
|
+
constructor() {
|
|
12
|
+
this.cache = new Map();
|
|
13
|
+
}
|
|
14
|
+
async lock(action) {
|
|
15
|
+
await this.semaphoreInstance.acquire();
|
|
16
|
+
try {
|
|
17
|
+
return await action();
|
|
18
|
+
}
|
|
19
|
+
finally {
|
|
20
|
+
this.semaphoreInstance.release();
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
async loadInternal(key) {
|
|
24
|
+
if (!this.cache.has(key)) {
|
|
25
|
+
const targetPath = this.buidPathFromKey(key);
|
|
26
|
+
if (!(0, fs_1.existsSync)(targetPath)) {
|
|
27
|
+
return {};
|
|
28
|
+
}
|
|
29
|
+
const fileContent = await (0, promises_1.readFile)(targetPath, 'utf8');
|
|
30
|
+
if (fileContent) {
|
|
31
|
+
const data = JSON.parse(fileContent);
|
|
32
|
+
this.cache.set(key, data);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return this.cache.get(key) ?? {};
|
|
36
|
+
}
|
|
37
|
+
async save(data, key) {
|
|
38
|
+
this.cache.delete(key);
|
|
39
|
+
const targetPath = this.buidPathFromKey(key);
|
|
40
|
+
const folderName = (0, path_1.dirname)(targetPath);
|
|
41
|
+
if (!(0, fs_1.existsSync)(folderName)) {
|
|
42
|
+
await (0, promises_1.mkdir)(folderName, { recursive: true });
|
|
43
|
+
}
|
|
44
|
+
await (0, promises_1.writeFile)(targetPath, JSON.stringify(data), { flag: 'w+' });
|
|
45
|
+
}
|
|
46
|
+
buidPathFromKey(key) {
|
|
47
|
+
return 'storage/' + key.replaceAll(':', '/') + '.json';
|
|
48
|
+
}
|
|
49
|
+
async load(key) {
|
|
50
|
+
return await this.lock(async () => {
|
|
51
|
+
return this.loadInternal(key);
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
async saveMetadata(actions, botName) {
|
|
55
|
+
return await this.lock(async () => {
|
|
56
|
+
const targetPath = this.buidPathFromKey(`Metadata-${botName}`);
|
|
57
|
+
await (0, promises_1.writeFile)(targetPath, JSON.stringify(actions), {
|
|
58
|
+
flag: 'w+'
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
async getActionState(entity, chatId) {
|
|
63
|
+
return await this.lock(async () => {
|
|
64
|
+
const data = await this.loadInternal(entity.key);
|
|
65
|
+
return data[chatId] ?? entity.stateConstructor();
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
async commitTransactionForAction(action, chatId, transactionResult) {
|
|
69
|
+
await this.lock(async () => {
|
|
70
|
+
const data = await this.loadInternal(action.key);
|
|
71
|
+
if (transactionResult.shouldUpdate) {
|
|
72
|
+
data[chatId] = transactionResult.data;
|
|
73
|
+
await this.save(data, action.key);
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
Storage.semaphore = new async_sema_1.Sema(1);
|
|
79
|
+
exports.default = new Storage();
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
const taskRecord_1 = __importDefault(require("../entities/taskRecord"));
|
|
7
|
+
const timeConvertions_1 = require("../helpers/timeConvertions");
|
|
8
|
+
const logger_1 = __importDefault(require("./logger"));
|
|
9
|
+
class TaskScheduler {
|
|
10
|
+
constructor() {
|
|
11
|
+
this.activeTasks = [];
|
|
12
|
+
}
|
|
13
|
+
stopAll() {
|
|
14
|
+
this.activeTasks.forEach((task) => {
|
|
15
|
+
clearInterval(task.taskId);
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
createTask(name, action, interval, executeRightAway, ownerName) {
|
|
19
|
+
executeRightAway = executeRightAway ?? false;
|
|
20
|
+
const taskId = setInterval(action, interval);
|
|
21
|
+
const task = new taskRecord_1.default(name, taskId, interval);
|
|
22
|
+
if (executeRightAway) {
|
|
23
|
+
setTimeout(action, (0, timeConvertions_1.secondsToMilliseconds)(1));
|
|
24
|
+
}
|
|
25
|
+
logger_1.default.logWithTraceId(ownerName, `System:TaskScheduler-${ownerName}-${name}`, 'System', `Created task [${taskId}]${name}, that will run every ${interval}ms.`);
|
|
26
|
+
this.activeTasks.push(task);
|
|
27
|
+
}
|
|
28
|
+
createOnetimeTask(name, action, delay, ownerName) {
|
|
29
|
+
const actionWrapper = () => {
|
|
30
|
+
logger_1.default.logWithTraceId(ownerName, `System:TaskScheduler-${ownerName}-${name}`, 'System', `Executing delayed oneshot [${taskId}]${name}`);
|
|
31
|
+
action();
|
|
32
|
+
};
|
|
33
|
+
const taskId = setTimeout(actionWrapper, delay);
|
|
34
|
+
logger_1.default.logWithTraceId(ownerName, `System:TaskScheduler-${ownerName}-${name}`, 'System', `Created oneshot task [${taskId}]${name}, that will run in ${delay}ms.`);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
exports.default = new TaskScheduler();
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
const messageContext_1 = __importDefault(require("../entities/context/messageContext"));
|
|
7
|
+
const chatContext_1 = __importDefault(require("../entities/context/chatContext"));
|
|
8
|
+
const imageMessage_1 = __importDefault(require("../entities/responses/imageMessage"));
|
|
9
|
+
const textMessage_1 = __importDefault(require("../entities/responses/textMessage"));
|
|
10
|
+
const videoMessage_1 = __importDefault(require("../entities/responses/videoMessage"));
|
|
11
|
+
const taskScheduler_1 = __importDefault(require("./taskScheduler"));
|
|
12
|
+
const logger_1 = __importDefault(require("./logger"));
|
|
13
|
+
const reverseMap_1 = require("../helpers/reverseMap");
|
|
14
|
+
class TelegramApiService {
|
|
15
|
+
constructor(botName, telegraf, chats) {
|
|
16
|
+
this.messageQueue = [];
|
|
17
|
+
this.telegraf = telegraf;
|
|
18
|
+
this.botName = botName;
|
|
19
|
+
this.chats = (0, reverseMap_1.reverseMap)(chats);
|
|
20
|
+
taskScheduler_1.default.createTask('MessageSending', () => {
|
|
21
|
+
this.dequeueResponse();
|
|
22
|
+
}, 100, false, this.botName);
|
|
23
|
+
}
|
|
24
|
+
async dequeueResponse() {
|
|
25
|
+
const message = this.messageQueue.pop();
|
|
26
|
+
if (!message)
|
|
27
|
+
return;
|
|
28
|
+
try {
|
|
29
|
+
await this.processResponse(message);
|
|
30
|
+
}
|
|
31
|
+
catch (error) {
|
|
32
|
+
logger_1.default.errorWithTraceId(this.botName, message.traceId, this.chats.get(message.chatId), error, message);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
async processResponse(response) {
|
|
36
|
+
if ('emoji' in response) {
|
|
37
|
+
this.telegraf.telegram.setMessageReaction(response.chatId, response.messageId, [
|
|
38
|
+
{
|
|
39
|
+
type: 'emoji',
|
|
40
|
+
emoji: response.emoji
|
|
41
|
+
}
|
|
42
|
+
], true);
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
switch (response.constructor) {
|
|
46
|
+
case textMessage_1.default:
|
|
47
|
+
await this.telegraf.telegram.sendMessage(response.chatId, response.content, {
|
|
48
|
+
reply_to_message_id: response.replyId,
|
|
49
|
+
parse_mode: 'MarkdownV2',
|
|
50
|
+
disable_web_page_preview: true
|
|
51
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
52
|
+
});
|
|
53
|
+
break;
|
|
54
|
+
case imageMessage_1.default:
|
|
55
|
+
await this.telegraf.telegram.sendPhoto(response.chatId, response.content, response.replyId
|
|
56
|
+
? // eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
57
|
+
{ reply_to_message_id: response.replyId }
|
|
58
|
+
: undefined);
|
|
59
|
+
break;
|
|
60
|
+
case videoMessage_1.default:
|
|
61
|
+
await this.telegraf.telegram.sendVideo(response.chatId, response.content, response.replyId
|
|
62
|
+
? // eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
63
|
+
{ reply_to_message_id: response.replyId }
|
|
64
|
+
: undefined);
|
|
65
|
+
break;
|
|
66
|
+
default:
|
|
67
|
+
logger_1.default.errorWithTraceId(this.botName, response.traceId, this.chats.get(response.chatId), `Unknown message type: ${response.constructor}`, response);
|
|
68
|
+
break;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
enqueueResponse(response) {
|
|
72
|
+
this.messageQueue.push(response);
|
|
73
|
+
}
|
|
74
|
+
enqueueReaction(reaction) {
|
|
75
|
+
this.messageQueue.push(reaction);
|
|
76
|
+
}
|
|
77
|
+
getInteractions() {
|
|
78
|
+
return {
|
|
79
|
+
react: (reaction) => this.enqueueReaction(reaction),
|
|
80
|
+
respond: (response) => this.enqueueResponse(response)
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
createContextForMessage(incomingMessage) {
|
|
84
|
+
const firstName = incomingMessage.from?.first_name ?? 'Unknown user';
|
|
85
|
+
const lastName = incomingMessage.from?.last_name
|
|
86
|
+
? ` ${incomingMessage.from?.last_name}`
|
|
87
|
+
: '';
|
|
88
|
+
return new messageContext_1.default(this.botName, this.getInteractions(), incomingMessage.chat.id, incomingMessage.chatName, incomingMessage.message_id, incomingMessage.text, incomingMessage.from?.id, incomingMessage.traceId, firstName + lastName);
|
|
89
|
+
}
|
|
90
|
+
createContextForChat(chatId, scheduledName) {
|
|
91
|
+
return new chatContext_1.default(this.botName, this.getInteractions(), chatId, this.chats.get(chatId), `Scheduled:${scheduledName}:${chatId}`);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
exports.default = TelegramApiService;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.Day = void 0;
|
|
4
|
+
var Day;
|
|
5
|
+
(function (Day) {
|
|
6
|
+
Day[Day["Sunday"] = 0] = "Sunday";
|
|
7
|
+
Day[Day["Monday"] = 1] = "Monday";
|
|
8
|
+
Day[Day["Tuesday"] = 2] = "Tuesday";
|
|
9
|
+
Day[Day["Wednesday"] = 3] = "Wednesday";
|
|
10
|
+
Day[Day["Thursday"] = 4] = "Thursday";
|
|
11
|
+
Day[Day["Friday"] = 5] = "Friday";
|
|
12
|
+
Day[Day["Saturday"] = 6] = "Saturday";
|
|
13
|
+
})(Day || (exports.Day = Day = {}));
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import storage from '../../services/storage';
|
|
2
|
+
import TransactionResult from '../transactionResult';
|
|
3
|
+
import moment from 'moment';
|
|
4
|
+
import logger from '../../services/logger';
|
|
5
|
+
import MessageContext from '../context/messageContext';
|
|
6
|
+
import IActionWithState from '../../types/actionWithState';
|
|
7
|
+
import toArray from '../../helpers/toArray';
|
|
8
|
+
import IActionState from '../../types/actionState';
|
|
9
|
+
import CommandTriggerCheckResult from '../commandTriggerCheckResult';
|
|
10
|
+
import { CommandHandler } from '../../types/handlers';
|
|
11
|
+
import { CommandCondition } from '../../types/commandCondition';
|
|
12
|
+
import { Seconds } from '../../types/timeValues';
|
|
13
|
+
import { secondsToMilliseconds } from '../../helpers/timeConvertions';
|
|
14
|
+
|
|
15
|
+
export default class CommandAction<TActionState extends IActionState>
|
|
16
|
+
implements IActionWithState
|
|
17
|
+
{
|
|
18
|
+
triggers: (string | RegExp)[];
|
|
19
|
+
handler: CommandHandler<TActionState>;
|
|
20
|
+
name: string;
|
|
21
|
+
cooldownInSeconds: Seconds;
|
|
22
|
+
active: boolean;
|
|
23
|
+
chatsBlacklist: number[];
|
|
24
|
+
allowedUsers: number[];
|
|
25
|
+
condition: CommandCondition<TActionState>;
|
|
26
|
+
stateConstructor: () => TActionState;
|
|
27
|
+
key: string;
|
|
28
|
+
|
|
29
|
+
constructor(
|
|
30
|
+
trigger: string | RegExp | Array<string> | Array<RegExp>,
|
|
31
|
+
handler: CommandHandler<TActionState>,
|
|
32
|
+
name: string,
|
|
33
|
+
active: boolean,
|
|
34
|
+
cooldown: Seconds,
|
|
35
|
+
chatsBlacklist: Array<number>,
|
|
36
|
+
allowedUsers: Array<number>,
|
|
37
|
+
condition: CommandCondition<TActionState>,
|
|
38
|
+
stateConstructor: () => TActionState
|
|
39
|
+
) {
|
|
40
|
+
this.triggers = toArray(trigger);
|
|
41
|
+
this.handler = handler;
|
|
42
|
+
this.name = name;
|
|
43
|
+
this.cooldownInSeconds = cooldown;
|
|
44
|
+
this.active = active;
|
|
45
|
+
this.chatsBlacklist = chatsBlacklist;
|
|
46
|
+
this.allowedUsers = allowedUsers;
|
|
47
|
+
this.condition = condition;
|
|
48
|
+
this.stateConstructor = stateConstructor;
|
|
49
|
+
|
|
50
|
+
this.key = `command:${this.name.replace('.', '-')}`;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
async exec(ctx: MessageContext<TActionState>) {
|
|
54
|
+
if (!this.active || this.chatsBlacklist.includes(ctx.chatId)) return;
|
|
55
|
+
|
|
56
|
+
const isConditionMet = await this.condition(ctx);
|
|
57
|
+
|
|
58
|
+
if (!isConditionMet) return;
|
|
59
|
+
|
|
60
|
+
const state = await storage.getActionState<TActionState>(
|
|
61
|
+
this,
|
|
62
|
+
ctx.chatId
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
const { shouldTrigger, matchResults, skipCooldown } = this.triggers
|
|
66
|
+
.map((x) => this.checkTrigger(ctx, x, state))
|
|
67
|
+
.reduce(
|
|
68
|
+
(acc, curr) => acc.mergeWith(curr),
|
|
69
|
+
CommandTriggerCheckResult.DoNotTrigger
|
|
70
|
+
);
|
|
71
|
+
|
|
72
|
+
if (!shouldTrigger) return;
|
|
73
|
+
|
|
74
|
+
logger.logWithTraceId(
|
|
75
|
+
ctx.botName,
|
|
76
|
+
ctx.traceId,
|
|
77
|
+
ctx.chatName,
|
|
78
|
+
` - Executing [${this.name}] in ${ctx.chatId}`
|
|
79
|
+
);
|
|
80
|
+
ctx.matchResults = matchResults;
|
|
81
|
+
|
|
82
|
+
await this.handler(ctx, state);
|
|
83
|
+
|
|
84
|
+
if (skipCooldown) {
|
|
85
|
+
ctx.startCooldown = false;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (ctx.startCooldown) {
|
|
89
|
+
state.lastExecutedDate = moment().valueOf();
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
ctx.updateActions.forEach((action) => action(state));
|
|
93
|
+
|
|
94
|
+
await storage.commitTransactionForAction(
|
|
95
|
+
this,
|
|
96
|
+
ctx.chatId,
|
|
97
|
+
new TransactionResult(state, ctx.startCooldown && shouldTrigger)
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
private checkTrigger(
|
|
102
|
+
ctx: MessageContext<TActionState>,
|
|
103
|
+
trigger: RegExp | string,
|
|
104
|
+
state: IActionState
|
|
105
|
+
) {
|
|
106
|
+
let shouldTrigger = false;
|
|
107
|
+
const matchResults: RegExpExecArray[] = [];
|
|
108
|
+
|
|
109
|
+
if (!ctx.fromUserId)
|
|
110
|
+
return CommandTriggerCheckResult.DontTriggerAndSkipCooldown;
|
|
111
|
+
|
|
112
|
+
const isUserAllowed =
|
|
113
|
+
this.allowedUsers.length == 0 ||
|
|
114
|
+
this.allowedUsers.includes(ctx.fromUserId);
|
|
115
|
+
const cooldownInMilliseconds = secondsToMilliseconds(
|
|
116
|
+
this.cooldownInSeconds
|
|
117
|
+
);
|
|
118
|
+
const notOnCooldown =
|
|
119
|
+
moment().valueOf() - state.lastExecutedDate >=
|
|
120
|
+
cooldownInMilliseconds;
|
|
121
|
+
|
|
122
|
+
if (isUserAllowed && notOnCooldown) {
|
|
123
|
+
if (typeof trigger == 'string') {
|
|
124
|
+
shouldTrigger = ctx.messageText.toLowerCase() == trigger;
|
|
125
|
+
} else {
|
|
126
|
+
let matchCount = ctx.messageText.match(trigger)?.length ?? 0;
|
|
127
|
+
if (matchCount > 0) {
|
|
128
|
+
for (; matchCount > 0; matchCount--) {
|
|
129
|
+
const execResult = trigger.exec(ctx.messageText);
|
|
130
|
+
if (execResult) matchResults.push(execResult);
|
|
131
|
+
}
|
|
132
|
+
shouldTrigger = true;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return new CommandTriggerCheckResult(
|
|
138
|
+
shouldTrigger,
|
|
139
|
+
matchResults,
|
|
140
|
+
!isUserAllowed
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import storage from '../../services/storage';
|
|
2
|
+
import TransactionResult from '../transactionResult';
|
|
3
|
+
import moment from 'moment';
|
|
4
|
+
import logger from '../../services/logger';
|
|
5
|
+
import taskScheduler from '../../services/taskScheduler';
|
|
6
|
+
import { Sema as Semaphore } from 'async-sema';
|
|
7
|
+
import ChatContext from '../context/chatContext';
|
|
8
|
+
import IActionWithState from '../../types/actionWithState';
|
|
9
|
+
import ActionStateBase from '../states/actionStateBase';
|
|
10
|
+
import IActionState from '../../types/actionState';
|
|
11
|
+
import { ScheduledHandler } from '../../types/handlers';
|
|
12
|
+
import CachedStateFactory from '../cachedStateFactory';
|
|
13
|
+
import { hoursToMilliseconds } from '../../helpers/timeConvertions';
|
|
14
|
+
import { HoursOfDay } from '../../types/timeValues';
|
|
15
|
+
|
|
16
|
+
export default class ScheduledAction implements IActionWithState {
|
|
17
|
+
static semaphore = new Semaphore(1);
|
|
18
|
+
|
|
19
|
+
name: string;
|
|
20
|
+
timeinHours: HoursOfDay;
|
|
21
|
+
active: boolean;
|
|
22
|
+
chatsWhitelist: number[];
|
|
23
|
+
key: string;
|
|
24
|
+
|
|
25
|
+
cachedState = new Map<string, unknown>();
|
|
26
|
+
stateConstructor = () => new ActionStateBase();
|
|
27
|
+
cachedStateFactories: Map<string, CachedStateFactory>;
|
|
28
|
+
handler: ScheduledHandler;
|
|
29
|
+
|
|
30
|
+
constructor(
|
|
31
|
+
name: string,
|
|
32
|
+
handler: ScheduledHandler,
|
|
33
|
+
timeinHours: HoursOfDay,
|
|
34
|
+
active: boolean,
|
|
35
|
+
whitelist: number[],
|
|
36
|
+
cachedStateFactories: Map<string, CachedStateFactory>
|
|
37
|
+
) {
|
|
38
|
+
this.name = name;
|
|
39
|
+
this.handler = handler;
|
|
40
|
+
this.timeinHours = timeinHours;
|
|
41
|
+
this.active = active;
|
|
42
|
+
this.chatsWhitelist = whitelist;
|
|
43
|
+
this.cachedStateFactories = cachedStateFactories;
|
|
44
|
+
this.key = `scheduled:${this.name.replace('.', '-')}`;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async exec(ctx: ChatContext) {
|
|
48
|
+
if (!this.active || !this.chatsWhitelist.includes(ctx.chatId)) return;
|
|
49
|
+
|
|
50
|
+
const state = await storage.getActionState(this, ctx.chatId);
|
|
51
|
+
const isAllowedToTrigger = this.shouldTrigger(state);
|
|
52
|
+
|
|
53
|
+
if (isAllowedToTrigger) {
|
|
54
|
+
logger.logWithTraceId(
|
|
55
|
+
ctx.botName,
|
|
56
|
+
ctx.traceId,
|
|
57
|
+
ctx.chatName,
|
|
58
|
+
` - Executing [${this.name}] in ${ctx.chatId}`
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
await this.handler(ctx, <TResult>(key: string) =>
|
|
62
|
+
this.getCachedValue<TResult>(key, ctx.botName)
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
state.lastExecutedDate = moment().valueOf();
|
|
66
|
+
|
|
67
|
+
await storage.commitTransactionForAction(
|
|
68
|
+
this,
|
|
69
|
+
ctx.chatId,
|
|
70
|
+
new TransactionResult(state, isAllowedToTrigger)
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
private async getCachedValue<TResult>(
|
|
76
|
+
key: string,
|
|
77
|
+
botName: string
|
|
78
|
+
): Promise<TResult> {
|
|
79
|
+
if (!this.cachedStateFactories.has(key)) {
|
|
80
|
+
throw new Error(
|
|
81
|
+
`No shared cache was set up for the key [${key}] in action '${this.name}'`
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
await ScheduledAction.semaphore.acquire();
|
|
86
|
+
|
|
87
|
+
try {
|
|
88
|
+
if (this.cachedState.has(key)) {
|
|
89
|
+
return this.cachedState.get(key) as TResult;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const cachedItemFactory = this.cachedStateFactories.get(key)!;
|
|
93
|
+
const value = await cachedItemFactory.getValue();
|
|
94
|
+
|
|
95
|
+
this.cachedState.set(key, value);
|
|
96
|
+
|
|
97
|
+
taskScheduler.createOnetimeTask(
|
|
98
|
+
`Drop cached value [${this.name} : ${key}]`,
|
|
99
|
+
() => this.cachedState.delete(key),
|
|
100
|
+
hoursToMilliseconds(
|
|
101
|
+
cachedItemFactory.invalidationTimeoutInHours
|
|
102
|
+
),
|
|
103
|
+
botName
|
|
104
|
+
);
|
|
105
|
+
|
|
106
|
+
return value as TResult;
|
|
107
|
+
} finally {
|
|
108
|
+
ScheduledAction.semaphore.release();
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
private shouldTrigger(state: IActionState): boolean {
|
|
113
|
+
const today = moment().startOf('day').valueOf();
|
|
114
|
+
|
|
115
|
+
const isAllowedToTrigger =
|
|
116
|
+
moment().hour().valueOf() >= this.timeinHours;
|
|
117
|
+
const hasTriggeredToday = state.lastExecutedDate >= today;
|
|
118
|
+
|
|
119
|
+
return isAllowedToTrigger && !hasTriggeredToday;
|
|
120
|
+
}
|
|
121
|
+
}
|