chz-telegram-bot 0.0.25 → 0.0.27

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 (65) hide show
  1. package/dist/entities/actions/commandAction.d.ts +3 -3
  2. package/dist/entities/actions/commandAction.d.ts.map +1 -1
  3. package/dist/entities/actions/commandAction.js +12 -7
  4. package/dist/entities/actions/scheduledAction.d.ts +2 -2
  5. package/dist/entities/actions/scheduledAction.d.ts.map +1 -1
  6. package/dist/entities/botInstance.d.ts.map +1 -1
  7. package/dist/entities/botInstance.js +3 -2
  8. package/dist/entities/context/chatContext.d.ts +3 -2
  9. package/dist/entities/context/chatContext.d.ts.map +1 -1
  10. package/dist/entities/context/chatContext.js +6 -6
  11. package/dist/entities/context/messageContext.d.ts +3 -1
  12. package/dist/entities/context/messageContext.d.ts.map +1 -1
  13. package/dist/entities/context/messageContext.js +19 -11
  14. package/dist/entities/responses/imageMessage.d.ts +3 -2
  15. package/dist/entities/responses/imageMessage.d.ts.map +1 -1
  16. package/dist/entities/responses/imageMessage.js +2 -2
  17. package/dist/entities/responses/reaction.d.ts +3 -2
  18. package/dist/entities/responses/reaction.d.ts.map +1 -1
  19. package/dist/entities/responses/reaction.js +2 -2
  20. package/dist/entities/responses/textMessage.d.ts +3 -2
  21. package/dist/entities/responses/textMessage.d.ts.map +1 -1
  22. package/dist/entities/responses/textMessage.js +2 -2
  23. package/dist/entities/responses/unpin.d.ts +3 -2
  24. package/dist/entities/responses/unpin.d.ts.map +1 -1
  25. package/dist/entities/responses/unpin.js +2 -2
  26. package/dist/entities/responses/videoMessage.d.ts +3 -2
  27. package/dist/entities/responses/videoMessage.d.ts.map +1 -1
  28. package/dist/entities/responses/videoMessage.js +2 -2
  29. package/dist/helpers/builders/commandActionBuilder.d.ts +3 -3
  30. package/dist/helpers/builders/commandActionBuilder.d.ts.map +1 -1
  31. package/dist/services/jsonFileStorage.d.ts +6 -7
  32. package/dist/services/jsonFileStorage.d.ts.map +1 -1
  33. package/dist/services/jsonFileStorage.js +29 -23
  34. package/dist/services/logger.d.ts.map +1 -1
  35. package/dist/services/logger.js +2 -8
  36. package/dist/services/taskScheduler.d.ts.map +1 -1
  37. package/dist/services/taskScheduler.js +0 -1
  38. package/dist/services/telegramApi.d.ts +1 -1
  39. package/dist/services/telegramApi.d.ts.map +1 -1
  40. package/dist/services/telegramApi.js +6 -9
  41. package/dist/types/actionWithState.d.ts +4 -1
  42. package/dist/types/actionWithState.d.ts.map +1 -1
  43. package/dist/types/response.d.ts +2 -1
  44. package/dist/types/response.d.ts.map +1 -1
  45. package/dist/types/storage.d.ts +4 -4
  46. package/dist/types/storage.d.ts.map +1 -1
  47. package/entities/actions/commandAction.ts +20 -12
  48. package/entities/actions/scheduledAction.ts +3 -3
  49. package/entities/botInstance.ts +4 -5
  50. package/entities/context/chatContext.ts +9 -15
  51. package/entities/context/messageContext.ts +30 -27
  52. package/entities/responses/imageMessage.ts +4 -3
  53. package/entities/responses/reaction.ts +4 -3
  54. package/entities/responses/textMessage.ts +4 -3
  55. package/entities/responses/unpin.ts +4 -3
  56. package/entities/responses/videoMessage.ts +4 -3
  57. package/helpers/builders/commandActionBuilder.ts +3 -3
  58. package/package.json +1 -1
  59. package/services/jsonFileStorage.ts +41 -27
  60. package/services/logger.ts +6 -8
  61. package/services/taskScheduler.ts +0 -1
  62. package/services/telegramApi.ts +9 -18
  63. package/types/actionWithState.ts +3 -1
  64. package/types/response.ts +3 -1
  65. package/types/storage.ts +4 -4
