Skip to content

Commit

Permalink
feat: allow for more media in body
Browse files Browse the repository at this point in the history
closes #84
  • Loading branch information
EdJoPaTo committed May 20, 2020
1 parent 6bfc7f6 commit f20fe2f
Show file tree
Hide file tree
Showing 5 changed files with 179 additions and 35 deletions.
82 changes: 69 additions & 13 deletions examples/main-typescript.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,25 +101,81 @@ menu.submenu('Food menu', 'food', foodMenu, {
hide: () => mainMenuToggle
})

let isAndroid = true
const photoMenu = new MenuTemplate<TelegrafContext>(() => ({
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<TelegrafContext>(() => {
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 <b>document</b>',
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<TelegrafContext>('/', menu)

Expand Down
60 changes: 52 additions & 8 deletions source/body.ts
Original file line number Diff line number Diff line change
@@ -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<TextBody> {
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 {
Expand Down
4 changes: 4 additions & 0 deletions source/generic-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@ export function isObject(something: unknown): something is Record<string, unknow
return typeof something === 'object' && something !== null
}

export function hasTruthyKey(something: unknown, key: string): boolean {
return isObject(something) && key in something && Boolean(something[key])
}

export function isRegExpExecArray(something: unknown): something is RegExpExecArray {
if (!Array.isArray(something)) {
return false
Expand Down
6 changes: 4 additions & 2 deletions source/menu-template.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import {InlineKeyboardButton} from 'telegram-typings'

import {Body} from './body'
import {Body, jsUserBodyHints} from './body'
import {ButtonAction, ActionHive, ActionFunc} from './action-hive'
import {Choices, ChoicesRecord, generateChoicesButtons, combineHideAndChoices} from './choices'
import {ChooseOptions} from './buttons/choose'
Expand Down Expand Up @@ -40,7 +40,9 @@ export class MenuTemplate<Context> {
* @param context Context to be supplied to the buttons on on creation
*/
async renderBody(context: Context, path: string): Promise<Body> {
return this._body(context, path)
const body = await this._body(context, path)
jsUserBodyHints(body)
return body
}

/**
Expand Down
62 changes: 50 additions & 12 deletions source/send-menu.ts
Original file line number Diff line number Diff line change
@@ -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<Context extends TelegrafContext>(menu: MenuLike<Context>, context: Context, path: string, extra: Readonly<ExtraReplyMessage> = {}): Promise<Message> {
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<Context extends TelegrafContext>(menu: MenuLike<Context>, context: Context, path: string, extra: Readonly<ExtraEditMessage> = {}): Promise<void> {
const body = await menu.renderBody(context, path)
jsUserBodyHints(body)
const keyboard = await menu.renderKeyboard(context, path)

const message = context.callbackQuery?.message
Expand All @@ -20,22 +22,22 @@ export async function editMenuOnContext<Context extends TelegrafContext>(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([
Expand Down Expand Up @@ -65,8 +67,27 @@ function catchMessageNotModified(error: any): void {
}

async function replyRenderedMenuPartsToContext<Context extends TelegrafContext>(body: Body, keyboard: InlineKeyboard, context: Context, extra: Readonly<ExtraReplyMessage>): Promise<Message> {
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)
Expand All @@ -80,10 +101,27 @@ async function replyRenderedMenuPartsToContext<Context extends TelegrafContext>(
export function generateSendMenuToChatFunction<Context>(menu: MenuLike<Context>, path: string): (telegram: Readonly<Telegram>, chatId: string | number, context: Context, extra?: Readonly<ExtraReplyMessage>) => Promise<Message> {
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)
Expand All @@ -105,7 +143,7 @@ function createTextExtra(body: string | TextBody, keyboard: InlineKeyboard, base
}
}

function createPhotoExtra(body: PhotoBody, keyboard: InlineKeyboard, base: Readonly<ExtraPhoto>): ExtraPhoto {
function createMediaExtra(body: MediaBody, keyboard: InlineKeyboard, base: Readonly<ExtraPhoto>): ExtraPhoto {
return {
...base,
parse_mode: body.parse_mode,
Expand Down

0 comments on commit f20fe2f

Please sign in to comment.