Skip to content

Commit

Permalink
fix(telegram): adjust adapter schema
Browse files Browse the repository at this point in the history
  • Loading branch information
shigma committed Feb 1, 2022
1 parent d5c2ff3 commit 34edd3a
Show file tree
Hide file tree
Showing 4 changed files with 68 additions and 64 deletions.
47 changes: 30 additions & 17 deletions plugins/adapter/telegram/src/bot.ts
Original file line number Diff line number Diff line change
@@ -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<any>, url: string, retcode: number, selfId: string) {
super(`Error when trying to send to ${url}, args: ${JSON.stringify(args)}, retcode: ${retcode}`)
Expand All @@ -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<BotConfig> = Schema.object({
token: Schema.string().description('机器人的用户令牌。').role('secret').required(),
pollingTimeout: Schema.union([
Schema.number(),
Schema.const<true>(true),
]).description('通过长轮询获取更新时请求的超时。单位为秒;true 为使用默认值 1 分钟。详情请见 Telegram API 中 getUpdate 的参数 timeout。不设置即使用 webhook 获取更新。'),
request: Quester.createSchema({
endpoint: 'https://api.telegram.org',
}),
})

export class TelegramBot extends Bot<BotConfig> {
Expand All @@ -49,20 +50,17 @@ export class TelegramBot extends Bot<BotConfig> {

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}`,
})
}

Expand Down Expand Up @@ -191,4 +189,19 @@ export class TelegramBot extends Bot<BotConfig> {
const data = await this.get<Telegram.User>('/getMe')
return TelegramBot.adaptUser(data)
}

async $getFileData(fileId: string) {
try {
const file = await this.get<Telegram.File>('/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 }
}
}
66 changes: 30 additions & 36 deletions plugins/adapter/telegram/src/http.ts
Original file line number Diff line number Diff line change
@@ -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'
Expand All @@ -15,17 +14,10 @@ type GetUpdatesOptions = {
}

abstract class TelegramAdapter extends Adapter<BotConfig, AdapterConfig> {
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<void>

Expand All @@ -43,8 +35,7 @@ abstract class TelegramAdapter extends Adapter<BotConfig, AdapterConfig> {

async onUpdate(update: Telegram.Update, bot: TelegramBot) {
logger.debug('receive %s', JSON.stringify(update))
const { selfId, token } = bot.config
const session: Partial<Session> = { selfId }
const session: Partial<Session> = { selfId: bot.selfId }

function parseText(text: string, entities: Telegram.MessageEntity[]): segment[] {
let curr = 0
Expand Down Expand Up @@ -94,23 +85,9 @@ abstract class TelegramAdapter extends Adapter<BotConfig, AdapterConfig> {
data: { lat: message.location.latitude, lon: message.location.longitude },
})
}
const getFileData = async (fileId) => {
try {
const file = await bot.get<Telegram.File>('/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
Expand All @@ -121,15 +98,20 @@ abstract class TelegramAdapter extends Adapter<BotConfig, AdapterConfig> {
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 || []))
Expand Down Expand Up @@ -159,6 +141,8 @@ abstract class TelegramAdapter extends Adapter<BotConfig, AdapterConfig> {
}

export class HttpServer extends TelegramAdapter {
static schema = BotConfig

constructor(ctx: Context, config: AdapterConfig) {
super(ctx, config)
config.path = sanitize(config.path || '/telegram')
Expand Down Expand Up @@ -199,19 +183,29 @@ export class HttpServer extends TelegramAdapter {
}

export class HttpPolling extends TelegramAdapter {
private offset: Record<string, number> = {}
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<number> = {}
private isStopped: boolean

start(): void {
start() {
this.isStopped = false
}

stop(): void {
stop() {
this.isStopped = true
}

async listenUpdates(bot: TelegramBot): Promise<void> {
const { selfId } = bot.config
const { selfId } = bot
this.offset[selfId] = this.offset[selfId] || 0

const { url } = await bot.get<Telegram.WebhookInfo, GetUpdatesOptions>('/getWebhookInfo', {})
Expand All @@ -230,7 +224,7 @@ export class HttpPolling extends TelegramAdapter {
const polling = async () => {
const updates = await bot.get<Telegram.Update[], GetUpdatesOptions>('/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)
Expand Down
15 changes: 6 additions & 9 deletions plugins/adapter/telegram/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
})
4 changes: 2 additions & 2 deletions plugins/adapter/telegram/src/utils.ts
Original file line number Diff line number Diff line change
@@ -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
}
Expand Down

0 comments on commit 34edd3a

Please sign in to comment.