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.
Files changed (83) hide show
  1. package/build.ps1 +34 -0
  2. package/bun.lock +506 -0
  3. package/dist/entities/actions/commandAction.js +79 -0
  4. package/dist/entities/actions/scheduledAction.js +65 -0
  5. package/dist/entities/bot.js +75 -0
  6. package/dist/entities/cachedStateFactory.js +9 -0
  7. package/dist/entities/commandTriggerCheckResult.js +22 -0
  8. package/dist/entities/context/chatContext.js +30 -0
  9. package/dist/entities/context/messageContext.js +46 -0
  10. package/dist/entities/incomingMessage.js +20 -0
  11. package/dist/entities/responses/imageMessage.js +11 -0
  12. package/dist/entities/responses/reaction.js +11 -0
  13. package/dist/entities/responses/textMessage.js +11 -0
  14. package/dist/entities/responses/videoMessage.js +11 -0
  15. package/dist/entities/states/actionStateBase.js +8 -0
  16. package/dist/entities/states/potuzhnoState.js +13 -0
  17. package/dist/entities/taskRecord.js +10 -0
  18. package/dist/entities/transactionResult.js +9 -0
  19. package/dist/helpers/builders/commandActionBuilder.js +61 -0
  20. package/dist/helpers/builders/scheduledActionBuilder.js +42 -0
  21. package/dist/helpers/escapeMarkdown.js +17 -0
  22. package/dist/helpers/getWeek.js +12 -0
  23. package/dist/helpers/noop.js +13 -0
  24. package/dist/helpers/randomInt.js +8 -0
  25. package/dist/helpers/reverseMap.js +6 -0
  26. package/dist/helpers/timeConvertions.js +14 -0
  27. package/dist/helpers/toArray.js +6 -0
  28. package/dist/index.js +33 -0
  29. package/dist/services/logger.js +17 -0
  30. package/dist/services/storage.js +79 -0
  31. package/dist/services/taskScheduler.js +37 -0
  32. package/dist/services/telegramApi.js +94 -0
  33. package/dist/types/actionState.js +2 -0
  34. package/dist/types/actionWithState.js +2 -0
  35. package/dist/types/cachedValueAccessor.js +2 -0
  36. package/dist/types/commandCondition.js +2 -0
  37. package/dist/types/daysOfTheWeek.js +13 -0
  38. package/dist/types/handlers.js +2 -0
  39. package/dist/types/replyMessage.js +2 -0
  40. package/dist/types/scheduledItem.js +2 -0
  41. package/dist/types/timeValues.js +2 -0
  42. package/entities/actions/commandAction.ts +143 -0
  43. package/entities/actions/scheduledAction.ts +121 -0
  44. package/entities/bot.ts +143 -0
  45. package/entities/cachedStateFactory.ts +14 -0
  46. package/entities/commandTriggerCheckResult.ts +30 -0
  47. package/entities/context/chatContext.ts +57 -0
  48. package/entities/context/messageContext.ts +94 -0
  49. package/entities/incomingMessage.ts +27 -0
  50. package/entities/responses/imageMessage.ts +21 -0
  51. package/entities/responses/reaction.ts +20 -0
  52. package/entities/responses/textMessage.ts +20 -0
  53. package/entities/responses/videoMessage.ts +21 -0
  54. package/entities/states/actionStateBase.ts +5 -0
  55. package/entities/states/potuzhnoState.ts +5 -0
  56. package/entities/taskRecord.ts +13 -0
  57. package/entities/transactionResult.ts +11 -0
  58. package/eslint.config.js +10 -0
  59. package/helpers/builders/commandActionBuilder.ts +88 -0
  60. package/helpers/builders/scheduledActionBuilder.ts +67 -0
  61. package/helpers/escapeMarkdown.ts +12 -0
  62. package/helpers/getWeek.ts +8 -0
  63. package/helpers/noop.ts +13 -0
  64. package/helpers/randomInt.ts +7 -0
  65. package/helpers/reverseMap.ts +3 -0
  66. package/helpers/timeConvertions.ts +13 -0
  67. package/helpers/toArray.ts +3 -0
  68. package/index.ts +47 -0
  69. package/package.json +30 -0
  70. package/services/logger.ts +30 -0
  71. package/services/storage.ts +111 -0
  72. package/services/taskScheduler.ts +66 -0
  73. package/services/telegramApi.ts +172 -0
  74. package/tsconfig.json +110 -0
  75. package/types/actionState.ts +3 -0
  76. package/types/actionWithState.ts +6 -0
  77. package/types/cachedValueAccessor.ts +1 -0
  78. package/types/commandCondition.ts +6 -0
  79. package/types/daysOfTheWeek.ts +9 -0
  80. package/types/handlers.ts +14 -0
  81. package/types/replyMessage.ts +6 -0
  82. package/types/scheduledItem.ts +6 -0
  83. 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,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -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,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -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
+ }