telegraf
Version:
📡 Modern Telegram bot framework
200 lines (184 loc) • 5.87 kB
JavaScript
const debug = require('debug')('telegraf:api')
const crypto = require('crypto')
const fetch = require('node-fetch')
const fs = require('fs')
const https = require('https')
const path = require('path')
const TelegramError = require('./error')
const MultipartStream = require('./multipart-stream')
const { isStream } = MultipartStream
const WebhookBlacklist = [
'getChat',
'getChatAdministrators',
'getChatMember',
'getChatMembersCount',
'getFile',
'getFileLink',
'getGameHighScores',
'getMe',
'getUserProfilePhotos',
'getWebhookInfo'
]
const DefaultExtensions = {
audio: 'mp3',
photo: 'jpg',
sticker: 'webp',
video: 'mp4',
video_note: 'mp4',
voice: 'ogg'
}
const DefaultOptions = {
apiRoot: 'https://api.telegram.org',
webhookReply: true,
agent: new https.Agent({
keepAlive: true,
keepAliveMsecs: 10000
})
}
function safeJSONParse (text) {
try {
return JSON.parse(text)
} catch (err) {
debug('JSON parse failed', err)
}
}
class ApiClient {
constructor (token, options, webhookResponse) {
this.token = token
this.options = Object.assign({}, DefaultOptions, options)
this.response = webhookResponse
}
set webhookReply (enable) {
this.options.webhookReply = enable
}
get webhookReply () {
return this.options.webhookReply
}
callApi (method, extra = {}) {
const isMultipartRequest = Object.keys(extra)
.filter((x) => extra[x])
.some((x) => Array.isArray(extra[x])
? extra[x].some((child) => typeof child.media === 'object' && (child.media.source || child.media.url))
: extra[x].source || extra[x].url
)
if (this.options.webhookReply && !isMultipartRequest && this.response && !this.response.finished && !WebhookBlacklist.includes(method)) {
debug('â–· webhook', method)
extra.method = method
if (typeof this.response.end === 'function') {
if (!this.response.headersSent) {
this.response.setHeader('connection', 'keep-alive')
this.response.setHeader('content-type', 'application/json')
}
this.response.end(JSON.stringify(extra))
} else if (typeof this.response.header === 'object') {
this.response.header['connection'] = 'keep-alive'
this.response.header['content-type'] = 'application/json'
this.response.body = extra
}
return Promise.resolve({webhook: true})
}
if (!this.token) {
throw new TelegramError({
error_code: 401,
description: 'Bot Token is required'
})
}
debug('▶︎ http', method)
const buildPayload = isMultipartRequest ? this.buildFormDataPayload(extra) : this.buildJSONPayload(extra)
return buildPayload
.then((payload) => {
payload.agent = this.options.agent
return fetch(`${this.options.apiRoot}/bot${this.token}/${method}`, payload)
})
.then((res) => res.text())
.then((text) => {
return safeJSONParse(text) || {
error_code: 500,
description: 'Unsupported message from Telegram',
response: text
}
})
.then((data) => {
if (!data.ok) {
throw new TelegramError(data)
}
return data.result
})
}
buildJSONPayload (options) {
return Promise.resolve({
method: 'POST',
headers: { 'content-type': 'application/json', 'connection': 'keep-alive' },
body: JSON.stringify(options)
})
}
buildFormDataPayload (options) {
if (options.reply_markup && typeof options.reply_markup !== 'string') {
options.reply_markup = JSON.stringify(options.reply_markup)
}
const boundary = crypto.randomBytes(30).toString('hex')
const formData = new MultipartStream(boundary)
const tasks = Object.keys(options).map((key) => attachFormValue(formData, options[key], key))
return Promise.all(tasks).then(() => {
return {
method: 'POST',
headers: { 'content-type': `multipart/form-data; boundary=${boundary}`, 'connection': 'keep-alive' },
body: formData
}
})
}
}
function attachFormValue (form, value, id) {
if (!value) {
return Promise.resolve()
}
const valueType = typeof value
if (valueType === 'string' || valueType === 'boolean' || valueType === 'number') {
form.addPart({
headers: { 'content-disposition': `form-data; name="${id}"` },
body: `${value}`
})
return Promise.resolve()
}
if (Array.isArray(value)) {
return Promise.all(
value.map((item) => {
if (typeof item.media === 'object') {
const attachmentId = crypto.randomBytes(30).toString('hex')
return attachFormMedia(form, item.media, attachmentId)
.then(() => Object.assign({}, item, { media: `attach://${attachmentId}` }))
}
return Promise.resolve(item)
})
).then((items) => form.addPart({
headers: { 'content-disposition': `form-data; name="${id}"` },
body: JSON.stringify(items)
}))
}
return attachFormMedia(form, value, id)
}
function attachFormMedia (form, media, id) {
let fileName = media.filename || `${id}.${DefaultExtensions[id] || 'dat'}`
if (media.url) {
return fetch(media.url).then((res) =>
form.addPart({
headers: { 'content-disposition': `form-data; name="${id}";filename="${fileName}"` },
body: res.body
})
)
}
if (media.source) {
if (fs.existsSync(media.source)) {
fileName = media.filename || path.basename(media.source)
media.source = fs.createReadStream(media.source)
}
if (isStream(media.source) || Buffer.isBuffer(media.source)) {
form.addPart({
headers: { 'content-disposition': `form-data; name="${id}";filename="${fileName}"` },
body: media.source
})
}
}
return Promise.resolve()
}
module.exports = ApiClient