Skip to content

Commit

Permalink
feat(telegram): support media group, improve message parsing (#261)
Browse files Browse the repository at this point in the history
  • Loading branch information
std-microblock authored Apr 14, 2024
1 parent 2e12d9e commit 46cc98d
Show file tree
Hide file tree
Showing 4 changed files with 242 additions and 95 deletions.
4 changes: 3 additions & 1 deletion adapters/telegram/src/bot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ export class TelegramBot<C extends Context = Context, T extends TelegramBot.Conf
local?: boolean
server?: string

telegram: TelegramClient

Check failure on line 37 in adapters/telegram/src/bot.ts

View workflow job for this annotation

GitHub Actions / build

Cannot find name 'TelegramClient'.

constructor(ctx: C, config: T) {
super(ctx, config, 'telegram')
this.selfId = config.token.split(':')[0]
Expand All @@ -59,7 +61,7 @@ export class TelegramBot<C extends Context = Context, T extends TelegramBot.Conf
ctx.get('server').get(route + '/:file+', async ctx => {
const { data, mime } = await this.$getFile(ctx.params.file)
ctx.set('content-type', mime)
ctx.body = data
ctx.body = Buffer.from(data)
})
}
}
Expand Down
202 changes: 142 additions & 60 deletions adapters/telegram/src/message.ts
Original file line number Diff line number Diff line change
@@ -1,32 +1,13 @@
import { Context, Dict, h, MessageEncoder } from '@satorijs/satori'
import { Context, Dict, Element, file, h, MessageEncoder } from '@satorijs/satori'

Check failure on line 1 in adapters/telegram/src/message.ts

View workflow job for this annotation

GitHub Actions / lint

'file' is defined but never used
import { TelegramBot } from './bot'
import * as Telegram from './utils'

type RenderMode = 'default' | 'figure'

type AssetMethod = 'sendPhoto' | 'sendAudio' | 'sendDocument' | 'sendVideo' | 'sendAnimation' | 'sendVoice'

async function appendAsset(bot: TelegramBot, form: FormData, element: h): Promise<AssetMethod> {
let method: AssetMethod
const { filename, data, mime } = await bot.ctx.http.file(element.attrs.src || element.attrs.url, element.attrs)
if (element.type === 'img' || element.type === 'image') {
method = mime === 'image/gif' ? 'sendAnimation' : 'sendPhoto'
} else if (element.type === 'file') {
method = 'sendDocument'
} else if (element.type === 'video') {
method = 'sendVideo'
} else if (element.type === 'audio') {
method = element.attrs.type === 'voice' ? 'sendVoice' : 'sendAudio'
}
const value = new Blob([data], { type: mime })
form.append(method.slice(4).toLowerCase(), value, filename)
return method
}

const supportedElements = ['b', 'strong', 'i', 'em', 'u', 'ins', 's', 'del', 'a']

