diff --git a/plugins/adapter/telegram/src/bot.ts b/plugins/adapter/telegram/src/bot.ts index 3cf2c9d10f..af13199c27 100644 --- a/plugins/adapter/telegram/src/bot.ts +++ b/plugins/adapter/telegram/src/bot.ts @@ -1,10 +1,12 @@ import FormData from 'form-data' -import { Adapter, assertProperty, Bot, camelCase, Dict, Quester, renameProperty, Schema, snakeCase } from 'koishi' +import { Adapter, assertProperty, Bot, camelCase, Dict, Logger, Quester, renameProperty, Schema, snakeCase } from 'koishi' import * as Telegram from './types' import { AdapterConfig } from './utils' import { AxiosError } from 'axios' import { Sender } from './sender' +const logger = new Logger('telegram') + export class SenderError extends Error { constructor(args: Dict, url: string, retcode: number, selfId: string) { super(`Error when trying to send to ${url}, args: ${JSON.stringify(args)}, retcode: ${retcode}`) @@ -24,17 +26,16 @@ export interface TelegramResponse { } export interface BotConfig extends Bot.BaseConfig { - selfId?: string token?: string - pollingTimeout?: number | true + request?: Quester.Config + pollingTimeout?: number } export const BotConfig: Schema = Schema.object({ token: Schema.string().description('机器人的用户令牌。').role('secret').required(), - pollingTimeout: Schema.union([ - Schema.number(), - Schema.const(true), - ]).description('通过长轮询获取更新时请求的超时。单位为秒;true 为使用默认值 1 分钟。详情请见 Telegram API 中 getUpdate 的参数 timeout。不设置即使用 webhook 获取更新。'), + request: Quester.createSchema({ + endpoint: 'https://api.telegram.org', + }), }) export class TelegramBot extends Bot { @@ -49,20 +50,17 @@ export class TelegramBot extends Bot { static schema = AdapterConfig - http: Quester + http: Quester & { file?: Quester } constructor(adapter: Adapter, config: BotConfig) { assertProperty(config, 'token') - if (!config.selfId) { - if (config.token.includes(':')) { - config.selfId = config.token.split(':')[0] - } else { - assertProperty(config, 'selfId') - } - } super(adapter, config) - this.http = adapter.http.extend({ - endpoint: `${adapter.http.config.endpoint}/bot${config.token}`, + this.selfId = config.token.split(':')[0] + this.http = this.app.http.extend({ + endpoint: `${config.request.endpoint}/bot${config.token}`, + }) + this.http.file = this.app.http.extend({ + endpoint: `${config.request.endpoint}/file/bot${config.token}`, }) } @@ -191,4 +189,19 @@ export class TelegramBot extends Bot { const data = await this.get('/getMe') return TelegramBot.adaptUser(data) } + + async $getFileData(fileId: string) { + try { + const file = await this.get('/getFile', { fileId }) + return await this.$getFileContent(file.filePath) + } catch (e) { + logger.warn('get file error', e) + } + } + + async $getFileContent(filePath: string) { + const res = await this.http.file.get(`/${filePath}`, { responseType: 'arraybuffer' }) + const base64 = `base64://` + res.toString('base64') + return { url: base64 } + } } diff --git a/plugins/adapter/telegram/src/http.ts b/plugins/adapter/telegram/src/http.ts index 2ea47780a2..5d126d6818 100644 --- a/plugins/adapter/telegram/src/http.ts +++ b/plugins/adapter/telegram/src/http.ts @@ -1,5 +1,4 @@ -import FormData from 'form-data' -import { Adapter, assertProperty, camelCase, Context, Logger, sanitize, segment, Session, trimSlash } from 'koishi' +import { Adapter, assertProperty, camelCase, Context, Dict, Logger, sanitize, Schema, segment, Session, Time, trimSlash } from 'koishi' import { BotConfig, TelegramBot } from './bot' import * as Telegram from './types' import { AdapterConfig } from './utils' @@ -15,17 +14,10 @@ type GetUpdatesOptions = { } abstract class TelegramAdapter extends Adapter { - static schema = BotConfig - constructor(ctx: Context, config: AdapterConfig) { super(ctx, config) - this.config.request = this.config.request || {} - this.config.request.endpoint = this.config.request.endpoint || 'https://api.telegram.org' - this.http = ctx.http.extend(config.request) } - abstract start(): void - abstract stop(): void /** Init telegram updates listening */ abstract listenUpdates(bot: TelegramBot): Promise @@ -43,8 +35,7 @@ abstract class TelegramAdapter extends Adapter { async onUpdate(update: Telegram.Update, bot: TelegramBot) { logger.debug('receive %s', JSON.stringify(update)) - const { selfId, token } = bot.config - const session: Partial = { selfId } + const session: Partial = { selfId: bot.selfId } function parseText(text: string, entities: Telegram.MessageEntity[]): segment[] { let curr = 0 @@ -94,23 +85,9 @@ abstract class TelegramAdapter extends Adapter { data: { lat: message.location.latitude, lon: message.location.longitude }, }) } - const getFileData = async (fileId) => { - try { - const file = await bot.get('/getFile', { fileId }) - return await getFileContent(file.filePath) - } catch (e) { - logger.warn('get file error', e) - } - } - const getFileContent = async (filePath) => { - const downloadUrl = `${this.config.request.endpoint}/file/bot${token}/${filePath}` - const res = await this.ctx.http.get(downloadUrl, { responseType: 'arraybuffer' }) - const base64 = `base64://` + res.toString('base64') - return { url: base64 } - } if (message.photo) { const photo = message.photo.sort((s1, s2) => s2.fileSize - s1.fileSize)[0] - segments.push({ type: 'image', data: await getFileData(photo.fileId) }) + segments.push({ type: 'image', data: await bot.$getFileData(photo.fileId) }) } if (message.sticker) { // TODO: Convert tgs to gif @@ -121,15 +98,20 @@ abstract class TelegramAdapter extends Adapter { if (file.filePath.endsWith('.tgs')) { throw 'tgs is not supported now' } - segments.push({ type: 'image', data: await getFileContent(file.filePath) }) + segments.push({ type: 'image', data: await bot.$getFileContent(file.filePath) }) } catch (e) { logger.warn('get file error', e) segments.push({ type: 'text', data: { content: `[${message.sticker.setName || 'sticker'} ${message.sticker.emoji || ''}]` } }) } - } else if (message.animation) segments.push({ type: 'image', data: await getFileData(message.animation.fileId) }) - else if (message.voice) segments.push({ type: 'audio', data: await getFileData(message.voice.fileId) }) - else if (message.video) segments.push({ type: 'video', data: await getFileData(message.video.fileId) }) - else if (message.document) segments.push({ type: 'file', data: await getFileData(message.document.fileId) }) + } else if (message.animation) { + segments.push({ type: 'image', data: await bot.$getFileData(message.animation.fileId) }) + } else if (message.voice) { + segments.push({ type: 'audio', data: await bot.$getFileData(message.voice.fileId) }) + } else if (message.video) { + segments.push({ type: 'video', data: await bot.$getFileData(message.video.fileId) }) + } else if (message.document) { + segments.push({ type: 'file', data: await bot.$getFileData(message.document.fileId) }) + } const msgText: string = message.text || message.caption segments.push(...parseText(msgText, message.entities || [])) @@ -159,6 +141,8 @@ abstract class TelegramAdapter extends Adapter { } export class HttpServer extends TelegramAdapter { + static schema = BotConfig + constructor(ctx: Context, config: AdapterConfig) { super(ctx, config) config.path = sanitize(config.path || '/telegram') @@ -199,19 +183,29 @@ export class HttpServer extends TelegramAdapter { } export class HttpPolling extends TelegramAdapter { - private offset: Record = {} + static schema = Schema.intersect([ + BotConfig, + Schema.object({ + pollingTimeout: Schema.union([ + Schema.number(), + Schema.transform(Schema.const(true as const), () => Time.minute), + ]).description('通过长轮询获取更新时请求的超时 (单位为秒)。'), + }), + ]) + + private offset: Dict = {} private isStopped: boolean - start(): void { + start() { this.isStopped = false } - stop(): void { + stop() { this.isStopped = true } async listenUpdates(bot: TelegramBot): Promise { - const { selfId } = bot.config + const { selfId } = bot this.offset[selfId] = this.offset[selfId] || 0 const { url } = await bot.get('/getWebhookInfo', {}) @@ -230,7 +224,7 @@ export class HttpPolling extends TelegramAdapter { const polling = async () => { const updates = await bot.get('/getUpdates', { offset: this.offset[selfId] + 1, - timeout: bot.config.pollingTimeout === true ? 60 : bot.config.pollingTimeout, + timeout: bot.config.pollingTimeout, }) for (const e of updates) { this.offset[selfId] = Math.max(this.offset[selfId], e.updateId) diff --git a/plugins/adapter/telegram/src/index.ts b/plugins/adapter/telegram/src/index.ts index e9f7952c6d..36a0e2e3b1 100644 --- a/plugins/adapter/telegram/src/index.ts +++ b/plugins/adapter/telegram/src/index.ts @@ -2,18 +2,15 @@ import { Adapter } from 'koishi' import { TelegramBot } from './bot' import { HttpServer, HttpPolling } from './http' -declare module 'koishi' { - interface Modules { - 'adapter-telegram': typeof import('.') - } -} +export * as Telegram from './types' +export * from './bot' +export * from './http' +export * from './sender' +export * from './utils' -export { TelegramBot } from './bot' -export const webhookAdapter = Adapter.define('telegram', TelegramBot, HttpServer) -export const pollingAdapter = Adapter.define('telegram', TelegramBot, HttpPolling) export default Adapter.define('telegram', TelegramBot, { webhook: HttpServer, polling: HttpPolling, }, ({ pollingTimeout }) => { - return pollingTimeout !== undefined ? 'polling' : 'webhook' + return pollingTimeout ? 'polling' : 'webhook' }) diff --git a/plugins/adapter/telegram/src/utils.ts b/plugins/adapter/telegram/src/utils.ts index d06ff679ae..ac196a49fc 100644 --- a/plugins/adapter/telegram/src/utils.ts +++ b/plugins/adapter/telegram/src/utils.ts @@ -1,6 +1,6 @@ -import { App, Schema } from 'koishi' +import { Schema } from 'koishi' -export interface AdapterConfig extends App.Config.Request { +export interface AdapterConfig { path?: string selfUrl?: string }