@@ -1,5 +1,6 @@
1
1
  import { TelegramEmoji } from 'telegraf/types';
2
2
  import { BotResponseTypes, IChatResponse } from '../../types/response';
3
+ import { IActionWithState } from '../../types/actionWithState';
3
4
 
4
5
  export class Reaction implements IChatResponse {
5
6
  kind = BotResponseTypes.react;
@@ -8,19 +9,19 @@ export class Reaction implements IChatResponse {
8
9
  messageId: number;
9
10
  traceId: number | string;
10
11
  emoji: TelegramEmoji;
11
- sourceActionKey: string;
12
+ action: IActionWithState;
12
13
 
13
14
  constructor(
14
15
  traceId: number | string,
15
16
  chatId: number,
16
17
  messageId: number,
17
18
  emoji: TelegramEmoji,
18
- sourceActionKey: string
19
+ action: IActionWithState
19
20
  ) {
20
21
  this.chatId = chatId;
21
22
  this.messageId = messageId;
22
23
  this.emoji = emoji;
23
24
  this.traceId = traceId;
24
- this.sourceActionKey = sourceActionKey;
25
+ this.action = action;
25
26
  }
26
27
  }
@@ -1,5 +1,6 @@
1
1
  import { TextMessageSendingOptions } from '../../types/messageSendingOptions';
2
2
  import { BotResponseTypes, IReplyMessage } from '../../types/response';
3
+ import { IActionWithState } from '../../types/actionWithState';
3
4
 
4
5
  export class TextMessage implements IReplyMessage<string> {
5
6
  kind = BotResponseTypes.text;
@@ -10,14 +11,14 @@ export class TextMessage implements IReplyMessage<string> {
10
11
  traceId: string | number;
11
12
  disableWebPreview: boolean;
12
13
  shouldPin: boolean;
13
- sourceActionKey: string;
14
+ action: IActionWithState;
14
15
 
15
16
  constructor(
16
17
  text: string,
17
18
  chatId: number,
18
19
  replyId: number | undefined,
19
20
  traceId: string | number,
20
- sourceActionKey: string,
21
+ action: IActionWithState,
21
22
  options?: TextMessageSendingOptions
22
23
  ) {
23
24
  this.content = text;
@@ -26,6 +27,6 @@ export class TextMessage implements IReplyMessage<string> {
26
27
  this.traceId = traceId;
27
28
  this.disableWebPreview = options?.disableWebPreview ?? false;
28
29
  this.shouldPin = options?.pin ?? false;
29
- this.sourceActionKey = sourceActionKey;
30
+ this.action = action;
30
31
  }
31
32
  }
@@ -1,4 +1,5 @@
1
1
  import { BotResponseTypes, IChatResponse } from '../../types/response';
2
+ import { IActionWithState } from '../../types/actionWithState';
2
3
 
3
4
  export class UnpinResponse implements IChatResponse {
4
5
  kind = BotResponseTypes.unpin;
@@ -6,17 +7,17 @@ export class UnpinResponse implements IChatResponse {
6
7
  messageId: number;
7
8
  chatId: number;
8
9
  traceId: number | string;
9
- sourceActionKey: string;
10
+ action: IActionWithState;
10
11
 
11
12
  constructor(
12
13
  messageId: number,
13
14
  chatId: number,
14
15
  traceId: number | string,
15
- sourceActionKey: string
16
+ action: IActionWithState
16
17
  ) {
17
18
  this.messageId = messageId;
18
19
  this.chatId = chatId;
19
20
  this.traceId = traceId;
20
- this.sourceActionKey = sourceActionKey;
21
+ this.action = action;
21
22
  }
22
23
  }
@@ -1,6 +1,7 @@
1
1
  import { InputFile } from 'telegraf/types';
2
2
  import { BotResponseTypes, IReplyMessage } from '../../types/response';
3
3
  import { MessageSendingOptions } from '../../types/messageSendingOptions';
4
+ import { IActionWithState } from '../../types/actionWithState';
4
5
 
5
6
  export class VideoMessage implements IReplyMessage<InputFile> {
6
7
  kind = BotResponseTypes.video;
@@ -11,14 +12,14 @@ export class VideoMessage implements IReplyMessage<InputFile> {
11
12
  traceId: string | number;
12
13
  disableWebPreview = false;
13
14
  shouldPin: boolean;
14
- sourceActionKey: string;
15
+ action: IActionWithState;
15
16
 
16
17
  constructor(
17
18
  video: InputFile,
18
19
  chatId: number,
19
20
  replyId: number | undefined,
20
21
  traceId: number | string,
21
- sourceActionKey: string,
22
+ action: IActionWithState,
22
23
  options?: MessageSendingOptions
23
24
  ) {
24
25
  this.content = video;
@@ -26,6 +27,6 @@ export class VideoMessage implements IReplyMessage<InputFile> {
26
27
  this.replyId = replyId;
27
28
  this.traceId = traceId;
28
29
  this.shouldPin = options?.pin ?? false;
29
- this.sourceActionKey = sourceActionKey;
30
+ this.action = action;
30
31
  }
31
32
  }
@@ -12,7 +12,7 @@ import { Noop } from '../noop';
12
12
  */
13
13
  export class CommandActionBuilderWithState<TActionState extends IActionState> {
14
14
  name: string;
15
- trigger: string | RegExp | Array<string> | Array<RegExp> = [];
15
+ trigger: string | RegExp | string[] | RegExp[] = [];
16
16
 
17
17
  active = true;
18
18
  cooldownSeconds: Seconds = 0 as Seconds;
@@ -38,7 +38,7 @@ export class CommandActionBuilderWithState<TActionState extends IActionState> {
38
38
  *
39
39
  * If `RegExp` or `RegExp[]` is provided, will be triggered on successful match.
40
40
  */
41
- on(trigger: string | RegExp | Array<string> | Array<RegExp>) {
41
+ on(trigger: string | RegExp | string[] | RegExp[]) {
42
42
  this.trigger = trigger;
43
43
 
44
44
  return this;
@@ -47,7 +47,7 @@ export class CommandActionBuilderWithState<TActionState extends IActionState> {
47
47
  /** Defines id (or ids) of users that are allowed to trigger this action.
48
48
  * @param id User id or ids
49
49
  */
50
- from(id: number | Array<number>) {
50
+ from(id: number | number[]) {
51
51
  this.allowedUsers = toArray(id);
52
52
 
53
53
  return this;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "chz-telegram-bot",
3
- "version": "0.0.25",
3
+ "version": "0.0.27",
4
4
  "type": "module",
5
5
  "dependencies": {
6
6
  "async-sema": "^3.1.1",
@@ -6,15 +6,15 @@ import { IStorageClient } from '../types/storage';
6
6
  import { ActionStateBase } from '../entities/states/actionStateBase';
7
7
  import { ActionExecutionResult } from '../entities/actionExecutionResult';
8
8
  import { IActionState } from '../types/actionState';
9
- import { IActionWithState } from '../types/actionWithState';
9
+ import { IActionWithState, ActionKey } from '../types/actionWithState';
10
10
 
11
11
  export class JsonFileStorage implements IStorageClient {
12
- semaphore = new Semaphore(1);
12
+ private locks = new Map<ActionKey, Semaphore>();
13
13
  private cache: Map<string, Record<number, IActionState>>;
14
14
  private storagePath: string;
15
15
  private botName: string;
16
16
 
17
- constructor(botName: string, path?: string) {
17
+ constructor(botName: string, actions: IActionWithState[], path?: string) {
18
18
  this.cache = new Map<string, Record<number, IActionState>>();
19
19
  this.botName = botName;
20
20
  this.storagePath = path ?? 'storage';
@@ -24,19 +24,31 @@ export class JsonFileStorage implements IStorageClient {
24
24
  recursive: true
25
25
  });
26
26
  }
27
+
28
+ for (const action of actions) {
29
+ this.locks.set(action.key, new Semaphore(1));
30
+ }
27
31
  }
28
32
 
29
- private async lock<TType>(action: () => Promise<TType>) {
30
- await this.semaphore.acquire();
33
+ private async lock<TType>(key: ActionKey, action: () => Promise<TType>) {
34
+ const lock = this.locks.get(key);
35
+
36
+ if (!lock) {
37
+ throw new Error(`Lock for action ${key} not found`);
38
+ }
39
+
40
+ await lock.acquire();
31
41
 
32
42
  try {
33
43
  return await action();
34
44
  } finally {
35
- this.semaphore.release();
45
+ lock.release();
36
46
  }
37
47
  }
38
48
 
39
- private async loadInternal<TActionState extends IActionState>(key: string) {
49
+ private async loadInternal<TActionState extends IActionState>(
50
+ key: ActionKey
51
+ ) {
40
52
  if (!this.cache.has(key)) {
41
53
  const targetPath = this.buidPathFromKey(key);
42
54
  if (!existsSync(targetPath)) {
@@ -55,7 +67,7 @@ export class JsonFileStorage implements IStorageClient {
55
67
  return (this.cache.get(key) ?? {}) as Record<number, TActionState>;
56
68
  }
57
69
 
58
- private async save(data: Record<number, ActionStateBase>, key: string) {
70
+ private async save(data: Record<number, ActionStateBase>, key: ActionKey) {
59
71
  this.cache.delete(key);
60
72
 
61
73
  const targetPath = this.buidPathFromKey(key);
@@ -68,38 +80,38 @@ export class JsonFileStorage implements IStorageClient {
68
80
  await writeFile(targetPath, JSON.stringify(data), { flag: 'w+' });
69
81
  }
70
82
 
71
- private buidPathFromKey(key: string) {
83
+ private buidPathFromKey(key: ActionKey) {
72
84
  return `${this.storagePath}/${this.botName}/${key.replaceAll(
73
85
  ':',
74
86
  '/'
75
87
  )}.json`;
76
88
  }
77
89
 
78
- async load<TActionState extends IActionState>(key: string) {
79
- return await this.lock(async () => {
90
+ async load<TActionState extends IActionState>(key: ActionKey) {
91
+ return await this.lock(key, async () => {
80
92
  return this.loadInternal<TActionState>(key);
81
93
  });
82
94
  }
83
95
 
84
96
  async saveMetadata(actions: IActionWithState[], botName: string) {
85
- return await this.lock(async () => {
86
- const targetPath = this.buidPathFromKey(`Metadata-${botName}`);
97
+ const targetPath = this.buidPathFromKey(
98
+ `Metadata-${botName}` as ActionKey
99
+ );
87
100
 
88
- await writeFile(targetPath, JSON.stringify(actions), {
89
- flag: 'w+'
90
- });
101
+ await writeFile(targetPath, JSON.stringify(actions), {
102
+ flag: 'w+'
91
103
  });
92
104
  }
93
105
 
94
106
  async getActionState<TActionState extends IActionState>(
95
- entity: IActionWithState,
107
+ action: IActionWithState,
96
108
  chatId: number
97
109
  ) {
98
- return await this.lock(async () => {
99
- const data = await this.loadInternal(entity.key);
110
+ return await this.lock(action.key, async () => {
111
+ const data = await this.loadInternal(action.key);
100
112
 
101
113
  return Object.assign(
102
- entity.stateConstructor(),
114
+ action.stateConstructor(),
103
115
  data[chatId]
104
116
  ) as TActionState;
105
117
  });
@@ -110,7 +122,7 @@ export class JsonFileStorage implements IStorageClient {
110
122
  chatId: number,
111
123
  transactionResult: ActionExecutionResult
112
124
  ) {
113
- await this.lock(async () => {
125
+ await this.lock(action.key, async () => {
114
126
  const data = await this.loadInternal(action.key);
115
127
 
116
128
  if (transactionResult.shouldUpdate) {
@@ -121,24 +133,26 @@ export class JsonFileStorage implements IStorageClient {
121
133
  }
122
134
 
123
135
  async close(): Promise<void> {
124
- await this.semaphore.acquire();
136
+ for (const lock of this.locks.values()) {
137
+ await lock.acquire();
138
+ }
125
139
  }
126
140
 
127
141
  async updateStateFor<TActionState extends IActionState>(
128
- sourceActionKey: string,
142
+ action: IActionWithState,
129
143
  chatId: number,
130
144
  update: (state: TActionState) => Promise<void>
131
145
  ) {
132
- await this.lock(async () => {
133
- const data = await this.loadInternal(sourceActionKey);
146
+ await this.lock(action.key, async () => {
147
+ const data = await this.loadInternal(action.key);
134
148
  const state = Object.assign(
135
- new ActionStateBase(),
149
+ action.stateConstructor(),
136
150
  data[chatId]
137
151
  ) as TActionState;
138
152
 
139
153
  await update(state);
140
154
 
141
- await this.save(data, sourceActionKey);
155
+ await this.save(data, action.key);
142
156
  });
143
157
  }
144
158
  }
@@ -14,7 +14,9 @@ class JsonLogger {
14
14
  chatName: string,
15
15
  text: string
16
16
  ) {
17
- console.log(JSON.stringify({ botName, traceId, chatName, text }));
17
+ console.log(
18
+ `{"botName":"${botName}","traceId":"${traceId}","chatName":"${chatName}","text":"${text}"}`
19
+ );
18
20
  }
19
21
 
20
22
  errorWithTraceId<TData>(
@@ -25,13 +27,9 @@ class JsonLogger {
25
27
  extraData?: TData | undefined
26
28
  ) {
27
29
  console.error(
28
- JSON.stringify({
29
- botName,
30
- traceId,
31
- chatName,
32
- error: this.serializeError(errorObj),
33
- extraData
34
- })
30
+ `{"botName":"${botName}","traceId":"${traceId}","chatName":"${chatName}","error":${this.serializeError(
31
+ errorObj
32
+ )}${extraData ? `,"extraData":${JSON.stringify(extraData)}` : ''}}`
35
33
  );
36
34
  }
37
35
  }
@@ -18,7 +18,6 @@ class TaskScheduler {
18
18
  executeRightAway: boolean,
19
19
  ownerName: string
20
20
  ) {
21
- executeRightAway = executeRightAway ?? false;
22
21
  const taskId = setInterval(action, interval);
23
22
  const task = new TaskRecord(name, taskId, interval);
24
23
 
@@ -18,9 +18,11 @@ import { ScheduledAction } from '../entities/actions/scheduledAction';
18
18
  import { IActionState } from '../types/actionState';
19
19
  import { CommandAction } from '../entities/actions/commandAction';
20
20
 
21
+ const TELEGRAM_RATELIMIT_DELAY = 35 as Milliseconds;
22
+
21
23
  export class TelegramApiService {
22
24
  isFlushing = false;
23
- messageQueue: Array<BotResponse> = [];
25
+ messageQueue: BotResponse[] = [];
24
26
 
25
27
  botName: string;
26
28
  telegram: Telegram;
@@ -51,7 +53,7 @@ export class TelegramApiService {
51
53
 
52
54
  try {
53
55
  await this.processResponse(message);
54
- await setTimeout(100 as Milliseconds);
56
+ await setTimeout(TELEGRAM_RATELIMIT_DELAY);
55
57
  } catch (error) {
56
58
  Logger.errorWithTraceId(
57
59
  this.botName,
@@ -78,7 +80,7 @@ export class TelegramApiService {
78
80
  );
79
81
 
80
82
  await this.storage.updateStateFor(
81
- response.sourceActionKey,
83
+ response.action,
82
84
  response.chatId,
83
85
  async (state) => {
84
86
  state.pinnedMessages.push(sentMessage.message_id);
@@ -150,7 +152,7 @@ export class TelegramApiService {
150
152
  );
151
153
 
152
154
  await this.storage.updateStateFor(
153
- response.sourceActionKey,
155
+ response.action,
154
156
  response.chatId,
155
157
  async (state) => {
156
158
  state.pinnedMessages = state.pinnedMessages.filter(
@@ -178,22 +180,11 @@ export class TelegramApiService {
178
180
  incomingMessage: IncomingMessage,
179
181
  command: CommandAction<TActionState>
180
182
  ) {
181
- const firstName = incomingMessage.from?.first_name ?? 'Unknown user';
182
- const lastName = incomingMessage.from?.last_name
183
- ? ` ${incomingMessage.from?.last_name}`
184
- : '';
185
-
186
183
  return new MessageContext<TActionState>(
187
184
  this.botName,
188
- command.key,
185
+ command,
189
186
  this.getInteractions(),
190
- incomingMessage.chat.id,
191
- incomingMessage.chatName,
192
- incomingMessage.message_id,
193
- incomingMessage.text,
194
- incomingMessage.from?.id,
195
- incomingMessage.traceId,
196
- firstName + lastName,
187
+ incomingMessage,
197
188
  this.storage
198
189
  );
199
190
  }
@@ -204,7 +195,7 @@ export class TelegramApiService {
204
195
  ) {
205
196
  return new ChatContext<TActionState>(
206
197
  this.botName,
207
- scheduledAction.key,
198
+ scheduledAction,
208
199
  this.getInteractions(),
209
200
  chatId,
210
201
  this.chats[chatId],
@@ -1,6 +1,8 @@
1
1
  import { IActionState } from './actionState';
2
2
 
3
+ export type ActionKey = string & { __brand: 'actionKey' };
4
+
3
5
  export interface IActionWithState {
4
- key: string;
6
+ key: ActionKey;
5
7
  stateConstructor: () => IActionState;
6
8
  }
package/types/response.ts CHANGED
@@ -3,6 +3,7 @@ import { Reaction } from '../entities/responses/reaction';
3
3
  import { TextMessage } from '../entities/responses/textMessage';
4
4
  import { UnpinResponse } from '../entities/responses/unpin';
5
5
  import { VideoMessage } from '../entities/responses/videoMessage';
6
+ import { IActionWithState } from './actionWithState';
6
7
 
7
8
  export const BotResponseTypes = {
8
9
  unpin: 'unpin',
@@ -23,7 +24,8 @@ export interface IChatResponse {
23
24
  kind: keyof typeof BotResponseTypes;
24
25
  chatId: number;
25
26
  traceId: number | string;
26
- sourceActionKey: string;
27
+
28
+ action: IActionWithState;
27
29
  }
28
30
 
29
31
  export interface IReplyMessage<TType> extends IChatResponse {
package/types/storage.ts CHANGED
@@ -1,20 +1,20 @@
1
1
  import { ActionExecutionResult } from '../entities/actionExecutionResult';
2
2
  import { IActionState } from './actionState';
3
- import { IActionWithState } from './actionWithState';
3
+ import { ActionKey, IActionWithState } from './actionWithState';
4
4
 
5
5
  export interface IStorageClient {
6
6
  updateStateFor<TActionState extends IActionState>(
7
- sourceActionKey: string,
7
+ action: IActionWithState,
8
8
  chatId: number,
9
9
  update: (state: TActionState) => Promise<void>
10
10
  ): Promise<void>;
11
11
  close(): Promise<void>;
12
12
  load<TActionState extends IActionState>(
13
- key: string
13
+ key: ActionKey
14
14
  ): Promise<Record<number, TActionState>>;
15
15
  saveMetadata(actions: IActionWithState[], botName: string): Promise<void>;
16
16
  getActionState<TActionState extends IActionState>(
17
- entity: IActionWithState,
17
+ action: IActionWithState,
18
18
  chatId: number
19
19
  ): Promise<TActionState>;
20
20
  saveActionExecutionResult(