From f20fe2f3e095bb56017a03fb9c740e988d5668c2 Mon Sep 17 00:00:00 2001 From: EdJoPaTo Date: Wed, 20 May 2020 17:40:56 +0200 Subject: [PATCH] feat: allow for more media in body closes #84 --- examples/main-typescript.ts | 82 +++++++++++++++++++++++++++++++------ source/body.ts | 60 +++++++++++++++++++++++---- source/generic-types.ts | 4 ++ source/menu-template.ts | 6 ++- source/send-menu.ts | 62 ++++++++++++++++++++++------ 5 files changed, 179 insertions(+), 35 deletions(-) diff --git a/examples/main-typescript.ts b/examples/main-typescript.ts index cfd66e5..812f3ff 100644 --- a/examples/main-typescript.ts +++ b/examples/main-typescript.ts @@ -101,25 +101,81 @@ menu.submenu('Food menu', 'food', foodMenu, { hide: () => mainMenuToggle }) -let isAndroid = true -const photoMenu = new MenuTemplate(() => ({ - photo: { - filename: 'device.jpg', - url: isAndroid ? 'https://telegram.org/img/SiteAndroid.jpg' : 'https://telegram.org/img/SiteiOs.jpg' +let mediaOption = 'photo1' +const mediaMenu = new MenuTemplate(() => { + if (mediaOption === 'video') { + return { + type: 'video', + media: { + filename: 'android.mp4', + url: 'https://telegram.org/img/t_main_Android_demo.mp4' + }, + text: 'Just a caption for a video' + } } -})) -photoMenu.interact('Just a button', 'a', { + + if (mediaOption === 'animation') { + return { + type: 'animation', + media: { + filename: 'android.mp4', + url: 'https://telegram.org/img/t_main_Android_demo.mp4' + }, + text: 'Just a caption for an animation' + } + } + + if (mediaOption === 'photo2') { + return { + type: 'photo', + media: { + filename: 'android.jpg', + url: 'https://telegram.org/img/SiteAndroid.jpg' + }, + text: 'Just a caption for a *photo*', + parse_mode: 'Markdown' + } + } + + if (mediaOption === 'document') { + return { + type: 'document', + media: { + filename: 'logos.zip', + url: 'https://telegram.org/file/464001088/1/bI7AJLo7oX4.287931.zip/374fe3b0a59dc60005' + }, + text: 'Just a caption for a document', + parse_mode: 'HTML' + } + } + + if (mediaOption === 'just text') { + return { + text: 'Just some text' + } + } + + return { + type: 'photo', + media: { + filename: 'ios.jpg', + url: 'https://telegram.org/img/SiteiOs.jpg' + } + } +}) +mediaMenu.interact('Just a button', 'a', { do: async ctx => ctx.answerCbQuery('Just a callback query answer') }) -photoMenu.select('img', ['iOS', 'Android'], { - isSet: (_ctx, key) => key === 'Android' ? isAndroid : !isAndroid, - set: (_ctx, key) => { - isAndroid = key === 'Android' +mediaMenu.select('type', ['animation', 'document', 'photo1', 'photo2', 'video', 'just text'], { + columns: 2, + isSet: (_, key) => mediaOption === key, + set: (_, key) => { + mediaOption = key } }) -photoMenu.manualRow(createBackMainMenuButtons()) +mediaMenu.manualRow(createBackMainMenuButtons()) -menu.submenu('Photo Menu', 'photo', photoMenu) +menu.submenu('Media Menu', 'media', mediaMenu) const menuMiddleware = new MenuMiddleware('/', menu) diff --git a/source/body.ts b/source/body.ts index 5cb4af0..b02704a 100644 --- a/source/body.ts +++ b/source/body.ts @@ -1,22 +1,66 @@ import {InputFile, ParseMode} from 'telegraf/typings/telegram-types' -import {isObject} from './generic-types' +import {hasTruthyKey, isObject} from './generic-types' -export type Body = string | TextBody | PhotoBody +export type Body = string | TextBody | MediaBody + +export type MediaType = 'animation' | 'audio' | 'document' | 'photo' | 'video' +export const MEDIA_TYPES: readonly MediaType[] = ['animation', 'audio', 'document', 'photo', 'video'] export interface TextBody { readonly text: string; readonly parse_mode?: ParseMode; } -export interface PhotoBody { - readonly photo: InputFile; - readonly text?: string; - readonly parse_mode?: ParseMode; +export interface MediaBody extends Partial { + readonly type: MediaType; + readonly media: InputFile; +} + +export function jsUserBodyHints(body: Body): void { + if (typeof body === 'string') { + return + } + + if (!isObject(body)) { + throw new TypeError('The body has to be a string or an object. Check the telegraf-inline-menu Documentation.') + } + + if ('media' in body && !('type' in body)) { + throw new TypeError('When you have a MediaBody you need to specify its type like \'photo\' or \'video\'') + } +} + +export function isTextBody(body: Body): body is string | TextBody { + if (typeof body === 'string') { + return true + } + + if (!isObject(body)) { + return false + } + + if (body.type !== undefined && body.type !== 'text') { + return false + } + + return hasTruthyKey(body, 'text') } -export function isPhotoBody(body: Body): body is PhotoBody { - return isObject(body) && 'photo' in body && Boolean(body.photo) +export function isMediaBody(body: Body): body is MediaBody { + if (!isObject(body)) { + return false + } + + if (typeof body.type !== 'string') { + return false + } + + if (!(MEDIA_TYPES as readonly string[]).includes(body.type)) { + return false + } + + return hasTruthyKey(body, 'media') } export function getBodyText(body: TextBody | string): string { diff --git a/source/generic-types.ts b/source/generic-types.ts index 8313d3c..5966fc3 100644 --- a/source/generic-types.ts +++ b/source/generic-types.ts @@ -15,6 +15,10 @@ export function isObject(something: unknown): something is Record { * @param context Context to be supplied to the buttons on on creation */ async renderBody(context: Context, path: string): Promise { - return this._body(context, path) + const body = await this._body(context, path) + jsUserBodyHints(body) + return body } /** diff --git a/source/send-menu.ts b/source/send-menu.ts index 258d885..28949da 100644 --- a/source/send-menu.ts +++ b/source/send-menu.ts @@ -1,17 +1,19 @@ import {Telegram, Context as TelegrafContext} from 'telegraf' import {ExtraPhoto, ExtraReplyMessage, ExtraEditMessage, Message, InputMediaPhoto} from 'telegraf/typings/telegram-types' -import {Body, TextBody, PhotoBody, isPhotoBody, getBodyText} from './body' +import {Body, TextBody, MediaBody, isMediaBody, getBodyText, jsUserBodyHints} from './body' import {MenuLike, InlineKeyboard} from './menu-like' export async function replyMenuToContext(menu: MenuLike, context: Context, path: string, extra: Readonly = {}): Promise { const body = await menu.renderBody(context, path) + jsUserBodyHints(body) const keyboard = await menu.renderKeyboard(context, path) return replyRenderedMenuPartsToContext(body, keyboard, context, extra) } export async function editMenuOnContext(menu: MenuLike, context: Context, path: string, extra: Readonly = {}): Promise { const body = await menu.renderBody(context, path) + jsUserBodyHints(body) const keyboard = await menu.renderKeyboard(context, path) const message = context.callbackQuery?.message @@ -20,22 +22,22 @@ export async function editMenuOnContext(menu: M return } - if (isPhotoBody(body)) { - if (message.photo) { + if (isMediaBody(body)) { + if ('animation' in message || 'audio' in message || 'document' in message || 'photo' in message || 'video' in message) { const media: InputMediaPhoto = { - type: 'photo', - media: body.photo, + type: body.type, + media: body.media, caption: body.text } await Promise.all([ - context.editMessageMedia(media, createPhotoExtra(body, keyboard, extra as any)) + context.editMessageMedia(media, createMediaExtra(body, keyboard, extra as any)) .catch(catchMessageNotModified), context.answerCbQuery() ]) return } - } else if (getBodyText(body)) { + } else { const text = getBodyText(body) if (message.text) { await Promise.all([ @@ -65,8 +67,27 @@ function catchMessageNotModified(error: any): void { } async function replyRenderedMenuPartsToContext(body: Body, keyboard: InlineKeyboard, context: Context, extra: Readonly): Promise { - if (isPhotoBody(body)) { - return context.replyWithPhoto(body.photo, createPhotoExtra(body, keyboard, extra as any)) + jsUserBodyHints(body) + + if (isMediaBody(body)) { + const mediaExtra = createMediaExtra(body, keyboard, extra as any) + + switch (body.type) { + case 'animation': + // TODO: use typings when PR is merged https://github.com/telegraf/telegraf/pull/1042 + return (context as any).replyWithAnimation(body.media, mediaExtra) + case 'audio': + return context.replyWithAudio(body.media, mediaExtra) + case 'document': + return context.replyWithDocument(body.media, mediaExtra) + case 'photo': + return context.replyWithPhoto(body.media, mediaExtra) + case 'video': + return context.replyWithVideo(body.media, mediaExtra) + + default: + throw new Error('The media body could not be replied. Either you specified the type wrong or the type is not implemented.') + } } const text = getBodyText(body) @@ -80,10 +101,27 @@ async function replyRenderedMenuPartsToContext( export function generateSendMenuToChatFunction(menu: MenuLike, path: string): (telegram: Readonly, chatId: string | number, context: Context, extra?: Readonly) => Promise { return async (telegram, chatId, context, extra = {}) => { const body = await menu.renderBody(context, path) + jsUserBodyHints(body) const keyboard = await menu.renderKeyboard(context, path) - if (isPhotoBody(body)) { - return telegram.sendPhoto(chatId, body.photo, createPhotoExtra(body, keyboard, extra as any)) + if (isMediaBody(body)) { + const mediaExtra = createMediaExtra(body, keyboard, extra as any) + + switch (body.type) { + case 'animation': + return telegram.sendAnimation(chatId, body.media, mediaExtra) + case 'audio': + return telegram.sendAudio(chatId, body.media, mediaExtra) + case 'document': + return telegram.sendDocument(chatId, body.media, mediaExtra) + case 'photo': + return telegram.sendPhoto(chatId, body.media, mediaExtra) + case 'video': + return telegram.sendVideo(chatId, body.media, mediaExtra) + + default: + throw new Error('The media body could not be sent. Either you specified the type wrong or the type is not implemented.') + } } const text = getBodyText(body) @@ -105,7 +143,7 @@ function createTextExtra(body: string | TextBody, keyboard: InlineKeyboard, base } } -function createPhotoExtra(body: PhotoBody, keyboard: InlineKeyboard, base: Readonly): ExtraPhoto { +function createMediaExtra(body: MediaBody, keyboard: InlineKeyboard, base: Readonly): ExtraPhoto { return { ...base, parse_mode: body.parse_mode,