export class TelegramMessageEncoder<C extends Context = Context> extends MessageEncoder<C, TelegramBot<C>> {
private asset: h = null
private asset: h[] = []
private payload: Dict
private mode: RenderMode = 'default'
private rows: Telegram.InlineKeyboardButton[][] = []
Expand All @@ -42,47 +23,151 @@ export class TelegramMessageEncoder<C extends Context = Context> extends Message
async addResult(result: Telegram.Message) {
const session = this.bot.session()
await Telegram.decodeMessage(this.bot, result, session.event.message = {}, session.event)
session.event._data ??= {}
session.event._data.message = result
this.results.push(session.event.message)
session.app.emit(session, 'send', session)
}

async sendAsset() {
const form = new FormData()
for (const key in this.payload) {
form.append(key, this.payload[key].toString())
}
form.append('reply_markup', JSON.stringify({
inline_keyboard: this.rows,
}))
const method = await appendAsset(this.bot, form, this.asset)
const result = await this.bot.internal[method](form as any)
await this.addResult(result)
delete this.payload.reply_to_message_id
this.asset = null
this.payload.caption = ''
}

async flush() {
if (this.asset) {
// send previous asset if there is any
await this.sendAsset()
} else if (this.payload.caption) {
if (this.payload.caption || this.asset.length > 0) {
this.trimButtons()
const result = await this.bot.internal.sendMessage({
chat_id: this.payload.chat_id,
text: this.payload.caption,
parse_mode: this.payload.parse_mode,
reply_to_message_id: this.payload.reply_to_message_id,
message_thread_id: this.payload.message_thread_id,
disable_web_page_preview: !this.options.linkPreview,
reply_markup: {
inline_keyboard: this.rows,
},
})
await this.addResult(result)
delete this.payload.reply_to_message_id
this.payload.caption = ''
this.rows = []

if (this.asset.length > 0) {
const files: {
filename: string
data: ArrayBuffer
mime: string
type: string
element: Element
}[] = []

const typeMap = {
img: 'photo',
image: 'photo',
audio: 'audio',
video: 'video',
file: 'document',
}

let i = 0;

Check failure on line 53 in adapters/telegram/src/message.ts

View workflow job for this annotation

GitHub Actions / lint

Extra semicolon
for (const element of this.asset) {
const { filename, data, mime } = await this.bot.ctx.http.file(element.attrs.src || element.attrs.url, element.attrs)
files.push({
filename: (i++) + filename,
data,
mime,
type: filename.endsWith('gif') ? 'animation' : typeMap[element.type] ?? element.type,
element

Check failure on line 61 in adapters/telegram/src/message.ts

View workflow job for this annotation

GitHub Actions / lint

Missing trailing comma
})
}

// Array of InputMediaAudio, InputMediaDocument, InputMediaPhoto and InputMediaVideo
const inputFiles: Telegram.InputFile[] = []

for (const { filename, data, mime, type, element } of files) {

Check failure on line 68 in adapters/telegram/src/message.ts

View workflow job for this annotation

GitHub Actions / lint

'data' is assigned a value but never used

Check failure on line 68 in adapters/telegram/src/message.ts

View workflow job for this annotation

GitHub Actions / lint

'mime' is assigned a value but never used
const media = 'attach://' + filename
inputFiles.push({
media, type,

Check failure on line 71 in adapters/telegram/src/message.ts

View workflow job for this annotation

GitHub Actions / lint

Object properties must go on a new line if they aren't all on the same line
has_spoiler: element.attrs.spoiler

Check failure on line 72 in adapters/telegram/src/message.ts

View workflow job for this annotation

GitHub Actions / lint

Missing trailing comma
})
}

if (files.length > 1) {
inputFiles[0].caption = this.payload.caption
inputFiles[0].parse_mode = this.payload.parse_mode

const form = new FormData()

const data = {
chat_id: this.payload.chat_id,
reply_to_message_id: this.payload.reply_to_message_id,
message_thread_id: this.payload.message_thread_id,
media: JSON.stringify(inputFiles)

Check failure on line 86 in adapters/telegram/src/message.ts

View workflow job for this annotation

GitHub Actions / lint

Missing trailing comma
}
for (const key in data) {
form.append(key, data[key])
}

for (const { filename, data, mime } of files) {
form.append(filename, new Blob([data], { type: mime }), filename)
}

// @ts-ignore
const result = await this.bot.internal.sendMediaGroup(form)

for (const x of result)
await this.addResult(x)

Check failure on line 100 in adapters/telegram/src/message.ts

View workflow job for this annotation

GitHub Actions / lint

Expected { after 'for-of'

if (this.rows.length > 0 && this.rows[0].length > 0) {
const result2 = await this.bot.internal.sendMessage({
chat_id: this.payload.chat_id,
text: this.payload.caption,
parse_mode: this.payload.parse_mode,
reply_to_message_id: result[0].message_id,
message_thread_id: this.payload.message_thread_id,
disable_web_page_preview: !this.options.linkPreview,
reply_markup: {
inline_keyboard: this.rows,
},
})

await this.addResult(result2)
delete this.payload.reply_to_message_id
this.payload.caption = ''
this.rows = []
}

delete this.payload.reply_to_message_id
this.payload.caption = ''
this.rows = []
} else {
const sendMap = [
['audio', ['sendAudio', 'audio']],
['voice', ['sendAudio', 'audio']],
['video', ['sendVideo', 'video']],
['animation', ['sendAnimation', 'animation']],
['image', ['sendPhoto', 'photo']],
['photo', ['sendPhoto', 'photo']],
['document', ['sendDocument', 'document']],
['', ['sendDocument', 'document']],
] as const
const [_, [method, dataKey]] = sendMap.find(([key]) => files[0].type.startsWith(key)) || []

Check failure on line 135 in adapters/telegram/src/message.ts

View workflow job for this annotation

GitHub Actions / lint

'_' is assigned a value but never used

const formData = new FormData()
formData.append('chat_id', this.payload.chat_id)
formData.append('caption', this.payload.caption)
formData.append('parse_mode', this.payload.parse_mode)
formData.append('reply_to_message_id', this.payload.reply_to_message_id)
formData.append('message_thread_id', this.payload.message_thread_id)
formData.append('has_spoiler', files[0].element.attrs.spoiler ? 'true' : 'false')
formData.append(dataKey, 'attach://' + files[0].filename)
formData.append(files[0].filename, new Blob([files[0].data], { type: files[0].mime }), files[0].filename)

// @ts-ignore
const result = await this.bot.internal[method](formData)
await this.addResult(result)
this.payload.caption = ''
this.rows = []
delete this.payload.reply_to_message_id
}
} else {
const result = await this.bot.internal.sendMessage({
chat_id: this.payload.chat_id,
text: this.payload.caption,
parse_mode: this.payload.parse_mode,
reply_to_message_id: this.payload.reply_to_message_id,
message_thread_id: this.payload.message_thread_id,
disable_web_page_preview: !this.options.linkPreview,
reply_markup: {
inline_keyboard: this.rows,
},
})
await this.addResult(result)
delete this.payload.reply_to_message_id
this.payload.caption = ''
this.rows = []
}
}
}

Expand Down Expand Up @@ -143,10 +228,7 @@ export class TelegramMessageEncoder<C extends Context = Context> extends Message
this.payload.caption += `<a href="tg://user?id=${attrs.id}">@${attrs.name || attrs.id}</a>`
}
} else if (['img', 'image', 'audio', 'video', 'file'].includes(type)) {
if (this.mode === 'default') {
await this.flush()
}
this.asset = element
this.asset.push(element)
} else if (type === 'figure') {
await this.flush()
this.mode = 'figure'
Expand Down
Loading

0 comments on commit 46cc98d

Please sign in to comment.