Skip to content

Commit a74a6e9

Browse files
committed
Type ApiClient and Telegraf
1 parent c0e78fa commit a74a6e9

File tree

5 files changed

+275
-149
lines changed

5 files changed

+275
-149
lines changed

.eslintrc

+1
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
"files": "*.ts",
99
"extends": ["plugin:prettier/recommended", "prettier/@typescript-eslint"],
1010
"rules": {
11+
"sort-imports": ["warn", { "ignoreCase": true }],
1112
"@typescript-eslint/ban-ts-comment": "warn",
1213
"@typescript-eslint/explicit-function-return-type": "off",
1314
"@typescript-eslint/no-explicit-any": "warn",

package.json

+1
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@
5555
},
5656
"devDependencies": {
5757
"@types/node": "^13.1.0",
58+
"@types/node-fetch": "^2.5.7",
5859
"@typescript-eslint/eslint-plugin": "^3.5.0",
5960
"@typescript-eslint/parser": "^3.5.0",
6061
"ava": "^3.0.0",

src/context.ts

+7-3
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
1-
import type Telegram from './telegram'
21
import type * as tt from '../typings/telegram-types'
3-
import { Tail } from './types'
2+
import type ApiClient from './core/network/client'
3+
import type { Tail } from './types'
4+
import type Telegram from './telegram'
45

5-
type Shorthand<FName extends keyof Telegram> = Tail<Parameters<Telegram[FName]>>
6+
type Shorthand<FName extends Exclude<keyof Telegram, keyof ApiClient>> = Tail<
7+
Parameters<Telegram[FName]>
8+
>
69

710
const UpdateTypes = [
811
'callback_query',
@@ -57,6 +60,7 @@ const MessageSubTypesMapping = {
5760
}
5861

5962
class TelegrafContext {
63+
public botInfo?: tt.User
6064
readonly updateType: tt.UpdateType
6165
readonly updateSubTypes: ReadonlyArray<typeof MessageSubTypes[number]>
6266
/** @deprecated */

src/core/network/client.ts

+80-52
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,14 @@
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
111
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')
912
const { isStream } = MultipartStream
1013

1114
const WEBHOOK_BLACKLIST = [
@@ -19,41 +22,44 @@ const WEBHOOK_BLACKLIST = [
1922
'getMe',
2023
'getUserProfilePhotos',
2124
'getWebhookInfo',
22-
'exportChatInviteLink'
25+
'exportChatInviteLink',
2326
]
2427

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+
2537
const DEFAULT_EXTENSIONS = {
2638
audio: 'mp3',
2739
photo: 'jpg',
2840
sticker: 'webp',
2941
video: 'mp4',
3042
animation: 'mp4',
3143
video_note: 'mp4',
32-
voice: 'ogg'
44+
voice: 'ogg',
3345
}
3446

3547
const DEFAULT_OPTIONS = {
3648
apiRoot: 'https://api.telegram.org',
3749
webhookReply: true,
3850
agent: new https.Agent({
3951
keepAlive: true,
40-
keepAliveMsecs: 10000
41-
})
52+
keepAliveMsecs: 10000,
53+
}),
4254
}
4355

4456
const WEBHOOK_REPLY_STUB = {
4557
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',
5560
}
5661

62+
// prettier-ignore
5763
function includesMedia (payload) {
5864
return Object.keys(payload).some(
5965
(key) => {
@@ -70,12 +76,12 @@ function includesMedia (payload) {
7076
)
7177
}
7278

73-
function buildJSONConfig (payload) {
79+
function buildJSONConfig(payload): Promise<RequestInit> {
7480
return Promise.resolve({
7581
method: 'POST',
7682
compress: true,
7783
headers: { 'content-type': 'application/json', connection: 'keep-alive' },
78-
body: JSON.stringify(payload)
84+
body: JSON.stringify(payload),
7985
})
8086
}
8187

@@ -84,28 +90,34 @@ const FORM_DATA_JSON_FIELDS = [
8490
'reply_markup',
8591
'mask_position',
8692
'shipping_options',
87-
'errors'
93+
'errors',
8894
]
8995

90-
function buildFormDataConfig (payload, agent) {
96+
function buildFormDataConfig(payload, agent): Promise<RequestInit> {
9197
for (const field of FORM_DATA_JSON_FIELDS) {
9298
if (field in payload && typeof payload[field] !== 'string') {
9399
payload[field] = JSON.stringify(payload[field])
94100
}
95101
}
96102
const boundary = crypto.randomBytes(32).toString('hex')
97103
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+
)
99107
return Promise.all(tasks).then(() => {
100108
return {
101109
method: 'POST',
102110
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,
105116
}
106117
})
107118
}
108119

120+
// prettier-ignore
109121
function attachFormValue (form, id, value, agent) {
110122
if (!value) {
111123
return Promise.resolve()
@@ -155,6 +167,7 @@ function attachFormValue (form, id, value, agent) {
155167
return attachFormMedia(form, value, id, agent)
156168
}
157169

170+
// prettier-ignore
158171
function attachFormMedia (form, media, id, agent) {
159172
let fileName = media.filename || `${id}.${DEFAULT_EXTENSIONS[id] || 'dat'}`
160173
if (media.url) {
@@ -180,11 +193,12 @@ function attachFormMedia (form, media, id, agent) {
180193
return Promise.resolve()
181194
}
182195

196+
// prettier-ignore
183197
function isKoaResponse (response) {
184198
return typeof response.set === 'function' && typeof response.header === 'object'
185199
}
186200

187-
function answerToWebhook (response, payload = {}, options) {
201+
function answerToWebhook(response, payload, options: ApiClient.Options) {
188202
if (!includesMedia(payload)) {
189203
if (isKoaResponse(response)) {
190204
response.body = payload
@@ -198,63 +212,84 @@ function answerToWebhook (response, payload = {}, options) {
198212
response.end(JSON.stringify(payload), 'utf-8')
199213
return resolve(WEBHOOK_REPLY_STUB)
200214
}
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+
)
202218
})
203219
}
204220

205-
return buildFormDataConfig(payload, options.agent)
206-
.then(({ headers, body }) => {
221+
return buildFormDataConfig(payload, options.agent).then(
222+
({ headers = {}, body }) => {
207223
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]))
209225
response.body = body
210226
return Promise.resolve(WEBHOOK_REPLY_STUB)
211227
}
212228
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+
)
214232
}
215233
return new Promise((resolve) => {
216234
response.on('finish', () => resolve(WEBHOOK_REPLY_STUB))
235+
// @ts-expect-error
217236
body.pipe(response)
218237
})
219-
})
238+
}
239+
)
220240
}
221241

242+
// eslint-disable-next-line no-redeclare
222243
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+
) {
224252
this.token = token
225253
this.options = {
226254
...DEFAULT_OPTIONS,
227-
...options
255+
...options,
228256
}
229257
if (this.options.apiRoot.startsWith('http://')) {
230-
this.options.agent = null
258+
this.options.agent = undefined
231259
}
232-
this.response = webhookResponse
233260
}
234261

235-
set webhookReply (enable) {
262+
set webhookReply(enable: boolean) {
236263
this.options.webhookReply = enable
237264
}
238265

239-
get webhookReply () {
266+
get webhookReply() {
240267
return this.options.webhookReply
241268
}
242269

243-
callApi (method, data = {}) {
270+
callApi(method: string, data = {}) {
244271
const { token, options, response, responseEnd } = this
245272

246273
const payload = Object.keys(data)
247274
.filter((key) => typeof data[key] !== 'undefined' && data[key] !== null)
248275
.reduce((acc, key) => ({ ...acc, [key]: data[key] }), {})
249276

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+
) {
251283
debug('Call via webhook', method, payload)
252284
this.responseEnd = true
253285
return answerToWebhook(response, { method, ...payload }, options)
254286
}
255287

256288
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+
})
258293
}
259294

260295
debug('HTTP call', method, payload)
@@ -267,14 +302,7 @@ class ApiClient {
267302
config.agent = options.agent
268303
return fetch(apiUrl, config)
269304
})
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())
278306
.then((data) => {
279307
if (!data.ok) {
280308
debug('API call failed', data)
@@ -285,4 +313,4 @@ class ApiClient {
285313
}
286314
}
287315

288-
module.exports = ApiClient
316+
export = ApiClient

0 commit comments

Comments
 (0)