@yaebal/test 0.0.1 → 0.2.0
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/README.md +400 -15
- package/lib/api.d.ts +83 -0
- package/lib/api.d.ts.map +1 -0
- package/lib/api.js +186 -0
- package/lib/api.js.map +1 -0
- package/lib/api.test.d.ts +2 -0
- package/lib/api.test.d.ts.map +1 -0
- package/lib/api.test.js +131 -0
- package/lib/api.test.js.map +1 -0
- package/lib/bot-messages.d.ts +42 -0
- package/lib/bot-messages.d.ts.map +1 -0
- package/lib/bot-messages.js +72 -0
- package/lib/bot-messages.js.map +1 -0
- package/lib/chat-actor.d.ts +45 -0
- package/lib/chat-actor.d.ts.map +1 -0
- package/lib/chat-actor.js +72 -0
- package/lib/chat-actor.js.map +1 -0
- package/lib/clock.d.ts +22 -0
- package/lib/clock.d.ts.map +1 -0
- package/lib/clock.js +72 -0
- package/lib/clock.js.map +1 -0
- package/lib/clock.test.d.ts +2 -0
- package/lib/clock.test.d.ts.map +1 -0
- package/lib/clock.test.js +69 -0
- package/lib/clock.test.js.map +1 -0
- package/lib/env.d.ts +100 -0
- package/lib/env.d.ts.map +1 -0
- package/lib/env.js +164 -0
- package/lib/env.js.map +1 -0
- package/lib/env.test.d.ts +2 -0
- package/lib/env.test.d.ts.map +1 -0
- package/lib/env.test.js +302 -0
- package/lib/env.test.js.map +1 -0
- package/lib/fetch.d.ts +3 -0
- package/lib/fetch.d.ts.map +1 -0
- package/lib/fetch.js +12 -0
- package/lib/fetch.js.map +1 -0
- package/lib/index.d.ts +19 -52
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +19 -115
- package/lib/index.js.map +1 -1
- package/lib/internal.d.ts +25 -0
- package/lib/internal.d.ts.map +1 -0
- package/lib/internal.js +19 -0
- package/lib/internal.js.map +1 -0
- package/lib/keyboard.d.ts +15 -0
- package/lib/keyboard.d.ts.map +1 -0
- package/lib/keyboard.js +25 -0
- package/lib/keyboard.js.map +1 -0
- package/lib/keyboard.test.d.ts +2 -0
- package/lib/keyboard.test.d.ts.map +1 -0
- package/lib/keyboard.test.js +31 -0
- package/lib/keyboard.test.js.map +1 -0
- package/lib/normalize.d.ts +10 -0
- package/lib/normalize.d.ts.map +1 -0
- package/lib/normalize.js +27 -0
- package/lib/normalize.js.map +1 -0
- package/lib/reactions.d.ts +3 -0
- package/lib/reactions.d.ts.map +1 -0
- package/lib/reactions.js +17 -0
- package/lib/reactions.js.map +1 -0
- package/lib/updates.d.ts +126 -0
- package/lib/updates.d.ts.map +1 -0
- package/lib/updates.js +200 -0
- package/lib/updates.js.map +1 -0
- package/lib/updates.test.d.ts +2 -0
- package/lib/updates.test.d.ts.map +1 -0
- package/lib/updates.test.js +72 -0
- package/lib/updates.test.js.map +1 -0
- package/lib/user-actor.d.ts +188 -0
- package/lib/user-actor.d.ts.map +1 -0
- package/lib/user-actor.js +465 -0
- package/lib/user-actor.js.map +1 -0
- package/lib/webhook.d.ts +18 -0
- package/lib/webhook.d.ts.map +1 -0
- package/lib/webhook.js +25 -0
- package/lib/webhook.js.map +1 -0
- package/lib/webhook.test.d.ts +2 -0
- package/lib/webhook.test.d.ts.map +1 -0
- package/lib/webhook.test.js +36 -0
- package/lib/webhook.test.js.map +1 -0
- package/package.json +7 -5
- package/src/api.test.ts +180 -0
- package/src/api.ts +289 -0
- package/src/bot-messages.ts +117 -0
- package/src/chat-actor.ts +101 -0
- package/src/clock.test.ts +80 -0
- package/src/clock.ts +118 -0
- package/src/env.test.ts +370 -0
- package/src/env.ts +235 -0
- package/src/fetch.ts +11 -0
- package/src/index.ts +79 -169
- package/src/internal.ts +43 -0
- package/src/keyboard.test.ts +35 -0
- package/src/keyboard.ts +38 -0
- package/src/normalize.ts +34 -0
- package/src/reactions.ts +18 -0
- package/src/updates.test.ts +107 -0
- package/src/updates.ts +354 -0
- package/src/user-actor.ts +702 -0
- package/src/webhook.test.ts +54 -0
- package/src/webhook.ts +48 -0
- package/lib/index.test.d.ts +0 -2
- package/lib/index.test.d.ts.map +0 -1
- package/lib/index.test.js +0 -66
- package/lib/index.test.js.map +0 -1
- package/src/index.test.ts +0 -101
|
@@ -0,0 +1,702 @@
|
|
|
1
|
+
import type { Message, MessageEntity, Update, User } from "@yaebal/core";
|
|
2
|
+
import { ChatActor } from "./chat-actor.js";
|
|
3
|
+
import type { ActorHost, SendText } from "./internal.js";
|
|
4
|
+
import { fakeFile, resolveSendText } from "./internal.js";
|
|
5
|
+
import { findButton } from "./keyboard.js";
|
|
6
|
+
import { reactionsOf } from "./reactions.js";
|
|
7
|
+
import { buildUser, createUpdate } from "./updates.js";
|
|
8
|
+
|
|
9
|
+
/** shapes not separately exported by `@yaebal/core` — derived structurally from `Message`/`Update` instead of adding a `@yaebal/types` dependency just for names. */
|
|
10
|
+
type MessageField<K extends keyof Message> = NonNullable<Message[K]>;
|
|
11
|
+
type UpdateField<K extends keyof Update> = NonNullable<Update[K]>;
|
|
12
|
+
type Location = MessageField<"location">;
|
|
13
|
+
type Contact = MessageField<"contact">;
|
|
14
|
+
type Venue = MessageField<"venue">;
|
|
15
|
+
type SuccessfulPayment = MessageField<"successful_payment">;
|
|
16
|
+
type PreCheckoutQuery = UpdateField<"pre_checkout_query">;
|
|
17
|
+
type ShippingQuery = UpdateField<"shipping_query">;
|
|
18
|
+
|
|
19
|
+
/** options shared by `sendMessage`/`sendReply`/`editMessage`. */
|
|
20
|
+
export interface MessageOptions {
|
|
21
|
+
/** extra entities to merge in alongside whatever `text` (as a `format` result) already carries. */
|
|
22
|
+
entities?: MessageEntity[];
|
|
23
|
+
/** sets `reply_to_message`. */
|
|
24
|
+
replyTo?: Message;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** options shared by the media-sending shortcuts (`sendPhoto`, `sendVideo`, ...). */
|
|
28
|
+
export interface MediaOptions {
|
|
29
|
+
/** caption — a plain string or a `format`/`fmt` result (entities auto-extracted). */
|
|
30
|
+
caption?: SendText;
|
|
31
|
+
/** sets `has_media_spoiler = true`. */
|
|
32
|
+
spoiler?: boolean;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
type ChatArg = ChatActor | undefined;
|
|
36
|
+
|
|
37
|
+
function resolveArgs<Opts>(
|
|
38
|
+
a: ChatArg | Opts,
|
|
39
|
+
b: Opts | undefined,
|
|
40
|
+
pm: ChatActor,
|
|
41
|
+
): [ChatActor, Opts] {
|
|
42
|
+
if (a instanceof ChatActor) return [a, (b ?? {}) as Opts];
|
|
43
|
+
return [pm, (a ?? {}) as Opts];
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function captionFields(caption: SendText | undefined): Record<string, unknown> {
|
|
47
|
+
if (caption === undefined) return {};
|
|
48
|
+
const { text, entities } = resolveSendText(caption);
|
|
49
|
+
return { caption: text, ...(entities.length ? { caption_entities: entities } : {}) };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* a user actor — the primary way to drive a test scenario. every method emits the update a real
|
|
54
|
+
* Telegram user's action would produce, then dispatches it through the bot under test.
|
|
55
|
+
*/
|
|
56
|
+
export class UserActor {
|
|
57
|
+
readonly id: number;
|
|
58
|
+
readonly payload: User;
|
|
59
|
+
readonly pmChat: ChatActor;
|
|
60
|
+
|
|
61
|
+
private readonly host: ActorHost;
|
|
62
|
+
|
|
63
|
+
constructor(host: ActorHost, payload: User) {
|
|
64
|
+
this.host = host;
|
|
65
|
+
this.payload = payload;
|
|
66
|
+
this.id = payload.id;
|
|
67
|
+
this.pmChat = new ChatActor(host, { type: "private", id: payload.id });
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** scope every send/click/react to a specific chat. */
|
|
71
|
+
in(chat: ChatActor): UserInChatScope {
|
|
72
|
+
return new UserInChatScope(this, chat);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** scope click/react/edit to a specific message (its own chat is inferred). */
|
|
76
|
+
on(message: Message): UserOnMessageScope {
|
|
77
|
+
return new UserOnMessageScope(this, message);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
private buildMessage(chat: ChatActor, fields: Partial<Message>): Message {
|
|
81
|
+
const message = {
|
|
82
|
+
message_id: this.host.nextMessageId(),
|
|
83
|
+
date: this.host.now(),
|
|
84
|
+
chat: chat.toChat(),
|
|
85
|
+
from: this.payload,
|
|
86
|
+
...fields,
|
|
87
|
+
} as Message;
|
|
88
|
+
|
|
89
|
+
chat.messages.push(message);
|
|
90
|
+
return message;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
private async dispatchMessage(message: Message): Promise<Message> {
|
|
94
|
+
await this.host.dispatch(createUpdate({ message }));
|
|
95
|
+
return message;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** send a text message — to the user's own PM by default, or into `chat` if given first. */
|
|
99
|
+
sendMessage(text: SendText, options?: MessageOptions): Promise<Message>;
|
|
100
|
+
sendMessage(chat: ChatActor, text: SendText, options?: MessageOptions): Promise<Message>;
|
|
101
|
+
sendMessage(
|
|
102
|
+
a: ChatActor | SendText,
|
|
103
|
+
b?: SendText | MessageOptions,
|
|
104
|
+
c?: MessageOptions,
|
|
105
|
+
): Promise<Message> {
|
|
106
|
+
const chat = a instanceof ChatActor ? a : this.pmChat;
|
|
107
|
+
const text = (a instanceof ChatActor ? b : a) as SendText;
|
|
108
|
+
const options = (a instanceof ChatActor ? c : b) as MessageOptions | undefined;
|
|
109
|
+
|
|
110
|
+
const { text: resolvedText, entities } = resolveSendText(text, options?.entities ?? []);
|
|
111
|
+
const message = this.buildMessage(chat, {
|
|
112
|
+
text: resolvedText,
|
|
113
|
+
...(entities.length ? { entities } : {}),
|
|
114
|
+
...(options?.replyTo ? { reply_to_message: options.replyTo } : {}),
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
return this.dispatchMessage(message);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** reply to `message` — same chat, `reply_to_message` set automatically. */
|
|
121
|
+
sendReply(message: Message, text: SendText): Promise<Message> {
|
|
122
|
+
const chat = this.chatOf(message);
|
|
123
|
+
return this.sendMessage(chat, text, { replyTo: message });
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/** send `/name args` — a `bot_command` entity is attached automatically. */
|
|
127
|
+
sendCommand(name: string, args?: string): Promise<Message>;
|
|
128
|
+
sendCommand(chat: ChatActor, name: string, args?: string): Promise<Message>;
|
|
129
|
+
sendCommand(a: ChatActor | string, b?: string, c?: string): Promise<Message> {
|
|
130
|
+
const chat = a instanceof ChatActor ? a : this.pmChat;
|
|
131
|
+
const name = (a instanceof ChatActor ? b : a) as string;
|
|
132
|
+
const args = a instanceof ChatActor ? c : (b as string | undefined);
|
|
133
|
+
|
|
134
|
+
const command = `/${name}`;
|
|
135
|
+
const text = args ? `${command} ${args}` : command;
|
|
136
|
+
const entity: MessageEntity = { type: "bot_command", offset: 0, length: command.length };
|
|
137
|
+
|
|
138
|
+
return this.sendMessage(chat, { text, entities: [entity] });
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
private mediaAttachment(
|
|
142
|
+
chatOrOverrides: ChatArg | MediaOptions,
|
|
143
|
+
overrides: MediaOptions | undefined,
|
|
144
|
+
fields: Record<string, unknown>,
|
|
145
|
+
): Promise<Message> {
|
|
146
|
+
const [chat, options] = resolveArgs<MediaOptions>(chatOrOverrides, overrides, this.pmChat);
|
|
147
|
+
|
|
148
|
+
const message = this.buildMessage(chat, {
|
|
149
|
+
...fields,
|
|
150
|
+
...captionFields(options.caption),
|
|
151
|
+
...(options.spoiler ? { has_media_spoiler: true } : {}),
|
|
152
|
+
} as Partial<Message>);
|
|
153
|
+
|
|
154
|
+
return this.dispatchMessage(message);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
sendPhoto(chat?: ChatActor, overrides?: MediaOptions): Promise<Message>;
|
|
158
|
+
sendPhoto(overrides?: MediaOptions): Promise<Message>;
|
|
159
|
+
sendPhoto(a?: ChatArg | MediaOptions, b?: MediaOptions): Promise<Message> {
|
|
160
|
+
return this.mediaAttachment(a, b, {
|
|
161
|
+
photo: [
|
|
162
|
+
{ ...fakeFile("photo"), width: 100, height: 100 },
|
|
163
|
+
{ ...fakeFile("photo"), width: 800, height: 600 },
|
|
164
|
+
],
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
sendVideo(chat?: ChatActor, overrides?: MediaOptions): Promise<Message>;
|
|
169
|
+
sendVideo(overrides?: MediaOptions): Promise<Message>;
|
|
170
|
+
sendVideo(a?: ChatArg | MediaOptions, b?: MediaOptions): Promise<Message> {
|
|
171
|
+
return this.mediaAttachment(a, b, {
|
|
172
|
+
video: { ...fakeFile("video"), width: 1280, height: 720, duration: 10 },
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
sendDocument(chat?: ChatActor, overrides?: MediaOptions): Promise<Message>;
|
|
177
|
+
sendDocument(overrides?: MediaOptions): Promise<Message>;
|
|
178
|
+
sendDocument(a?: ChatArg | MediaOptions, b?: MediaOptions): Promise<Message> {
|
|
179
|
+
return this.mediaAttachment(a, b, { document: { ...fakeFile("doc"), file_name: "file.bin" } });
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
sendVoice(chat?: ChatActor, overrides?: MediaOptions): Promise<Message>;
|
|
183
|
+
sendVoice(overrides?: MediaOptions): Promise<Message>;
|
|
184
|
+
sendVoice(a?: ChatArg | MediaOptions, b?: MediaOptions): Promise<Message> {
|
|
185
|
+
return this.mediaAttachment(a, b, { voice: { ...fakeFile("voice"), duration: 5 } });
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
sendAudio(chat?: ChatActor, overrides?: MediaOptions): Promise<Message>;
|
|
189
|
+
sendAudio(overrides?: MediaOptions): Promise<Message>;
|
|
190
|
+
sendAudio(a?: ChatArg | MediaOptions, b?: MediaOptions): Promise<Message> {
|
|
191
|
+
return this.mediaAttachment(a, b, { audio: { ...fakeFile("audio"), duration: 30 } });
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
sendAnimation(chat?: ChatActor, overrides?: MediaOptions): Promise<Message>;
|
|
195
|
+
sendAnimation(overrides?: MediaOptions): Promise<Message>;
|
|
196
|
+
sendAnimation(a?: ChatArg | MediaOptions, b?: MediaOptions): Promise<Message> {
|
|
197
|
+
return this.mediaAttachment(a, b, {
|
|
198
|
+
animation: { ...fakeFile("gif"), width: 480, height: 270, duration: 3 },
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
sendVideoNote(chat?: ChatActor, overrides?: MediaOptions): Promise<Message>;
|
|
203
|
+
sendVideoNote(overrides?: MediaOptions): Promise<Message>;
|
|
204
|
+
sendVideoNote(a?: ChatArg | MediaOptions, b?: MediaOptions): Promise<Message> {
|
|
205
|
+
return this.mediaAttachment(a, b, {
|
|
206
|
+
video_note: { ...fakeFile("videonote"), length: 240, duration: 10 },
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
sendSticker(chat?: ChatActor, overrides?: Partial<Message["sticker"]>): Promise<Message>;
|
|
211
|
+
sendSticker(overrides?: Partial<Message["sticker"]>): Promise<Message>;
|
|
212
|
+
sendSticker(
|
|
213
|
+
a?: ChatArg | Partial<Message["sticker"]>,
|
|
214
|
+
b?: Partial<Message["sticker"]>,
|
|
215
|
+
): Promise<Message> {
|
|
216
|
+
const [chat, overrides] = resolveArgs<Partial<Message["sticker"]>>(a, b, this.pmChat);
|
|
217
|
+
const message = this.buildMessage(chat, {
|
|
218
|
+
sticker: {
|
|
219
|
+
...fakeFile("sticker"),
|
|
220
|
+
type: "regular",
|
|
221
|
+
width: 512,
|
|
222
|
+
height: 512,
|
|
223
|
+
is_animated: false,
|
|
224
|
+
is_video: false,
|
|
225
|
+
...overrides,
|
|
226
|
+
},
|
|
227
|
+
} as Partial<Message>);
|
|
228
|
+
|
|
229
|
+
return this.dispatchMessage(message);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
sendLocation(
|
|
233
|
+
location: Partial<Location> & { latitude: number; longitude: number },
|
|
234
|
+
chat?: ChatActor,
|
|
235
|
+
): Promise<Message>;
|
|
236
|
+
sendLocation(
|
|
237
|
+
chat: ChatActor,
|
|
238
|
+
location: Partial<Location> & { latitude: number; longitude: number },
|
|
239
|
+
): Promise<Message>;
|
|
240
|
+
sendLocation(
|
|
241
|
+
a: ChatActor | (Partial<Location> & { latitude: number; longitude: number }),
|
|
242
|
+
b?: ChatActor | (Partial<Location> & { latitude: number; longitude: number }),
|
|
243
|
+
): Promise<Message> {
|
|
244
|
+
const chat = a instanceof ChatActor ? a : this.pmChat;
|
|
245
|
+
const location = (a instanceof ChatActor ? b : a) as Location;
|
|
246
|
+
|
|
247
|
+
return this.dispatchMessage(this.buildMessage(chat, { location } as Partial<Message>));
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
sendContact(
|
|
251
|
+
contact: Partial<Contact> & { phone_number: string; first_name: string },
|
|
252
|
+
chat?: ChatActor,
|
|
253
|
+
): Promise<Message>;
|
|
254
|
+
sendContact(
|
|
255
|
+
chat: ChatActor,
|
|
256
|
+
contact: Partial<Contact> & { phone_number: string; first_name: string },
|
|
257
|
+
): Promise<Message>;
|
|
258
|
+
sendContact(
|
|
259
|
+
a: ChatActor | (Partial<Contact> & { phone_number: string; first_name: string }),
|
|
260
|
+
b?: ChatActor | (Partial<Contact> & { phone_number: string; first_name: string }),
|
|
261
|
+
): Promise<Message> {
|
|
262
|
+
const chat = a instanceof ChatActor ? a : this.pmChat;
|
|
263
|
+
const contact = (a instanceof ChatActor ? b : a) as Contact;
|
|
264
|
+
|
|
265
|
+
return this.dispatchMessage(this.buildMessage(chat, { contact } as Partial<Message>));
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
sendVenue(
|
|
269
|
+
venue: Partial<Venue> & { location: Location; title: string; address: string },
|
|
270
|
+
chat?: ChatActor,
|
|
271
|
+
): Promise<Message>;
|
|
272
|
+
sendVenue(
|
|
273
|
+
chat: ChatActor,
|
|
274
|
+
venue: Partial<Venue> & { location: Location; title: string; address: string },
|
|
275
|
+
): Promise<Message>;
|
|
276
|
+
sendVenue(
|
|
277
|
+
a: ChatActor | (Partial<Venue> & { location: Location; title: string; address: string }),
|
|
278
|
+
b?: ChatActor | (Partial<Venue> & { location: Location; title: string; address: string }),
|
|
279
|
+
): Promise<Message> {
|
|
280
|
+
const chat = a instanceof ChatActor ? a : this.pmChat;
|
|
281
|
+
const venue = (a instanceof ChatActor ? b : a) as Venue;
|
|
282
|
+
|
|
283
|
+
return this.dispatchMessage(this.buildMessage(chat, { venue } as Partial<Message>));
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
sendDice(chat?: ChatActor, emoji?: string): Promise<Message>;
|
|
287
|
+
sendDice(emoji?: string): Promise<Message>;
|
|
288
|
+
sendDice(a?: ChatArg | string, b?: string): Promise<Message> {
|
|
289
|
+
const chat = a instanceof ChatActor ? a : this.pmChat;
|
|
290
|
+
const emoji = (a instanceof ChatActor ? b : a) ?? "🎲";
|
|
291
|
+
const faces: Record<string, number> = { "🎯": 6, "🏀": 5, "⚽": 5, "🎳": 6, "🎰": 64 };
|
|
292
|
+
const value = 1 + Math.floor(Math.random() * (faces[emoji] ?? 6));
|
|
293
|
+
|
|
294
|
+
return this.dispatchMessage(
|
|
295
|
+
this.buildMessage(chat, { dice: { emoji, value } } as Partial<Message>),
|
|
296
|
+
);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/** send several media items as one album — one `message` update per item, sharing `media_group_id`. */
|
|
300
|
+
async sendMediaGroup(chat: ChatActor, items: Array<Partial<Message>>): Promise<Message[]> {
|
|
301
|
+
const groupId = `mg_${this.host.nextMessageId()}`;
|
|
302
|
+
const out: Message[] = [];
|
|
303
|
+
|
|
304
|
+
for (const item of items) {
|
|
305
|
+
const message = this.buildMessage(chat, {
|
|
306
|
+
...item,
|
|
307
|
+
media_group_id: groupId,
|
|
308
|
+
} as Partial<Message>);
|
|
309
|
+
out.push(await this.dispatchMessage(message));
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
return out;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
private chatOf(message: Message): ChatActor {
|
|
316
|
+
return message.chat.id === this.pmChat.id
|
|
317
|
+
? this.pmChat
|
|
318
|
+
: ChatActor.fromChat(this.host, message.chat);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/** edit a message's text in place — dispatches `edited_message`; `ctx.updatedAt` (if your context exposes it) reflects the new `edit_date`. */
|
|
322
|
+
async editMessage(message: Message, text: SendText): Promise<Message> {
|
|
323
|
+
const { text: resolvedText, entities } = resolveSendText(text);
|
|
324
|
+
|
|
325
|
+
const edited = {
|
|
326
|
+
...message,
|
|
327
|
+
text: resolvedText,
|
|
328
|
+
...(entities.length ? { entities } : { entities: undefined }),
|
|
329
|
+
edit_date: this.host.now(),
|
|
330
|
+
} as Message;
|
|
331
|
+
|
|
332
|
+
await this.host.dispatch(createUpdate({ edited_message: edited }));
|
|
333
|
+
return edited;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/** forward `message` — to the user's own PM by default, or to `toChat`. */
|
|
337
|
+
async forwardMessage(message: Message, toChat?: ChatActor): Promise<Message> {
|
|
338
|
+
const chat = toChat ?? this.pmChat;
|
|
339
|
+
const { message_id: _id, date: _date, chat: _chat, from, ...content } = message;
|
|
340
|
+
|
|
341
|
+
const forwarded = this.buildMessage(chat, {
|
|
342
|
+
...content,
|
|
343
|
+
forward_origin: from
|
|
344
|
+
? { type: "user", date: message.date, sender_user: from }
|
|
345
|
+
: {
|
|
346
|
+
type: "channel",
|
|
347
|
+
date: message.date,
|
|
348
|
+
chat: message.chat,
|
|
349
|
+
message_id: message.message_id,
|
|
350
|
+
},
|
|
351
|
+
} as Partial<Message>);
|
|
352
|
+
|
|
353
|
+
return this.dispatchMessage(forwarded);
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
/** pin `message` — dispatches a service `message` update with `pinned_message` set. */
|
|
357
|
+
async pinMessage(message: Message, inChat?: ChatActor): Promise<Message> {
|
|
358
|
+
const chat = inChat ?? this.chatOf(message);
|
|
359
|
+
const service = this.buildMessage(chat, { pinned_message: message } as Partial<Message>);
|
|
360
|
+
|
|
361
|
+
return this.dispatchMessage(service);
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
/** click an inline button by its `callback_data` — dispatches `callback_query`. */
|
|
365
|
+
async click(callbackData: string, message?: Message): Promise<void> {
|
|
366
|
+
const msg = message ?? this.buildMessage(this.pmChat, {});
|
|
367
|
+
|
|
368
|
+
await this.host.dispatch(
|
|
369
|
+
createUpdate({
|
|
370
|
+
callback_query: {
|
|
371
|
+
id: String(this.host.nextMessageId()),
|
|
372
|
+
chat_instance: "0",
|
|
373
|
+
from: this.payload,
|
|
374
|
+
data: callbackData,
|
|
375
|
+
message: msg,
|
|
376
|
+
},
|
|
377
|
+
}),
|
|
378
|
+
);
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
/** react to `message` with one or more emojis — `old_reaction` is inferred from this user's last reaction on it. pass `[]` to clear. */
|
|
382
|
+
async react(emojis: string | string[], message: Message): Promise<void> {
|
|
383
|
+
const newReaction = Array.isArray(emojis) ? emojis : [emojis];
|
|
384
|
+
const state = reactionsOf(message);
|
|
385
|
+
const oldReaction = state.get(this.id) ?? [];
|
|
386
|
+
|
|
387
|
+
if (newReaction.length === 0) state.delete(this.id);
|
|
388
|
+
else state.set(this.id, newReaction);
|
|
389
|
+
|
|
390
|
+
await this.host.dispatch(
|
|
391
|
+
createUpdate({
|
|
392
|
+
message_reaction: {
|
|
393
|
+
chat: message.chat,
|
|
394
|
+
message_id: message.message_id,
|
|
395
|
+
user: this.payload,
|
|
396
|
+
date: this.host.now(),
|
|
397
|
+
old_reaction: oldReaction.map((emoji) => ({ type: "emoji", emoji })),
|
|
398
|
+
new_reaction: newReaction.map((emoji) => ({ type: "emoji", emoji })),
|
|
399
|
+
},
|
|
400
|
+
}),
|
|
401
|
+
);
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
/** join `chat` — emits `chat_member` (membership change) and a `new_chat_members` service message. */
|
|
405
|
+
async join(chat: ChatActor): Promise<void> {
|
|
406
|
+
chat.members.add(this);
|
|
407
|
+
chat.setMembership(this.id, { status: "member", since: this.host.now() });
|
|
408
|
+
|
|
409
|
+
await this.host.dispatch(
|
|
410
|
+
createUpdate({
|
|
411
|
+
chat_member: {
|
|
412
|
+
chat: chat.toChat(),
|
|
413
|
+
from: this.payload,
|
|
414
|
+
date: this.host.now(),
|
|
415
|
+
old_chat_member: { status: "left", user: this.payload },
|
|
416
|
+
new_chat_member: { status: "member", user: this.payload },
|
|
417
|
+
},
|
|
418
|
+
}),
|
|
419
|
+
);
|
|
420
|
+
|
|
421
|
+
await this.dispatchMessage(
|
|
422
|
+
this.buildMessage(chat, { new_chat_members: [this.payload] } as Partial<Message>),
|
|
423
|
+
);
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
/** leave `chat` — emits `chat_member` (membership change) and a `left_chat_member` service message. */
|
|
427
|
+
async leave(chat: ChatActor): Promise<void> {
|
|
428
|
+
chat.members.delete(this);
|
|
429
|
+
chat.setMembership(this.id, { status: "left", since: this.host.now() });
|
|
430
|
+
|
|
431
|
+
await this.host.dispatch(
|
|
432
|
+
createUpdate({
|
|
433
|
+
chat_member: {
|
|
434
|
+
chat: chat.toChat(),
|
|
435
|
+
from: this.payload,
|
|
436
|
+
date: this.host.now(),
|
|
437
|
+
old_chat_member: { status: "member", user: this.payload },
|
|
438
|
+
new_chat_member: { status: "left", user: this.payload },
|
|
439
|
+
},
|
|
440
|
+
}),
|
|
441
|
+
);
|
|
442
|
+
|
|
443
|
+
await this.dispatchMessage(
|
|
444
|
+
this.buildMessage(chat, { left_chat_member: this.payload } as Partial<Message>),
|
|
445
|
+
);
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
/** send an inline query — pass a `ChatActor` to set `chat_type` automatically. */
|
|
449
|
+
async sendInlineQuery(
|
|
450
|
+
query: string,
|
|
451
|
+
chatOrOptions?: ChatActor | { offset?: string },
|
|
452
|
+
options?: { offset?: string },
|
|
453
|
+
): Promise<void> {
|
|
454
|
+
const chat = chatOrOptions instanceof ChatActor ? chatOrOptions : undefined;
|
|
455
|
+
const opts = (chatOrOptions instanceof ChatActor ? options : chatOrOptions) ?? {};
|
|
456
|
+
|
|
457
|
+
await this.host.dispatch(
|
|
458
|
+
createUpdate({
|
|
459
|
+
inline_query: {
|
|
460
|
+
id: String(this.host.nextMessageId()),
|
|
461
|
+
from: this.payload,
|
|
462
|
+
query,
|
|
463
|
+
offset: opts.offset ?? "",
|
|
464
|
+
...(chat ? { chat_type: chat.type === "private" ? "private" : chat.type } : {}),
|
|
465
|
+
},
|
|
466
|
+
}),
|
|
467
|
+
);
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
/** choose a result from a previous inline query — dispatches `chosen_inline_result`. */
|
|
471
|
+
async chooseInlineResult(
|
|
472
|
+
resultId: string,
|
|
473
|
+
query: string,
|
|
474
|
+
options: { inlineMessageId?: string } = {},
|
|
475
|
+
): Promise<void> {
|
|
476
|
+
await this.host.dispatch(
|
|
477
|
+
createUpdate({
|
|
478
|
+
chosen_inline_result: {
|
|
479
|
+
result_id: resultId,
|
|
480
|
+
from: this.payload,
|
|
481
|
+
query,
|
|
482
|
+
...(options.inlineMessageId ? { inline_message_id: options.inlineMessageId } : {}),
|
|
483
|
+
},
|
|
484
|
+
}),
|
|
485
|
+
);
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
/** emit a `pre_checkout_query` — the bot is expected to `answerPreCheckoutQuery`. */
|
|
489
|
+
async sendPreCheckoutQuery(overrides: Partial<PreCheckoutQuery> = {}): Promise<PreCheckoutQuery> {
|
|
490
|
+
const query: PreCheckoutQuery = {
|
|
491
|
+
id: String(this.host.nextMessageId()),
|
|
492
|
+
from: this.payload,
|
|
493
|
+
currency: "XTR",
|
|
494
|
+
total_amount: 100,
|
|
495
|
+
invoice_payload: "payload",
|
|
496
|
+
...overrides,
|
|
497
|
+
};
|
|
498
|
+
|
|
499
|
+
await this.host.dispatch(createUpdate({ pre_checkout_query: query }));
|
|
500
|
+
return query;
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
/** emit a `shipping_query`. default shipping address is New York, US. */
|
|
504
|
+
async sendShippingQuery(overrides: Partial<ShippingQuery> = {}): Promise<ShippingQuery> {
|
|
505
|
+
const query: ShippingQuery = {
|
|
506
|
+
id: String(this.host.nextMessageId()),
|
|
507
|
+
from: this.payload,
|
|
508
|
+
invoice_payload: "payload",
|
|
509
|
+
shipping_address: {
|
|
510
|
+
country_code: "US",
|
|
511
|
+
state: "",
|
|
512
|
+
city: "New York",
|
|
513
|
+
street_line1: "",
|
|
514
|
+
street_line2: "",
|
|
515
|
+
post_code: "10001",
|
|
516
|
+
},
|
|
517
|
+
...overrides,
|
|
518
|
+
};
|
|
519
|
+
|
|
520
|
+
await this.host.dispatch(createUpdate({ shipping_query: query }));
|
|
521
|
+
return query;
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
/**
|
|
525
|
+
* the full payments flow: emits `pre_checkout_query`, then throws unless the bot answered it
|
|
526
|
+
* with `ok: true` (real Telegram never sends `successful_payment` otherwise), then dispatches
|
|
527
|
+
* a `message` update carrying `successful_payment`.
|
|
528
|
+
*/
|
|
529
|
+
async sendSuccessfulPayment(
|
|
530
|
+
chatOrOverrides?: ChatActor | Partial<SuccessfulPayment>,
|
|
531
|
+
overrides?: Partial<SuccessfulPayment>,
|
|
532
|
+
): Promise<Message> {
|
|
533
|
+
const [chat, payment] = resolveArgs<Partial<SuccessfulPayment>>(
|
|
534
|
+
chatOrOverrides,
|
|
535
|
+
overrides,
|
|
536
|
+
this.pmChat,
|
|
537
|
+
);
|
|
538
|
+
|
|
539
|
+
const preCheckout = await this.sendPreCheckoutQuery({
|
|
540
|
+
currency: payment.currency,
|
|
541
|
+
total_amount: payment.total_amount,
|
|
542
|
+
invoice_payload: payment.invoice_payload,
|
|
543
|
+
});
|
|
544
|
+
|
|
545
|
+
const approved = this.host.answeredPreCheckoutQuery(preCheckout.id);
|
|
546
|
+
if (!approved) {
|
|
547
|
+
throw new Error(
|
|
548
|
+
"sendSuccessfulPayment(): the bot never answered the pre_checkout_query with { ok: true } — " +
|
|
549
|
+
"real Telegram would never send successful_payment in that case",
|
|
550
|
+
);
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
const message = this.buildMessage(chat, {
|
|
554
|
+
successful_payment: {
|
|
555
|
+
currency: "XTR",
|
|
556
|
+
total_amount: 100,
|
|
557
|
+
invoice_payload: "payload",
|
|
558
|
+
...payment,
|
|
559
|
+
},
|
|
560
|
+
} as Partial<Message>);
|
|
561
|
+
|
|
562
|
+
return this.dispatchMessage(message);
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
/** returned by {@link UserActor.in} — every method delegates to the underlying user, pre-bound to `chat`. */
|
|
567
|
+
export class UserInChatScope {
|
|
568
|
+
constructor(
|
|
569
|
+
private readonly user: UserActor,
|
|
570
|
+
private readonly chat: ChatActor,
|
|
571
|
+
) {}
|
|
572
|
+
|
|
573
|
+
sendMessage(text: SendText, options?: MessageOptions): Promise<Message> {
|
|
574
|
+
return this.user.sendMessage(this.chat, text, options);
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
sendReply(message: Message, text: SendText): Promise<Message> {
|
|
578
|
+
return this.user.sendReply(message, text);
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
sendCommand(name: string, args?: string): Promise<Message> {
|
|
582
|
+
return this.user.sendCommand(this.chat, name, args);
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
sendPhoto(overrides?: MediaOptions): Promise<Message> {
|
|
586
|
+
return this.user.sendPhoto(this.chat, overrides);
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
sendVideo(overrides?: MediaOptions): Promise<Message> {
|
|
590
|
+
return this.user.sendVideo(this.chat, overrides);
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
sendDocument(overrides?: MediaOptions): Promise<Message> {
|
|
594
|
+
return this.user.sendDocument(this.chat, overrides);
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
sendVoice(overrides?: MediaOptions): Promise<Message> {
|
|
598
|
+
return this.user.sendVoice(this.chat, overrides);
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
sendAudio(overrides?: MediaOptions): Promise<Message> {
|
|
602
|
+
return this.user.sendAudio(this.chat, overrides);
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
sendAnimation(overrides?: MediaOptions): Promise<Message> {
|
|
606
|
+
return this.user.sendAnimation(this.chat, overrides);
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
sendVideoNote(overrides?: MediaOptions): Promise<Message> {
|
|
610
|
+
return this.user.sendVideoNote(this.chat, overrides);
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
sendSticker(overrides?: Partial<Message["sticker"]>): Promise<Message> {
|
|
614
|
+
return this.user.sendSticker(this.chat, overrides);
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
sendLocation(
|
|
618
|
+
location: Partial<Location> & { latitude: number; longitude: number },
|
|
619
|
+
): Promise<Message> {
|
|
620
|
+
return this.user.sendLocation(this.chat, location);
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
sendContact(
|
|
624
|
+
contact: Partial<Contact> & { phone_number: string; first_name: string },
|
|
625
|
+
): Promise<Message> {
|
|
626
|
+
return this.user.sendContact(this.chat, contact);
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
sendVenue(
|
|
630
|
+
venue: Partial<Venue> & { location: Location; title: string; address: string },
|
|
631
|
+
): Promise<Message> {
|
|
632
|
+
return this.user.sendVenue(this.chat, venue);
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
sendDice(emoji?: string): Promise<Message> {
|
|
636
|
+
return this.user.sendDice(this.chat, emoji);
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
sendMediaGroup(items: Array<Partial<Message>>): Promise<Message[]> {
|
|
640
|
+
return this.user.sendMediaGroup(this.chat, items);
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
sendInlineQuery(query: string, options?: { offset?: string }): Promise<void> {
|
|
644
|
+
return this.user.sendInlineQuery(query, this.chat, options);
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
sendSuccessfulPayment(overrides?: Partial<SuccessfulPayment>): Promise<Message> {
|
|
648
|
+
return this.user.sendSuccessfulPayment(this.chat, overrides);
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
join(): Promise<void> {
|
|
652
|
+
return this.user.join(this.chat);
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
leave(): Promise<void> {
|
|
656
|
+
return this.user.leave(this.chat);
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
on(message: Message): UserOnMessageScope {
|
|
660
|
+
return this.user.on(message);
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
/** returned by {@link UserActor.on} — click/react/edit pre-bound to `message`. */
|
|
665
|
+
export class UserOnMessageScope {
|
|
666
|
+
constructor(
|
|
667
|
+
private readonly user: UserActor,
|
|
668
|
+
private readonly message: Message,
|
|
669
|
+
) {}
|
|
670
|
+
|
|
671
|
+
click(callbackData: string): Promise<void> {
|
|
672
|
+
return this.user.click(callbackData, this.message);
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
/** find a button by its label (string or regex) in `message`'s inline keyboard and click it. throws if none match. */
|
|
676
|
+
clickByText(match: string | RegExp): Promise<void> {
|
|
677
|
+
const button = findButton((this.message as { reply_markup?: unknown }).reply_markup, match);
|
|
678
|
+
if (!button) {
|
|
679
|
+
throw new Error(`clickByText(): no inline button matching ${String(match)} on this message`);
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
return this.user.click(String(button.callback_data), this.message);
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
react(emojis: string | string[]): Promise<void> {
|
|
686
|
+
return this.user.react(emojis, this.message);
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
editMessage(text: SendText): Promise<Message> {
|
|
690
|
+
return this.user.editMessage(this.message, text);
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
forwardMessage(toChat?: ChatActor): Promise<Message> {
|
|
694
|
+
return this.user.forwardMessage(this.message, toChat);
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
pinMessage(inChat?: ChatActor): Promise<Message> {
|
|
698
|
+
return this.user.pinMessage(this.message, inChat);
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
export { buildUser };
|