1 | import * as crypto from 'crypto'
|
2 | import * as http from 'http'
|
3 | import * as https from 'https'
|
4 | import * as tg from './core/types/typegram'
|
5 | import * as tt from './telegram-types'
|
6 | import * as util from 'util'
|
7 | import { Composer, MaybePromise } from './composer'
|
8 | import ApiClient from './core/network/client'
|
9 | import { compactOptions } from './core/helpers/compact'
|
10 | import Context from './context'
|
11 | import d from 'debug'
|
12 | import generateCallback from './core/network/webhook'
|
13 | import { Polling } from './core/network/polling'
|
14 | import pTimeout from 'p-timeout'
|
15 | import Telegram from './telegram'
|
16 | import { TlsOptions } from 'tls'
|
17 | import { URL } from 'url'
|
18 | const debug = d('telegraf:main')
|
19 |
|
20 | const DEFAULT_OPTIONS: Telegraf.Options<Context> = {
|
21 | telegram: {},
|
22 | handlerTimeout: 90_000,
|
23 | contextType: Context,
|
24 | }
|
25 |
|
26 | function always<T>(x: T) {
|
27 | return () => x
|
28 | }
|
29 | const anoop = always(Promise.resolve())
|
30 |
|
31 |
|
32 | export namespace Telegraf {
|
33 | export interface Options<TContext extends Context> {
|
34 | contextType: new (
|
35 | ...args: ConstructorParameters<typeof Context>
|
36 | ) => TContext
|
37 | handlerTimeout: number
|
38 | telegram?: Partial<ApiClient.Options>
|
39 | }
|
40 |
|
41 | export interface LaunchOptions {
|
42 | dropPendingUpdates?: boolean
|
43 |
|
44 | allowedUpdates?: tt.UpdateType[]
|
45 |
|
46 | webhook?: {
|
47 |
|
48 | domain?: string
|
49 |
|
50 |
|
51 | hookPath?: string
|
52 |
|
53 | host?: string
|
54 | port?: number
|
55 |
|
56 |
|
57 | tlsOptions?: TlsOptions
|
58 |
|
59 | cb?: http.RequestListener
|
60 | }
|
61 | }
|
62 | }
|
63 |
|
64 |
|
65 | export class Telegraf<C extends Context = Context> extends Composer<C> {
|
66 | private readonly options: Telegraf.Options<C>
|
67 | private webhookServer?: http.Server | https.Server
|
68 | private polling?: Polling
|
69 |
|
70 | public botInfo?: tg.UserFromGetMe
|
71 | public telegram: Telegram
|
72 | readonly context: Partial<C> = {}
|
73 |
|
74 | private handleError = (err: unknown, ctx: C): MaybePromise<void> => {
|
75 |
|
76 |
|
77 |
|
78 | process.exitCode = 1
|
79 | console.error('Unhandled error while processing', ctx.update)
|
80 | throw err
|
81 | }
|
82 |
|
83 | constructor(token: string, options?: Partial<Telegraf.Options<C>>) {
|
84 | super()
|
85 |
|
86 | this.options = {
|
87 | ...DEFAULT_OPTIONS,
|
88 | ...compactOptions(options),
|
89 | }
|
90 | this.telegram = new Telegram(token, this.options.telegram)
|
91 | debug('Created a `Telegraf` instance')
|
92 | }
|
93 |
|
94 | private get token() {
|
95 | return this.telegram.token
|
96 | }
|
97 |
|
98 |
|
99 | set webhookReply(webhookReply: boolean) {
|
100 | this.telegram.webhookReply = webhookReply
|
101 | }
|
102 |
|
103 | get webhookReply() {
|
104 | return this.telegram.webhookReply
|
105 | }
|
106 |
|
107 | |
108 |
|
109 |
|
110 | catch(handler: (err: unknown, ctx: C) => MaybePromise<void>) {
|
111 | this.handleError = handler
|
112 | return this
|
113 | }
|
114 |
|
115 | webhookCallback(path = '/') {
|
116 | return generateCallback(
|
117 | path,
|
118 | (update: tg.Update, res: http.ServerResponse) =>
|
119 | this.handleUpdate(update, res)
|
120 | )
|
121 | }
|
122 |
|
123 | private startPolling(allowedUpdates: tt.UpdateType[] = []) {
|
124 | this.polling = new Polling(this.telegram, allowedUpdates)
|
125 |
|
126 | this.polling.loop(async (updates) => {
|
127 | await this.handleUpdates(updates)
|
128 | })
|
129 | }
|
130 |
|
131 | private startWebhook(
|
132 | hookPath: string,
|
133 | tlsOptions?: TlsOptions,
|
134 | port?: number,
|
135 | host?: string,
|
136 | cb?: http.RequestListener
|
137 | ) {
|
138 | const webhookCb = this.webhookCallback(hookPath)
|
139 | const callback: http.RequestListener =
|
140 | typeof cb === 'function'
|
141 | ? (req, res) => webhookCb(req, res, () => cb(req, res))
|
142 | : webhookCb
|
143 | this.webhookServer =
|
144 | tlsOptions != null
|
145 | ? https.createServer(tlsOptions, callback)
|
146 | : http.createServer(callback)
|
147 | this.webhookServer.listen(port, host, () => {
|
148 | debug('Webhook listening on port: %s', port)
|
149 | })
|
150 | return this
|
151 | }
|
152 |
|
153 | secretPathComponent() {
|
154 | return crypto
|
155 | .createHash('sha3-256')
|
156 | .update(this.token)
|
157 | .update(process.version)
|
158 | .digest('hex')
|
159 | }
|
160 |
|
161 | |
162 |
|
163 |
|
164 | async launch(config: Telegraf.LaunchOptions = {}) {
|
165 | debug('Connecting to Telegram')
|
166 | this.botInfo ??= await this.telegram.getMe()
|
167 | debug(`Launching @${this.botInfo.username}`)
|
168 | if (config.webhook === undefined) {
|
169 | await this.telegram.deleteWebhook({
|
170 | drop_pending_updates: config.dropPendingUpdates,
|
171 | })
|
172 | this.startPolling(config.allowedUpdates)
|
173 | debug('Bot started with long polling')
|
174 | return
|
175 | }
|
176 | if (
|
177 | typeof config.webhook.domain !== 'string' &&
|
178 | typeof config.webhook.hookPath !== 'string'
|
179 | ) {
|
180 | throw new Error('Webhook domain or webhook path is required')
|
181 | }
|
182 | let domain = config.webhook.domain ?? ''
|
183 | if (domain.startsWith('https://') || domain.startsWith('http://')) {
|
184 | domain = new URL(domain).host
|
185 | }
|
186 | const hookPath =
|
187 | config.webhook.hookPath ?? `/telegraf/${this.secretPathComponent()}`
|
188 | const { port, host, tlsOptions, cb } = config.webhook
|
189 | this.startWebhook(hookPath, tlsOptions, port, host, cb)
|
190 | if (!domain) {
|
191 | debug('Bot started with webhook')
|
192 | return
|
193 | }
|
194 | await this.telegram.setWebhook(`https://${domain}${hookPath}`, {
|
195 | drop_pending_updates: config.dropPendingUpdates,
|
196 | allowed_updates: config.allowedUpdates,
|
197 | })
|
198 | debug(`Bot started with webhook @ https://${domain}`)
|
199 | }
|
200 |
|
201 | stop(reason = 'unspecified') {
|
202 | debug('Stopping bot... Reason:', reason)
|
203 |
|
204 | if (this.polling === undefined && this.webhookServer === undefined) {
|
205 | throw new Error('Bot is not running!')
|
206 | }
|
207 | this.webhookServer?.close()
|
208 | this.polling?.stop()
|
209 | }
|
210 |
|
211 | private handleUpdates(updates: readonly tg.Update[]) {
|
212 | if (!Array.isArray(updates)) {
|
213 | throw new TypeError(util.format('Updates must be an array, got', updates))
|
214 | }
|
215 | return Promise.all(updates.map((update) => this.handleUpdate(update)))
|
216 | }
|
217 |
|
218 | private botInfoCall?: Promise<tg.UserFromGetMe>
|
219 | async handleUpdate(update: tg.Update, webhookResponse?: http.ServerResponse) {
|
220 | this.botInfo ??=
|
221 | (debug(
|
222 | 'Update %d is waiting for `botInfo` to be initialized',
|
223 | update.update_id
|
224 | ),
|
225 | await (this.botInfoCall ??= this.telegram.getMe()))
|
226 | debug('Processing update', update.update_id)
|
227 | const tg = new Telegram(this.token, this.telegram.options, webhookResponse)
|
228 | const TelegrafContext = this.options.contextType
|
229 | const ctx = new TelegrafContext(update, tg, this.botInfo)
|
230 | Object.assign(ctx, this.context)
|
231 | try {
|
232 | await pTimeout(
|
233 | Promise.resolve(this.middleware()(ctx, anoop)),
|
234 | this.options.handlerTimeout
|
235 | )
|
236 | } catch (err) {
|
237 | return await this.handleError(err, ctx)
|
238 | } finally {
|
239 | if (webhookResponse?.writableEnded === false) {
|
240 | webhookResponse.end()
|
241 | }
|
242 | debug('Finished processing update', update.update_id)
|
243 | }
|
244 | }
|
245 | }
|