1
+ /* eslint @typescript-eslint/restrict-template-expressions: [ "error", { "allowAny": true } ] */
2
+ import * as crypto from 'crypto'
3
+ import * as fs from 'fs'
4
+ import type * as http from 'http'
5
+ import * as https from 'https'
6
+ import * as path from 'path'
7
+ import fetch , { RequestInit } from 'node-fetch'
8
+ import MultipartStream from './multipart-stream'
9
+ import TelegramError from './error'
10
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
1
11
const debug = require ( 'debug' ) ( 'telegraf:client' )
2
- const crypto = require ( 'crypto' )
3
- const fetch = require ( 'node-fetch' ) . default
4
- const fs = require ( 'fs' )
5
- const https = require ( 'https' )
6
- const path = require ( 'path' )
7
- const TelegramError = require ( './error' )
8
- const MultipartStream = require ( './multipart-stream' )
9
12
const { isStream } = MultipartStream
10
13
11
14
const WEBHOOK_BLACKLIST = [
@@ -19,41 +22,44 @@ const WEBHOOK_BLACKLIST = [
19
22
'getMe' ,
20
23
'getUserProfilePhotos' ,
21
24
'getWebhookInfo' ,
22
- 'exportChatInviteLink'
25
+ 'exportChatInviteLink' ,
23
26
]
24
27
28
+ // eslint-disable-next-line @typescript-eslint/no-namespace
29
+ namespace ApiClient {
30
+ export interface Options {
31
+ agent ?: https . Agent | http . Agent
32
+ apiRoot : string
33
+ webhookReply : boolean
34
+ }
35
+ }
36
+
25
37
const DEFAULT_EXTENSIONS = {
26
38
audio : 'mp3' ,
27
39
photo : 'jpg' ,
28
40
sticker : 'webp' ,
29
41
video : 'mp4' ,
30
42
animation : 'mp4' ,
31
43
video_note : 'mp4' ,
32
- voice : 'ogg'
44
+ voice : 'ogg' ,
33
45
}
34
46
35
47
const DEFAULT_OPTIONS = {
36
48
apiRoot : 'https://api.telegram.org' ,
37
49
webhookReply : true ,
38
50
agent : new https . Agent ( {
39
51
keepAlive : true ,
40
- keepAliveMsecs : 10000
41
- } )
52
+ keepAliveMsecs : 10000 ,
53
+ } ) ,
42
54
}
43
55
44
56
const WEBHOOK_REPLY_STUB = {
45
57
webhook : true ,
46
- details : 'https://core.telegram.org/bots/api#making-requests-when-getting-updates'
47
- }
48
-
49
- function safeJSONParse ( text ) {
50
- try {
51
- return JSON . parse ( text )
52
- } catch ( err ) {
53
- debug ( 'JSON parse failed' , err )
54
- }
58
+ details :
59
+ 'https://core.telegram.org/bots/api#making-requests-when-getting-updates' ,
55
60
}
56
61
62
+ // prettier-ignore
57
63
function includesMedia ( payload ) {
58
64
return Object . keys ( payload ) . some (
59
65
( key ) => {
@@ -70,12 +76,12 @@ function includesMedia (payload) {
70
76
)
71
77
}
72
78
73
- function buildJSONConfig ( payload ) {
79
+ function buildJSONConfig ( payload ) : Promise < RequestInit > {
74
80
return Promise . resolve ( {
75
81
method : 'POST' ,
76
82
compress : true ,
77
83
headers : { 'content-type' : 'application/json' , connection : 'keep-alive' } ,
78
- body : JSON . stringify ( payload )
84
+ body : JSON . stringify ( payload ) ,
79
85
} )
80
86
}
81
87
@@ -84,28 +90,34 @@ const FORM_DATA_JSON_FIELDS = [
84
90
'reply_markup' ,
85
91
'mask_position' ,
86
92
'shipping_options' ,
87
- 'errors'
93
+ 'errors' ,
88
94
]
89
95
90
- function buildFormDataConfig ( payload , agent ) {
96
+ function buildFormDataConfig ( payload , agent ) : Promise < RequestInit > {
91
97
for ( const field of FORM_DATA_JSON_FIELDS ) {
92
98
if ( field in payload && typeof payload [ field ] !== 'string' ) {
93
99
payload [ field ] = JSON . stringify ( payload [ field ] )
94
100
}
95
101
}
96
102
const boundary = crypto . randomBytes ( 32 ) . toString ( 'hex' )
97
103
const formData = new MultipartStream ( boundary )
98
- const tasks = Object . keys ( payload ) . map ( ( key ) => attachFormValue ( formData , key , payload [ key ] , agent ) )
104
+ const tasks = Object . keys ( payload ) . map ( ( key ) =>
105
+ attachFormValue ( formData , key , payload [ key ] , agent )
106
+ )
99
107
return Promise . all ( tasks ) . then ( ( ) => {
100
108
return {
101
109
method : 'POST' ,
102
110
compress : true ,
103
- headers : { 'content-type' : `multipart/form-data; boundary=${ boundary } ` , connection : 'keep-alive' } ,
104
- body : formData
111
+ headers : {
112
+ 'content-type' : `multipart/form-data; boundary=${ boundary } ` ,
113
+ connection : 'keep-alive' ,
114
+ } ,
115
+ body : formData ,
105
116
}
106
117
} )
107
118
}
108
119
120
+ // prettier-ignore
109
121
function attachFormValue ( form , id , value , agent ) {
110
122
if ( ! value ) {
111
123
return Promise . resolve ( )
@@ -155,6 +167,7 @@ function attachFormValue (form, id, value, agent) {
155
167
return attachFormMedia ( form , value , id , agent )
156
168
}
157
169
170
+ // prettier-ignore
158
171
function attachFormMedia ( form , media , id , agent ) {
159
172
let fileName = media . filename || `${ id } .${ DEFAULT_EXTENSIONS [ id ] || 'dat' } `
160
173
if ( media . url ) {
@@ -180,11 +193,12 @@ function attachFormMedia (form, media, id, agent) {
180
193
return Promise . resolve ( )
181
194
}
182
195
196
+ // prettier-ignore
183
197
function isKoaResponse ( response ) {
184
198
return typeof response . set === 'function' && typeof response . header === 'object'
185
199
}
186
200
187
- function answerToWebhook ( response , payload = { } , options ) {
201
+ function answerToWebhook ( response , payload , options : ApiClient . Options ) {
188
202
if ( ! includesMedia ( payload ) ) {
189
203
if ( isKoaResponse ( response ) ) {
190
204
response . body = payload
@@ -198,63 +212,84 @@ function answerToWebhook (response, payload = {}, options) {
198
212
response . end ( JSON . stringify ( payload ) , 'utf-8' )
199
213
return resolve ( WEBHOOK_REPLY_STUB )
200
214
}
201
- response . end ( JSON . stringify ( payload ) , 'utf-8' , ( ) => resolve ( WEBHOOK_REPLY_STUB ) )
215
+ response . end ( JSON . stringify ( payload ) , 'utf-8' , ( ) =>
216
+ resolve ( WEBHOOK_REPLY_STUB )
217
+ )
202
218
} )
203
219
}
204
220
205
- return buildFormDataConfig ( payload , options . agent )
206
- . then ( ( { headers, body } ) => {
221
+ return buildFormDataConfig ( payload , options . agent ) . then (
222
+ ( { headers = { } , body } ) => {
207
223
if ( isKoaResponse ( response ) ) {
208
- Object . keys ( headers ) . forEach ( key => response . set ( key , headers [ key ] ) )
224
+ Object . keys ( headers ) . forEach ( ( key ) => response . set ( key , headers [ key ] ) )
209
225
response . body = body
210
226
return Promise . resolve ( WEBHOOK_REPLY_STUB )
211
227
}
212
228
if ( ! response . headersSent ) {
213
- Object . keys ( headers ) . forEach ( key => response . setHeader ( key , headers [ key ] ) )
229
+ Object . keys ( headers ) . forEach ( ( key ) =>
230
+ response . setHeader ( key , headers [ key ] )
231
+ )
214
232
}
215
233
return new Promise ( ( resolve ) => {
216
234
response . on ( 'finish' , ( ) => resolve ( WEBHOOK_REPLY_STUB ) )
235
+ // @ts -expect-error
217
236
body . pipe ( response )
218
237
} )
219
- } )
238
+ }
239
+ )
220
240
}
221
241
242
+ // eslint-disable-next-line no-redeclare
222
243
class ApiClient {
223
- constructor ( token , options , webhookResponse ) {
244
+ readonly options : ApiClient . Options
245
+ private responseEnd = false
246
+
247
+ constructor (
248
+ public token : string ,
249
+ options ?: Partial < ApiClient . Options > ,
250
+ private readonly response ?
251
+ ) {
224
252
this . token = token
225
253
this . options = {
226
254
...DEFAULT_OPTIONS ,
227
- ...options
255
+ ...options ,
228
256
}
229
257
if ( this . options . apiRoot . startsWith ( 'http://' ) ) {
230
- this . options . agent = null
258
+ this . options . agent = undefined
231
259
}
232
- this . response = webhookResponse
233
260
}
234
261
235
- set webhookReply ( enable ) {
262
+ set webhookReply ( enable : boolean ) {
236
263
this . options . webhookReply = enable
237
264
}
238
265
239
- get webhookReply ( ) {
266
+ get webhookReply ( ) {
240
267
return this . options . webhookReply
241
268
}
242
269
243
- callApi ( method , data = { } ) {
270
+ callApi ( method : string , data = { } ) {
244
271
const { token, options, response, responseEnd } = this
245
272
246
273
const payload = Object . keys ( data )
247
274
. filter ( ( key ) => typeof data [ key ] !== 'undefined' && data [ key ] !== null )
248
275
. reduce ( ( acc , key ) => ( { ...acc , [ key ] : data [ key ] } ) , { } )
249
276
250
- if ( options . webhookReply && response && ! responseEnd && ! WEBHOOK_BLACKLIST . includes ( method ) ) {
277
+ if (
278
+ options . webhookReply &&
279
+ response &&
280
+ ! responseEnd &&
281
+ ! WEBHOOK_BLACKLIST . includes ( method )
282
+ ) {
251
283
debug ( 'Call via webhook' , method , payload )
252
284
this . responseEnd = true
253
285
return answerToWebhook ( response , { method, ...payload } , options )
254
286
}
255
287
256
288
if ( ! token ) {
257
- throw new TelegramError ( { error_code : 401 , description : 'Bot Token is required' } )
289
+ throw new TelegramError ( {
290
+ error_code : 401 ,
291
+ description : 'Bot Token is required' ,
292
+ } )
258
293
}
259
294
260
295
debug ( 'HTTP call' , method , payload )
@@ -267,14 +302,7 @@ class ApiClient {
267
302
config . agent = options . agent
268
303
return fetch ( apiUrl , config )
269
304
} )
270
- . then ( ( res ) => res . text ( ) )
271
- . then ( ( text ) => {
272
- return safeJSONParse ( text ) || {
273
- error_code : 500 ,
274
- description : 'Unsupported http response from Telegram' ,
275
- response : text
276
- }
277
- } )
305
+ . then ( ( res ) => res . json ( ) )
278
306
. then ( ( data ) => {
279
307
if ( ! data . ok ) {
280
308
debug ( 'API call failed' , data )
@@ -285,4 +313,4 @@ class ApiClient {
285
313
}
286
314
}
287
315
288
- module . exports = ApiClient
316
+ export = ApiClient
0 commit comments