commit
This commit is contained in:
parent
be4fd23bcf
commit
0bd53741af
728 changed files with 86573 additions and 0 deletions
71
node_modules/telegraf/src/core/helpers/check.ts
generated
vendored
Normal file
71
node_modules/telegraf/src/core/helpers/check.ts
generated
vendored
Normal file
|
@ -0,0 +1,71 @@
|
|||
interface Mapping {
|
||||
string: string
|
||||
number: number
|
||||
bigint: bigint
|
||||
boolean: boolean
|
||||
symbol: symbol
|
||||
undefined: undefined
|
||||
object: Record<string, unknown>
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
function: (...props: any[]) => any
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a given object has a property with a given name.
|
||||
*
|
||||
* Example invocation:
|
||||
* ```js
|
||||
* let obj = { 'foo': 'bar', 'baz': () => {} }
|
||||
* hasProp(obj, 'foo') // true
|
||||
* hasProp(obj, 'baz') // true
|
||||
* hasProp(obj, 'abc') // false
|
||||
* ```
|
||||
*
|
||||
* @param obj An object to test
|
||||
* @param prop The name of the property
|
||||
*/
|
||||
export function hasProp<O extends object, K extends PropertyKey>(
|
||||
obj: O | undefined,
|
||||
prop: K
|
||||
): obj is O & Record<K, unknown> {
|
||||
return obj !== undefined && prop in obj
|
||||
}
|
||||
/**
|
||||
* Checks if a given object has a property with a given name.
|
||||
* Furthermore performs a `typeof` check on the property if it exists.
|
||||
*
|
||||
* Example invocation:
|
||||
* ```js
|
||||
* let obj = { 'foo': 'bar', 'baz': () => {} }
|
||||
* hasPropType(obj, 'foo', 'string') // true
|
||||
* hasPropType(obj, 'baz', 'function') // true
|
||||
* hasPropType(obj, 'abc', 'number') // false
|
||||
* ```
|
||||
*
|
||||
* @param obj An object to test
|
||||
* @param prop The name of the property
|
||||
* @param type The type the property is expected to have
|
||||
*/
|
||||
export function hasPropType<
|
||||
O extends object,
|
||||
K extends PropertyKey,
|
||||
T extends keyof Mapping,
|
||||
V extends Mapping[T]
|
||||
>(obj: O | undefined, prop: K, type: T): obj is O & Record<K, V> {
|
||||
return hasProp(obj, prop) && type === typeof obj[prop]
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the supplied array has two dimensions or not.
|
||||
*
|
||||
* Example invocations:
|
||||
* is2D([]) // false
|
||||
* is2D([[]]) // true
|
||||
* is2D([[], []]) // true
|
||||
* is2D([42]) // false
|
||||
*
|
||||
* @param arr an array with one or two dimensions
|
||||
*/
|
||||
export function is2D<E>(arr: E[] | E[][]): arr is E[][] {
|
||||
return Array.isArray(arr[0])
|
||||
}
|
12
node_modules/telegraf/src/core/helpers/compact.ts
generated
vendored
Normal file
12
node_modules/telegraf/src/core/helpers/compact.ts
generated
vendored
Normal file
|
@ -0,0 +1,12 @@
|
|||
export function compactOptions<T extends { [key: string]: unknown }>(
|
||||
options?: T
|
||||
): T | undefined {
|
||||
if (!options) {
|
||||
return options
|
||||
}
|
||||
|
||||
const keys = Object.keys(options) as Array<keyof T>
|
||||
const compactKeys = keys.filter((key) => options[key] !== undefined)
|
||||
const compactEntries = compactKeys.map((key) => [key, options[key]])
|
||||
return Object.fromEntries(compactEntries)
|
||||
}
|
74
node_modules/telegraf/src/core/helpers/formatting.ts
generated
vendored
Normal file
74
node_modules/telegraf/src/core/helpers/formatting.ts
generated
vendored
Normal file
|
@ -0,0 +1,74 @@
|
|||
import { MessageEntity, User } from 'typegram'
|
||||
|
||||
export interface FmtString {
|
||||
text: string
|
||||
entities?: MessageEntity[]
|
||||
parse_mode?: undefined
|
||||
}
|
||||
|
||||
export class FmtString implements FmtString {
|
||||
constructor(public text: string, entities?: MessageEntity[]) {
|
||||
if (entities) {
|
||||
this.entities = entities
|
||||
// force parse_mode to undefined if entities are present
|
||||
this.parse_mode = undefined
|
||||
}
|
||||
}
|
||||
static normalise(content: string | FmtString) {
|
||||
if (typeof content === 'string') return new FmtString(content)
|
||||
return content
|
||||
}
|
||||
}
|
||||
|
||||
export namespace Types {
|
||||
// prettier-ignore
|
||||
export type Containers = 'bold' | 'italic' | 'spoiler' | 'strikethrough' | 'underline'
|
||||
export type NonContainers = 'code' | 'pre'
|
||||
export type Text = Containers | NonContainers
|
||||
}
|
||||
|
||||
type TemplateParts = string | TemplateStringsArray | string[]
|
||||
|
||||
export function _fmt(
|
||||
kind: Types.Containers | 'very-plain'
|
||||
): (parts: TemplateParts, ...items: (string | FmtString)[]) => FmtString
|
||||
export function _fmt(
|
||||
kind: Types.NonContainers
|
||||
): (parts: TemplateParts, ...items: string[]) => FmtString
|
||||
export function _fmt(
|
||||
kind: 'pre',
|
||||
opts: { language: string }
|
||||
): (parts: TemplateParts, ...items: string[]) => FmtString
|
||||
export function _fmt(kind: Types.Text | 'very-plain', opts?: object) {
|
||||
return function fmt(parts: TemplateParts, ...items: (string | FmtString)[]) {
|
||||
let text = ''
|
||||
const entities: MessageEntity[] = []
|
||||
parts = typeof parts === 'string' ? [parts] : parts
|
||||
for (let i = 0; i < parts.length; i++) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
text += parts[i]!
|
||||
const item = items[i]
|
||||
if (!item) continue
|
||||
if (typeof item === 'string') {
|
||||
text += item
|
||||
continue
|
||||
}
|
||||
for (const child of item.entities || [])
|
||||
entities.push({ ...child, offset: text.length + child.offset })
|
||||
text += item.text
|
||||
}
|
||||
if (kind !== 'very-plain')
|
||||
entities.unshift({ type: kind, offset: 0, length: text.length, ...opts })
|
||||
return new FmtString(text, entities.length ? entities : undefined)
|
||||
}
|
||||
}
|
||||
export const linkOrMention = (
|
||||
content: string | FmtString,
|
||||
data:
|
||||
| { type: 'text_link'; url: string }
|
||||
| { type: 'text_mention'; user: User }
|
||||
) => {
|
||||
const { text, entities = [] } = FmtString.normalise(content)
|
||||
entities.unshift(Object.assign(data, { offset: 0, length: text.length }))
|
||||
return new FmtString(text, entities)
|
||||
}
|
380
node_modules/telegraf/src/core/network/client.ts
generated
vendored
Normal file
380
node_modules/telegraf/src/core/network/client.ts
generated
vendored
Normal file
|
@ -0,0 +1,380 @@
|
|||
/* eslint @typescript-eslint/restrict-template-expressions: [ "error", { "allowNumber": true, "allowBoolean": true } ] */
|
||||
import * as crypto from 'crypto'
|
||||
import * as fs from 'fs'
|
||||
import * as http from 'http'
|
||||
import * as https from 'https'
|
||||
import * as path from 'path'
|
||||
import fetch, { RequestInfo, RequestInit } from 'node-fetch'
|
||||
import { hasProp, hasPropType } from '../helpers/check'
|
||||
import { Opts, Telegram } from '../types/typegram'
|
||||
import { AbortSignal } from 'abort-controller'
|
||||
import { compactOptions } from '../helpers/compact'
|
||||
import MultipartStream from './multipart-stream'
|
||||
import { ReadStream } from 'fs'
|
||||
import TelegramError from './error'
|
||||
import { URL } from 'url'
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const debug = require('debug')('telegraf:client')
|
||||
const { isStream } = MultipartStream
|
||||
|
||||
const WEBHOOK_REPLY_METHOD_ALLOWLIST = new Set<keyof Telegram>([
|
||||
'answerCallbackQuery',
|
||||
'answerInlineQuery',
|
||||
'deleteMessage',
|
||||
'leaveChat',
|
||||
'sendChatAction',
|
||||
])
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-namespace
|
||||
namespace ApiClient {
|
||||
export type Agent = http.Agent | ((parsedUrl: URL) => http.Agent) | undefined
|
||||
export interface Options {
|
||||
/**
|
||||
* Agent for communicating with the bot API.
|
||||
*/
|
||||
agent?: http.Agent
|
||||
/**
|
||||
* Agent for attaching files via URL.
|
||||
* 1. Not all agents support both `http:` and `https:`.
|
||||
* 2. When passing a function, create the agents once, outside of the function.
|
||||
* Creating new agent every request probably breaks `keepAlive`.
|
||||
*/
|
||||
attachmentAgent?: Agent
|
||||
apiRoot: string
|
||||
/**
|
||||
* @default 'bot'
|
||||
* @see https://github.com/tdlight-team/tdlight-telegram-bot-api#user-mode
|
||||
*/
|
||||
apiMode: 'bot' | 'user'
|
||||
webhookReply: boolean
|
||||
testEnv: boolean
|
||||
}
|
||||
|
||||
export interface CallApiOptions {
|
||||
signal?: AbortSignal
|
||||
}
|
||||
}
|
||||
|
||||
const DEFAULT_EXTENSIONS: Record<string, string | undefined> = {
|
||||
audio: 'mp3',
|
||||
photo: 'jpg',
|
||||
sticker: 'webp',
|
||||
video: 'mp4',
|
||||
animation: 'mp4',
|
||||
video_note: 'mp4',
|
||||
voice: 'ogg',
|
||||
}
|
||||
|
||||
const DEFAULT_OPTIONS: ApiClient.Options = {
|
||||
apiRoot: 'https://api.telegram.org',
|
||||
apiMode: 'bot',
|
||||
webhookReply: true,
|
||||
agent: new https.Agent({
|
||||
keepAlive: true,
|
||||
keepAliveMsecs: 10000,
|
||||
}),
|
||||
attachmentAgent: undefined,
|
||||
testEnv: false,
|
||||
}
|
||||
|
||||
function includesMedia(payload: Record<string, unknown>) {
|
||||
return Object.values(payload).some((value) => {
|
||||
if (Array.isArray(value)) {
|
||||
return value.some(
|
||||
({ media }) =>
|
||||
media && typeof media === 'object' && (media.source || media.url)
|
||||
)
|
||||
}
|
||||
return (
|
||||
value &&
|
||||
typeof value === 'object' &&
|
||||
((hasProp(value, 'source') && value.source) ||
|
||||
(hasProp(value, 'url') && value.url) ||
|
||||
(hasPropType(value, 'media', 'object') &&
|
||||
((hasProp(value.media, 'source') && value.media.source) ||
|
||||
(hasProp(value.media, 'url') && value.media.url))))
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
function replacer(_: unknown, value: unknown) {
|
||||
if (value == null) return undefined
|
||||
return value
|
||||
}
|
||||
|
||||
function buildJSONConfig(payload: unknown): Promise<RequestInit> {
|
||||
return Promise.resolve({
|
||||
method: 'POST',
|
||||
compress: true,
|
||||
headers: { 'content-type': 'application/json', connection: 'keep-alive' },
|
||||
body: JSON.stringify(payload, replacer),
|
||||
})
|
||||
}
|
||||
|
||||
const FORM_DATA_JSON_FIELDS = [
|
||||
'results',
|
||||
'reply_markup',
|
||||
'mask_position',
|
||||
'shipping_options',
|
||||
'errors',
|
||||
]
|
||||
|
||||
async function buildFormDataConfig(
|
||||
payload: Record<string, unknown>,
|
||||
agent: ApiClient.Agent
|
||||
) {
|
||||
for (const field of FORM_DATA_JSON_FIELDS) {
|
||||
if (hasProp(payload, field) && typeof payload[field] !== 'string') {
|
||||
payload[field] = JSON.stringify(payload[field])
|
||||
}
|
||||
}
|
||||
const boundary = crypto.randomBytes(32).toString('hex')
|
||||
const formData = new MultipartStream(boundary)
|
||||
const tasks = Object.keys(payload).map((key) =>
|
||||
attachFormValue(formData, key, payload[key], agent)
|
||||
)
|
||||
await Promise.all(tasks)
|
||||
return {
|
||||
method: 'POST',
|
||||
compress: true,
|
||||
headers: {
|
||||
'content-type': `multipart/form-data; boundary=${boundary}`,
|
||||
connection: 'keep-alive',
|
||||
},
|
||||
body: formData,
|
||||
}
|
||||
}
|
||||
|
||||
async function attachFormValue(
|
||||
form: MultipartStream,
|
||||
id: string,
|
||||
value: unknown,
|
||||
agent: ApiClient.Agent
|
||||
) {
|
||||
if (value == null) {
|
||||
return
|
||||
}
|
||||
if (
|
||||
typeof value === 'string' ||
|
||||
typeof value === 'boolean' ||
|
||||
typeof value === 'number'
|
||||
) {
|
||||
form.addPart({
|
||||
headers: { 'content-disposition': `form-data; name="${id}"` },
|
||||
body: `${value}`,
|
||||
})
|
||||
return
|
||||
}
|
||||
if (id === 'thumb') {
|
||||
const attachmentId = crypto.randomBytes(16).toString('hex')
|
||||
await attachFormMedia(form, value as FormMedia, attachmentId, agent)
|
||||
return form.addPart({
|
||||
headers: { 'content-disposition': `form-data; name="${id}"` },
|
||||
body: `attach://${attachmentId}`,
|
||||
})
|
||||
}
|
||||
if (Array.isArray(value)) {
|
||||
const items = await Promise.all(
|
||||
value.map(async (item) => {
|
||||
if (typeof item.media !== 'object') {
|
||||
return await Promise.resolve(item)
|
||||
}
|
||||
const attachmentId = crypto.randomBytes(16).toString('hex')
|
||||
await attachFormMedia(form, item.media, attachmentId, agent)
|
||||
return { ...item, media: `attach://${attachmentId}` }
|
||||
})
|
||||
)
|
||||
return form.addPart({
|
||||
headers: { 'content-disposition': `form-data; name="${id}"` },
|
||||
body: JSON.stringify(items),
|
||||
})
|
||||
}
|
||||
if (
|
||||
value &&
|
||||
typeof value === 'object' &&
|
||||
hasProp(value, 'media') &&
|
||||
hasProp(value, 'type') &&
|
||||
typeof value.media !== 'undefined' &&
|
||||
typeof value.type !== 'undefined'
|
||||
) {
|
||||
const attachmentId = crypto.randomBytes(16).toString('hex')
|
||||
await attachFormMedia(form, value.media as FormMedia, attachmentId, agent)
|
||||
return form.addPart({
|
||||
headers: { 'content-disposition': `form-data; name="${id}"` },
|
||||
body: JSON.stringify({
|
||||
...value,
|
||||
media: `attach://${attachmentId}`,
|
||||
}),
|
||||
})
|
||||
}
|
||||
return await attachFormMedia(form, value as FormMedia, id, agent)
|
||||
}
|
||||
|
||||
interface FormMedia {
|
||||
filename?: string
|
||||
url?: RequestInfo
|
||||
source?: string
|
||||
}
|
||||
async function attachFormMedia(
|
||||
form: MultipartStream,
|
||||
media: FormMedia,
|
||||
id: string,
|
||||
agent: ApiClient.Agent
|
||||
) {
|
||||
let fileName = media.filename ?? `${id}.${DEFAULT_EXTENSIONS[id] ?? 'dat'}`
|
||||
if (media.url !== undefined) {
|
||||
const res = await fetch(media.url, { agent })
|
||||
return form.addPart({
|
||||
headers: {
|
||||
'content-disposition': `form-data; name="${id}"; filename="${fileName}"`,
|
||||
},
|
||||
body: res.body,
|
||||
})
|
||||
}
|
||||
if (media.source) {
|
||||
let mediaSource: string | ReadStream = media.source
|
||||
if (fs.existsSync(media.source)) {
|
||||
fileName = media.filename ?? path.basename(media.source)
|
||||
mediaSource = fs.createReadStream(media.source)
|
||||
}
|
||||
if (isStream(mediaSource) || Buffer.isBuffer(mediaSource)) {
|
||||
form.addPart({
|
||||
headers: {
|
||||
'content-disposition': `form-data; name="${id}"; filename="${fileName}"`,
|
||||
},
|
||||
body: mediaSource,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function answerToWebhook(
|
||||
response: Response,
|
||||
payload: Record<string, unknown>,
|
||||
options: ApiClient.Options
|
||||
): Promise<true> {
|
||||
if (!includesMedia(payload)) {
|
||||
if (!response.headersSent) {
|
||||
response.setHeader('content-type', 'application/json')
|
||||
}
|
||||
response.end(JSON.stringify(payload), 'utf-8')
|
||||
return true
|
||||
}
|
||||
|
||||
const { headers, body } = await buildFormDataConfig(
|
||||
payload,
|
||||
options.attachmentAgent
|
||||
)
|
||||
if (!response.headersSent) {
|
||||
for (const [key, value] of Object.entries(headers)) {
|
||||
response.setHeader(key, value)
|
||||
}
|
||||
}
|
||||
await new Promise((resolve) => {
|
||||
response.on('finish', resolve)
|
||||
body.pipe(response)
|
||||
})
|
||||
return true
|
||||
}
|
||||
|
||||
function redactToken(error: Error): never {
|
||||
error.message = error.message.replace(
|
||||
/\/(bot|user)(\d+):[^/]+\//,
|
||||
'/$1$2:[REDACTED]/'
|
||||
)
|
||||
throw error
|
||||
}
|
||||
|
||||
type Response = http.ServerResponse
|
||||
class ApiClient {
|
||||
readonly options: ApiClient.Options
|
||||
|
||||
constructor(
|
||||
readonly token: string,
|
||||
options?: Partial<ApiClient.Options>,
|
||||
private readonly response?: Response
|
||||
) {
|
||||
this.options = {
|
||||
...DEFAULT_OPTIONS,
|
||||
...compactOptions(options),
|
||||
}
|
||||
if (this.options.apiRoot.startsWith('http://')) {
|
||||
this.options.agent = undefined
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* If set to `true`, first _eligible_ call will avoid performing a POST request.
|
||||
* Note that such a call:
|
||||
* 1. cannot report errors or return meaningful values,
|
||||
* 2. resolves before bot API has a chance to process it,
|
||||
* 3. prematurely confirms the update as processed.
|
||||
*
|
||||
* https://core.telegram.org/bots/faq#how-can-i-make-requests-in-response-to-updates
|
||||
* https://github.com/telegraf/telegraf/pull/1250
|
||||
*/
|
||||
set webhookReply(enable: boolean) {
|
||||
this.options.webhookReply = enable
|
||||
}
|
||||
|
||||
get webhookReply() {
|
||||
return this.options.webhookReply
|
||||
}
|
||||
|
||||
async callApi<M extends keyof Telegram>(
|
||||
method: M,
|
||||
payload: Opts<M>,
|
||||
{ signal }: ApiClient.CallApiOptions = {}
|
||||
): Promise<ReturnType<Telegram[M]>> {
|
||||
const { token, options, response } = this
|
||||
|
||||
if (
|
||||
options.webhookReply &&
|
||||
response?.writableEnded === false &&
|
||||
WEBHOOK_REPLY_METHOD_ALLOWLIST.has(method)
|
||||
) {
|
||||
debug('Call via webhook', method, payload)
|
||||
// @ts-expect-error using webhookReply is an optimisation that doesn't respond with normal result
|
||||
// up to the user to deal with this
|
||||
return await answerToWebhook(response, { method, ...payload }, options)
|
||||
}
|
||||
|
||||
if (!token) {
|
||||
throw new TelegramError({
|
||||
error_code: 401,
|
||||
description: 'Bot Token is required',
|
||||
})
|
||||
}
|
||||
|
||||
debug('HTTP call', method, payload)
|
||||
|
||||
const config: RequestInit = includesMedia(payload)
|
||||
? await buildFormDataConfig(
|
||||
{ method, ...payload },
|
||||
options.attachmentAgent
|
||||
)
|
||||
: await buildJSONConfig(payload)
|
||||
const apiUrl = new URL(
|
||||
`./${options.apiMode}${token}${options.testEnv ? '/test' : ''}/${method}`,
|
||||
options.apiRoot
|
||||
)
|
||||
config.agent = options.agent
|
||||
config.signal = signal
|
||||
const res = await fetch(apiUrl, config).catch(redactToken)
|
||||
if (res.status >= 500) {
|
||||
const errorPayload = {
|
||||
error_code: res.status,
|
||||
description: res.statusText,
|
||||
}
|
||||
throw new TelegramError(errorPayload, { method, payload })
|
||||
}
|
||||
const data = await res.json()
|
||||
if (!data.ok) {
|
||||
debug('API call failed', data)
|
||||
throw new TelegramError(data, { method, payload })
|
||||
}
|
||||
return data.result
|
||||
}
|
||||
}
|
||||
|
||||
export default ApiClient
|
26
node_modules/telegraf/src/core/network/error.ts
generated
vendored
Normal file
26
node_modules/telegraf/src/core/network/error.ts
generated
vendored
Normal file
|
@ -0,0 +1,26 @@
|
|||
import { ResponseParameters } from '../types/typegram'
|
||||
|
||||
interface ErrorPayload {
|
||||
error_code: number
|
||||
description: string
|
||||
parameters?: ResponseParameters
|
||||
}
|
||||
export class TelegramError extends Error {
|
||||
constructor(readonly response: ErrorPayload, readonly on = {}) {
|
||||
super(`${response.error_code}: ${response.description}`)
|
||||
}
|
||||
|
||||
get code() {
|
||||
return this.response.error_code
|
||||
}
|
||||
|
||||
get description() {
|
||||
return this.response.description
|
||||
}
|
||||
|
||||
get parameters() {
|
||||
return this.response.parameters
|
||||
}
|
||||
}
|
||||
|
||||
export default TelegramError
|
45
node_modules/telegraf/src/core/network/multipart-stream.ts
generated
vendored
Normal file
45
node_modules/telegraf/src/core/network/multipart-stream.ts
generated
vendored
Normal file
|
@ -0,0 +1,45 @@
|
|||
import * as stream from 'stream'
|
||||
import { hasPropType } from '../helpers/check'
|
||||
import SandwichStream from 'sandwich-stream'
|
||||
const CRNL = '\r\n'
|
||||
|
||||
interface Part {
|
||||
headers: { [key: string]: string }
|
||||
body: NodeJS.ReadStream | NodeJS.ReadableStream | string
|
||||
}
|
||||
|
||||
class MultipartStream extends SandwichStream {
|
||||
constructor(boundary: string) {
|
||||
super({
|
||||
head: `--${boundary}${CRNL}`,
|
||||
tail: `${CRNL}--${boundary}--`,
|
||||
separator: `${CRNL}--${boundary}${CRNL}`,
|
||||
})
|
||||
}
|
||||
|
||||
addPart(part: Part) {
|
||||
const partStream = new stream.PassThrough()
|
||||
for (const [key, header] of Object.entries(part.headers)) {
|
||||
partStream.write(`${key}:${header}${CRNL}`)
|
||||
}
|
||||
partStream.write(CRNL)
|
||||
if (MultipartStream.isStream(part.body)) {
|
||||
part.body.pipe(partStream)
|
||||
} else {
|
||||
partStream.end(part.body)
|
||||
}
|
||||
this.add(partStream)
|
||||
}
|
||||
|
||||
static isStream(
|
||||
stream: unknown
|
||||
): stream is { pipe: MultipartStream['pipe'] } {
|
||||
return (
|
||||
typeof stream === 'object' &&
|
||||
stream !== null &&
|
||||
hasPropType(stream, 'pipe', 'function')
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default MultipartStream
|
96
node_modules/telegraf/src/core/network/polling.ts
generated
vendored
Normal file
96
node_modules/telegraf/src/core/network/polling.ts
generated
vendored
Normal file
|
@ -0,0 +1,96 @@
|
|||
import * as tg from '../types/typegram'
|
||||
import * as tt from '../../telegram-types'
|
||||
import AbortController from 'abort-controller'
|
||||
import ApiClient from './client'
|
||||
import d from 'debug'
|
||||
import { promisify } from 'util'
|
||||
import { TelegramError } from './error'
|
||||
const debug = d('telegraf:polling')
|
||||
const wait = promisify(setTimeout)
|
||||
function always<T>(x: T) {
|
||||
return () => x
|
||||
}
|
||||
const noop = always(Promise.resolve())
|
||||
|
||||
export class Polling {
|
||||
private readonly abortController = new AbortController()
|
||||
private skipOffsetSync = false
|
||||
private offset = 0
|
||||
constructor(
|
||||
private readonly telegram: ApiClient,
|
||||
private readonly allowedUpdates: readonly tt.UpdateType[]
|
||||
) {}
|
||||
|
||||
private async *[Symbol.asyncIterator]() {
|
||||
debug('Starting long polling')
|
||||
do {
|
||||
try {
|
||||
const updates = await this.telegram.callApi(
|
||||
'getUpdates',
|
||||
{
|
||||
timeout: 50,
|
||||
offset: this.offset,
|
||||
allowed_updates: this.allowedUpdates,
|
||||
},
|
||||
this.abortController
|
||||
)
|
||||
const last = updates[updates.length - 1]
|
||||
if (last !== undefined) {
|
||||
this.offset = last.update_id + 1
|
||||
}
|
||||
yield updates
|
||||
} catch (error) {
|
||||
const err = error as Error & {
|
||||
parameters?: { retry_after: number }
|
||||
}
|
||||
|
||||
if (err.name === 'AbortError') return
|
||||
if (
|
||||
err.name === 'FetchError' ||
|
||||
(err instanceof TelegramError && err.code === 429) ||
|
||||
(err instanceof TelegramError && err.code >= 500)
|
||||
) {
|
||||
const retryAfter: number = err.parameters?.retry_after ?? 5
|
||||
debug('Failed to fetch updates, retrying after %ds.', retryAfter, err)
|
||||
await wait(retryAfter * 1000)
|
||||
continue
|
||||
}
|
||||
if (
|
||||
err instanceof TelegramError &&
|
||||
// Unauthorized Conflict
|
||||
(err.code === 401 || err.code === 409)
|
||||
) {
|
||||
this.skipOffsetSync = true
|
||||
throw err
|
||||
}
|
||||
throw err
|
||||
}
|
||||
} while (!this.abortController.signal.aborted)
|
||||
}
|
||||
|
||||
private async syncUpdateOffset() {
|
||||
if (this.skipOffsetSync) return
|
||||
debug('Syncing update offset...')
|
||||
await this.telegram.callApi('getUpdates', { offset: this.offset, limit: 1 })
|
||||
}
|
||||
|
||||
async loop(handleUpdate: (updates: tg.Update) => Promise<void>) {
|
||||
if (this.abortController.signal.aborted) {
|
||||
throw new Error('Polling instances must not be reused!')
|
||||
}
|
||||
try {
|
||||
for await (const updates of this) {
|
||||
await Promise.all(updates.map(handleUpdate))
|
||||
}
|
||||
} finally {
|
||||
debug('Long polling stopped')
|
||||
// prevent instance reuse
|
||||
this.stop()
|
||||
await this.syncUpdateOffset().catch(noop)
|
||||
}
|
||||
}
|
||||
|
||||
stop() {
|
||||
this.abortController.abort()
|
||||
}
|
||||
}
|
58
node_modules/telegraf/src/core/network/webhook.ts
generated
vendored
Normal file
58
node_modules/telegraf/src/core/network/webhook.ts
generated
vendored
Normal file
|
@ -0,0 +1,58 @@
|
|||
import * as http from 'http'
|
||||
import d from 'debug'
|
||||
import { type Update } from '../types/typegram'
|
||||
const debug = d('telegraf:webhook')
|
||||
|
||||
export default function generateWebhook(
|
||||
filter: (req: http.IncomingMessage) => boolean,
|
||||
updateHandler: (update: Update, res: http.ServerResponse) => Promise<void>
|
||||
) {
|
||||
return async (
|
||||
req: http.IncomingMessage & { body?: Update },
|
||||
res: http.ServerResponse,
|
||||
next = (): void => {
|
||||
res.statusCode = 403
|
||||
debug('Replying with status code', res.statusCode)
|
||||
res.end()
|
||||
}
|
||||
): Promise<void> => {
|
||||
debug('Incoming request', req.method, req.url)
|
||||
|
||||
if (!filter(req)) {
|
||||
debug('Webhook filter failed', req.method, req.url)
|
||||
return next()
|
||||
}
|
||||
|
||||
let update: Update
|
||||
|
||||
try {
|
||||
if (req.body != null) {
|
||||
/* If req.body is already set, we expect it to be the parsed
|
||||
request body (update object) received from Telegram
|
||||
However, some libraries such as `serverless-http` set req.body to the
|
||||
raw buffer, so we'll handle that additionally */
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
let body: any = req.body
|
||||
// if body is Buffer, parse it into string
|
||||
if (body instanceof Buffer) body = String(req.body)
|
||||
// if body is string, parse it into object
|
||||
if (typeof body === 'string') body = JSON.parse(body)
|
||||
update = body
|
||||
} else {
|
||||
let body = ''
|
||||
// parse each buffer to string and append to body
|
||||
for await (const chunk of req) body += String(chunk)
|
||||
// parse body to object
|
||||
update = JSON.parse(body)
|
||||
}
|
||||
} catch (error: unknown) {
|
||||
// if any of the parsing steps fails, give up and respond with error
|
||||
res.writeHead(415).end()
|
||||
debug('Failed to parse request body:', error)
|
||||
return
|
||||
}
|
||||
|
||||
return await updateHandler(update, res)
|
||||
}
|
||||
}
|
55
node_modules/telegraf/src/core/types/typegram.ts
generated
vendored
Normal file
55
node_modules/telegraf/src/core/types/typegram.ts
generated
vendored
Normal file
|
@ -0,0 +1,55 @@
|
|||
import { Typegram } from 'typegram'
|
||||
|
||||
// internal type provisions
|
||||
export * from 'typegram/api'
|
||||
export * from 'typegram/markup'
|
||||
export * from 'typegram/menu-button'
|
||||
export * from 'typegram/inline'
|
||||
export * from 'typegram/manage'
|
||||
export * from 'typegram/message'
|
||||
export * from 'typegram/passport'
|
||||
export * from 'typegram/payment'
|
||||
export * from 'typegram/update'
|
||||
|
||||
// telegraf input file definition
|
||||
interface InputFileByPath {
|
||||
source: string
|
||||
filename?: string
|
||||
}
|
||||
interface InputFileByReadableStream {
|
||||
source: NodeJS.ReadableStream
|
||||
filename?: string
|
||||
}
|
||||
interface InputFileByBuffer {
|
||||
source: Buffer
|
||||
filename?: string
|
||||
}
|
||||
interface InputFileByURL {
|
||||
url: string
|
||||
filename?: string
|
||||
}
|
||||
export type InputFile =
|
||||
| InputFileByPath
|
||||
| InputFileByReadableStream
|
||||
| InputFileByBuffer
|
||||
| InputFileByURL
|
||||
|
||||
// typegram proxy type setup
|
||||
type TelegrafTypegram = Typegram<InputFile>
|
||||
|
||||
export type Telegram = TelegrafTypegram['Telegram']
|
||||
export type Opts<M extends keyof Telegram> = TelegrafTypegram['Opts'][M]
|
||||
export type InputMedia = TelegrafTypegram['InputMedia']
|
||||
export type InputMediaPhoto = TelegrafTypegram['InputMediaPhoto']
|
||||
export type InputMediaVideo = TelegrafTypegram['InputMediaVideo']
|
||||
export type InputMediaAnimation = TelegrafTypegram['InputMediaAnimation']
|
||||
export type InputMediaAudio = TelegrafTypegram['InputMediaAudio']
|
||||
export type InputMediaDocument = TelegrafTypegram['InputMediaDocument']
|
||||
|
||||
// tiny helper types
|
||||
export type ChatAction = Opts<'sendChatAction'>['action']
|
||||
|
||||
/**
|
||||
* Sending video notes by a URL is currently unsupported
|
||||
*/
|
||||
export type InputFileVideoNote = Exclude<InputFile, InputFileByURL>
|
Loading…
Add table
Add a link
Reference in a new issue