Skip to content

Commit

Permalink
feat: include @telegraf/client
Browse files Browse the repository at this point in the history
  • Loading branch information
wojpawlik committed Sep 27, 2022
1 parent b125676 commit 868b7e9
Show file tree
Hide file tree
Showing 5 changed files with 149 additions and 50 deletions.
1 change: 0 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,6 @@
},
"types": "./typings/index.d.ts",
"dependencies": {
"@telegraf/client": "^0.7.1",
"debug": "^4.3.3",
"mri": "^1.2.0",
"p-timeout": "^4.1.0",
Expand Down
134 changes: 134 additions & 0 deletions src/core/network/client.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
// for https://gist.github.com/2b1b226d52d675ec246c6f8abdab81ef
export type { Update, UserFromGetMe } from 'typegram'
import type { ApiResponse, File, Typegram } from 'typegram'
import createDebug from 'debug'

const debug = createDebug('telegraf:client')

export const defaultOptions = {
api: {
mode: 'bot' as "bot" | "user",
root: new URL('https://api.telegram.org'),
},
}

export type ClientOptions = typeof defaultOptions
export type TelegrafTypegram = Typegram<InputFile>
export type InputFile = Blob | StreamFile
export type TelegramP = TelegrafTypegram['TelegramP']
export type Opts = TelegrafTypegram['Opts']

type Telegram = TelegrafTypegram['Telegram']

export type Ret = {
[M in keyof Opts]: ReturnType<Telegram[M]>
}

export class StreamFile {
readonly size = NaN
constructor(
readonly stream: () =>
| AsyncIterable<Uint8Array>
| ReadableStream<Uint8Array>,
readonly name: string
) {}
}

Object.defineProperty(StreamFile.prototype, Symbol.toStringTag, {
value: 'File',
})

// based on https://github.com/node-fetch/fetch-blob/blob/8ab587d34080de94140b54f07168451e7d0b655e/index.js#L229-L241 (MIT License)
export function isInputFile(value: any): value is InputFile {
return (
value !== null &&
typeof value === 'object' &&
typeof value.size === 'number' &&
typeof value.stream === 'function' &&
typeof value.constructor === 'function' &&
/^(?:Blob|File)$/.test(value[Symbol.toStringTag])
)
}

function stringify(value: unknown) {
if (typeof value === 'string') return value
if (isInputFile(value)) return value
return JSON.stringify(value)
}

function serialize(payload: Record<string, any>) {
const formData = new FormData()
const attach = (entry: any, index: number) => {
const result = { ...entry }
if (isInputFile(entry.media)) {
const id = entry.type + index
result.media = `attach://${id}`
formData.append(id, entry.media)
}
if (isInputFile(entry.thumb)) {
const id = 'thumb' + index
result.thumb = `attach://${id}`
formData.append(id, entry.thumb)
}
return result
}

// eslint-disable-next-line prefer-const
for (let [key, value] of Object.entries(payload)) {
if (key === 'media') {
value = value.map(attach)
}
if (value != null) formData.append(key, stringify(value) as any)
}
return formData
}

function redactToken(error: Error): never {
error.message = error.message.replace(
/\/(bot|user)(\d+):[^/]+\//,
'/$1$2:[REDACTED]/'
)
throw error
}

export interface Invocation<M extends keyof Opts> {
method: M
payload: Opts[M]
signal?: AbortSignal
}

export function createClient(token: string, { api } = defaultOptions) {
const call = async <M extends keyof Telegram>({
method,
payload,
signal,
}: Invocation<M>): Promise<ApiResponse<Ret[M]>> => {
debug('HTTP call', method, payload)
const body = serialize(payload)
const url = new URL(`./${api.mode}${token}/${method}`, api.root)
const init: RequestInit = { body, signal, method: 'post' }
const res = await fetch(url.href, init).catch(redactToken)
if (res.status >= 500) {
return {
ok: false,
error_code: res.status,
description: res.statusText,
}
}
return (await res.json()) as ApiResponse<Ret[M]>
}

const download = async (file: File) => {
const url = new URL(
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
`./file/${api.mode}${token}/${file.file_path!}`,
api.root
)
return await fetch(url)
}

return { call, download }
}

export type Client = ReturnType<typeof createClient>
9 changes: 4 additions & 5 deletions src/telegraf.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,10 @@ import Telegram from './telegram'
import { TlsOptions } from 'tls'
import { URL } from 'url'
import safeCompare = require('safe-compare')
import { Client, type ClientOptions } from '@telegraf/client'
import { createClient, type ClientOptions } from './core/network/client'
const debug = d('telegraf:main')

const DEFAULT_OPTIONS: Telegraf.Options<Context> = {
api: {},
handlerTimeout: 90_000, // 90s in ms
contextType: Context,
}
Expand All @@ -36,7 +35,7 @@ export namespace Telegraf {
...args: ConstructorParameters<typeof Context>
) => TContext
handlerTimeout: number
api?: Partial<ClientOptions>
client?: ClientOptions
}

