chz-telegram-bot 0.0.26 → 0.0.28

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 (60) hide show
  1. package/dist/entities/actions/commandAction.d.ts +2 -2
  2. package/dist/entities/actions/commandAction.d.ts.map +1 -1
  3. package/dist/entities/actions/scheduledAction.d.ts +2 -2
  4. package/dist/entities/actions/scheduledAction.d.ts.map +1 -1
  5. package/dist/entities/actions/scheduledAction.js +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 +2 -1
  12. package/dist/entities/context/messageContext.d.ts.map +1 -1
  13. package/dist/entities/context/messageContext.js +6 -6
  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/inverseRecord.d.ts +2 -0
  30. package/dist/helpers/inverseRecord.d.ts.map +1 -0
  31. package/dist/helpers/inverseRecord.js +6 -0
  32. package/dist/services/jsonFileStorage.d.ts +6 -7
  33. package/dist/services/jsonFileStorage.d.ts.map +1 -1
  34. package/dist/services/jsonFileStorage.js +29 -23
  35. package/dist/services/telegramApi.d.ts +1 -1
  36. package/dist/services/telegramApi.d.ts.map +1 -1
  37. package/dist/services/telegramApi.js +11 -13
  38. package/dist/types/actionWithState.d.ts +4 -1
  39. package/dist/types/actionWithState.d.ts.map +1 -1
  40. package/dist/types/response.d.ts +2 -1
  41. package/dist/types/response.d.ts.map +1 -1
  42. package/dist/types/storage.d.ts +4 -4
  43. package/dist/types/storage.d.ts.map +1 -1
  44. package/entities/actions/commandAction.ts +3 -3
  45. package/entities/actions/scheduledAction.ts +4 -4
  46. package/entities/botInstance.ts +4 -5
  47. package/entities/context/chatContext.ts +8 -12
  48. package/entities/context/messageContext.ts +11 -7
  49. package/entities/responses/imageMessage.ts +4 -3
  50. package/entities/responses/reaction.ts +4 -3
  51. package/entities/responses/textMessage.ts +4 -3
  52. package/entities/responses/unpin.ts +4 -3
  53. package/entities/responses/videoMessage.ts +4 -3
  54. package/helpers/{reverseRecord.ts → inverseRecord.ts} +1 -1
  55. package/package.json +1 -1
  56. package/services/jsonFileStorage.ts +41 -27
  57. package/services/telegramApi.ts +19 -20
  58. package/types/actionWithState.ts +3 -1
  59. package/types/response.ts +3 -1
  60. package/types/storage.ts +4 -4
@@ -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
  }
@@ -1,7 +1,7 @@
1
- import { InputFile, Message } from 'telegraf/types';
1
+ import { Message } from 'telegraf/types';
2
2
  import { ChatContext } from '../entities/context/chatContext';
3
3
  import { MessageContext } from '../entities/context/messageContext';
4
- import { reverseRecord } from '../helpers/reverseRecord';
4
+ import { inverseRecord as inverseRecord } from '../helpers/inverseRecord';
5
5
  import { IStorageClient } from '../types/storage';
6
6
  import { Logger } from './logger';
7
7
  import { Reaction } from '../entities/responses/reaction';
@@ -28,6 +28,7 @@ export class TelegramApiService {
28
28
  telegram: Telegram;
29
29
  chats: Record<number, string>;
30
30
  storage: IStorageClient;
31
+ interactions: IBotApiInteractions;
31
32
 
32
33
  constructor(
33
34
  botName: string,
@@ -37,8 +38,14 @@ export class TelegramApiService {
37
38
  ) {
38
39
  this.telegram = telegram;
39
40
  this.botName = botName;
40
- this.chats = reverseRecord(chats);
41
+ this.chats = inverseRecord(chats);
41
42
  this.storage = storage;
43
+
44
+ this.interactions = {
45
+ react: (reaction) => this.enqueue(reaction),
46
+ respond: (response) => this.enqueue(response),
47
+ unpin: (unpinMessage) => this.enqueue(unpinMessage)
48
+ } as IBotApiInteractions;
42
49
  }
43
50
 
44
51
  async flushResponses() {
@@ -80,7 +87,7 @@ export class TelegramApiService {
80
87
  );
81
88
 
82
89
  await this.storage.updateStateFor(
83
- response.sourceActionKey,
90
+ response.action,
84
91
  response.chatId,
85
92
  async (state) => {
86
93
  state.pinnedMessages.push(sentMessage.message_id);
@@ -96,7 +103,7 @@ export class TelegramApiService {
96
103
  case 'text':
97
104
  sentMessage = await this.telegram.sendMessage(
98
105
  response.chatId,
99
- response.content as string,
106
+ response.content,
100
107
  {
101
108
  reply_to_message_id: response.replyId,
102
109
  parse_mode: 'MarkdownV2',
@@ -110,7 +117,7 @@ export class TelegramApiService {
110
117
  case 'image':
111
118
  sentMessage = await this.telegram.sendPhoto(
112
119
  response.chatId,
113
- response.content as InputFile,
120
+ response.content,
114
121
  response.replyId
115
122
  ? // eslint-disable-next-line @typescript-eslint/no-explicit-any
116
123
  ({ reply_to_message_id: response.replyId } as any)
@@ -122,7 +129,7 @@ export class TelegramApiService {
122
129
  case 'video':
123
130
  sentMessage = await this.telegram.sendVideo(
124
131
  response.chatId,
125
- response.content as InputFile,
132
+ response.content,
126
133
  response.replyId
127
134
  ? // eslint-disable-next-line @typescript-eslint/no-explicit-any
128
135
  ({ reply_to_message_id: response.replyId } as any)
@@ -152,7 +159,7 @@ export class TelegramApiService {
152
159
  );
153
160
 
154
161
  await this.storage.updateStateFor(
155
- response.sourceActionKey,
162
+ response.action,
156
163
  response.chatId,
157
164
  async (state) => {
158
165
  state.pinnedMessages = state.pinnedMessages.filter(
@@ -168,22 +175,14 @@ export class TelegramApiService {
168
175
  this.messageQueue.push(response);
169
176
  }
170
177
 
171
- private getInteractions() {
172
- return {
173
- react: (reaction) => this.enqueue(reaction),
174
- respond: (response) => this.enqueue(response),
175
- unpin: (unpinMessage) => this.enqueue(unpinMessage)
176
- } as IBotApiInteractions;
177
- }
178
-
179
178
  createContextForMessage<TActionState extends IActionState>(
180
179
  incomingMessage: IncomingMessage,
181
180
  command: CommandAction<TActionState>
182
181
  ) {
183
182
  return new MessageContext<TActionState>(
184
183
  this.botName,
185
- command.key,
186
- this.getInteractions(),
184
+ command,
185
+ this.interactions,
187
186
  incomingMessage,
188
187
  this.storage
189
188
  );
@@ -195,8 +194,8 @@ export class TelegramApiService {
195
194
  ) {
196
195
  return new ChatContext<TActionState>(
197
196
  this.botName,
198
- scheduledAction.key,
199
- this.getInteractions(),
197
+ scheduledAction,
198
+ this.interactions,
200
199
  chatId,
201
200
  this.chats[chatId],
202
201
  `Scheduled:${scheduledAction.key}:${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(