export interface LaunchOptions {
Expand Down Expand Up @@ -140,7 +139,7 @@ export class Telegraf<C extends Context = Context> extends Composer<C> {
...compactOptions(options),
}
this.#token = token
this.telegram = new Telegram(new Client(token, this.options.api))
this.telegram = new Telegram(createClient(token, this.options.client))
debug('Created a `Telegraf` instance')
}

Expand Down Expand Up @@ -297,7 +296,7 @@ export class Telegraf<C extends Context = Context> extends Composer<C> {
await (this.botInfoCall ??= this.telegram.getMe()))
debug('Processing update', update.update_id)
// webhookResponse // TODO(mkr): remove webhookResponse entirely or re-introduce?
const tg = this.telegram.clone()
const tg = new Telegram(this.telegram)
const TelegrafContext = this.options.contextType
const ctx = new TelegrafContext(update, tg, this.botInfo)
Object.assign(ctx, this.context)
Expand Down
7 changes: 6 additions & 1 deletion src/telegram-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,12 @@

import { Message, Update } from 'typegram'
import { UnionKeys } from './deunionize'
import { TelegrafTypegram, TelegramP, InputFile, Opts } from '@telegraf/client'
import {
TelegrafTypegram,
TelegramP,
InputFile,
Opts,
} from './core/network/client'

export { Markup } from './markup'

Expand Down
48 changes: 5 additions & 43 deletions src/telegram.ts
Original file line number Diff line number Diff line change
@@ -1,33 +1,25 @@
import * as tg from 'typegram'
import * as tt from './telegram-types'
import { TT } from './telegram-types'
import { Client, type Opts } from '@telegraf/client'
import { isAbsolute } from 'path'
import { URL } from 'url'
import type { Client, Opts } from './core/network/client'
import { TelegramError } from './index'

export type EndoFunction<T> = (t: T) => T
export type Transformer = EndoFunction<Client['call']>

export class Telegram {
#client: Client
private call: Client['call']
call: Client['call']
readonly download: Client['download']

constructor(client: Client) {
this.#client = client
this.call = this.#client.call.bind(this.#client)
this.call = client.call
this.download = client.download
}

use(transform: Transformer) {
this.call = transform(this.call)
}

clone() {
const telegram = new Telegram(this.#client)
telegram.call = this.call
return telegram
}

async callApi<M extends keyof Opts>(
method: M,
payload: Opts[M],
Expand All @@ -53,36 +45,6 @@ export class Telegram {
return this.callApi('getFile', { file_id: fileId })
}

/**
* Get download link to a file.
*/
async getFileLink(fileId: string | tg.File) {
if (typeof fileId === 'string') {
fileId = await this.getFile(fileId)
} else if (fileId.file_path === undefined) {
fileId = await this.getFile(fileId.file_id)
}

const root = this.#client.options.api.root.toString()

// Local bot API instances return the absolute path to the file
if (fileId.file_path !== undefined && isAbsolute(fileId.file_path)) {
const url = new URL(root)
url.port = ''
url.pathname = fileId.file_path
url.protocol = 'file:'
return url
}

return new URL(
`./file/${this.#client.options.api.mode}${
this.#client.token
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
}/${fileId.file_path!}`,
root
)
}

/**
* Directly request incoming updates.
* You should probably use `Telegraf::launch` instead.
Expand Down

0 comments on commit 868b7e9

Please sign in to comment.