import { Composer , Context as BaseContext } from 'grammy' ; import { ActionFunc } from './action-hive' ; import { combineTrigger , createRootMenuTrigger , combinePath } from './path' ; import { ContextPathFunc , RegExpLike } from './generic-types' ; import { MenuLike } from './menu-like' ; import { SendMenuFunc , editMenuOnContext , replyMenuToContext } from './send-menu' ; type Responder < Context > = MenuResponder < Context > | ActionResponder < Context > ; interface MenuResponder < Context > { readonly type : 'menu' ; readonly trigger : RegExpLike ; readonly canEnter : ContextPathFunc < Context , boolean > ; readonly menu : MenuLike < Context > ; readonly submenuResponders : ReadonlyArray < MenuResponder < Context > > ; readonly actionResponders : ReadonlyArray < ActionResponder < Context > > ; } interface ActionResponder < Context > { readonly type : 'action' ; readonly trigger : RegExpLike ; readonly do : ActionFunc < Context > ; } export interface Options < Context > { /**
     * Function which is used to send and update the menu.
     *
     * Defaults to `editMenuOnContext`
     */ readonly sendMenu ? : SendMenuFunc < Context > ; } export class MenuMiddleware < Context extends BaseContext > { private readonly _sendMenu : SendMenuFunc < Context > ; private readonly _responder : MenuResponder < Context > ; constructor ( public readonly rootTrigger , readonly rootMenu , readonly options = { } ) { const rootTriggerRegex = createRootMenuTrigger ( rootTrigger ) ; this . _responder = createResponder ( rootTriggerRegex , ( ) => true , rootMenu ) ; this . _sendMenu = options . sendMenu ?? editMenuOnContext ; } param context example async replyToContext ( context , path = this . rootTrigger ) { if ( typeof path === 'function' ) { throw new TypeError ( 'Do not supply this as a middleware directly. Supply it as a function `ctx => menuMiddleware.replyToContext(ctx)`' ) ; } if ( typeof path !== 'string' ) { throw new TypeError ( 'You have to specify an absolute path explicitly as a string in the second argument.' ) ; } const { match , responder } = await getLongestMatchMenuResponder ( context , path , this . _responder ) ; if ( ! match ) { throw new Error ( 'There is no menu which works with your supplied path: ' + path ) ; } return replyMenuToContext ( responder . menu , context , path ) ; } /**
     * The tree structure can be shown for debugging purposes.
     * You can take a look on the menu you created.
     */ tree ( ) { return 'Menu Tree\n' + responderTree ( this . _responder ) ; } middleware ( ) { const composer = new Composer < Context > ( ) ; const trigger = new RegExp ( this . _responder . trigger . source , this . _responder . trigger . flags ) ; composer . callbackQuery ( trigger , async ( context , next ) => { if ( ! ( 'data' in context . callbackQuery ) ) { return next ( ) ; } const path = context . callbackQuery . data ; let target = path ; if ( ! path . endsWith ( '/' ) ) { const { match , responder } = await getLongestMatchActionResponder ( context , path , this . _responder ) ; if ( match ?. [ 0 ] && responder . type === 'action' ) { context . match = match ; const afterwardsTarget = await responder . do ( context , match [ 0 ] ) ; if ( typeof afterwardsTarget === 'string' && afterwardsTarget ) { target = combinePath ( path , afterwardsTarget ) ; } else if ( afterwardsTarget === true ) { target = combinePath ( path , '.' ) ; } else if ( afterwardsTarget === false ) { target = undefined ; } else { throw new Error ( 'You have to return in your do function if you want to update the menu afterwards or not. If not just use return false.' ) ; } } } if ( target ) { const { match , responder } = await getLongestMatchMenuResponder ( context , target , this . _responder ) ; if ( ! match ?. [ 0 ] ) { throw new Error ( `There is no menu "${ target }" which can be reached in this menu` ) ; } context . match = match ; const targetPath = match [ 0 ] ; await this . _sendMenu ( responder . menu as any , context , targetPath ) ; await context . answerCallbackQuery ( ) . catch ( catchCallbackOld ) ; } } ) ; return composer . middleware ( ) ; } } function catchCallbackOld ( error ) { if ( error instanceof Error && error . message . includes ( 'query is too old and response timeout expired' ) ) { return ; } throw error ; } function responderMatch < Context > ( responder , path ) { return new RegExp ( responder . trigger . source , responder . trigger . flags ) . exec ( path ) ; } async function getLongestMatchMenuResponder < Context extends BaseContext > ( context , path , current ) { for ( const sub of current . submenuResponders ) { const match = responderMatch ( sub , path ) ; if ( ! match ?. [ 0 ] ) { continue ; } context . match = match ; if ( await sub . canEnter ( context , match [ 0 ] ) ) { return getLongestMatchMenuResponder ( context , path , sub ) ; } } const match = responderMatch ( current , path ) ; return { match , responder : current } ; } async function getLongestMatchActionResponder < Context extends BaseContext > ( context , path , current ) { const currentMatch = responderMatch ( current , path ) ; for ( const sub of current . submenuResponders ) { const match = responderMatch ( sub , path ) ; if ( ! match ?. [ 0 ] ) { continue ; } context . match = match ; if ( await sub . canEnter ( context , match [ 0 ] ) ) { return getLongestMatchActionResponder ( context , path , sub ) ; } return { match : currentMatch , responder : current } ; } for ( const sub of current . actionResponders ) { const match = responderMatch ( sub , path ) ; if ( ! match ) { continue ; } return { match , responder : sub } ; } return { match : currentMatch , responder : current } ; } function createResponder < Context extends BaseContext > ( menuTrigger , canEnter , menu ) { const actionResponders = [ ... menu . renderActionHandlers ( menuTrigger ) ] . map ( ( { trigger , doFunction } ) : ActionResponder < Context > => ( { type : 'action' , trigger , do : doFunction , } ) ) ; const submenuResponders = [ ... menu . listSubmenus ( ) ] . map ( ( submenu ) : MenuResponder < Context > => { const submenuTrigger = combineTrigger ( menuTrigger , submenu . action ) ; const canEnterSubmenu = async ( context , path ) => { if ( await submenu . hide ?. ( context , path ) ) { return false ; } return true ; } ; return createResponder ( submenuTrigger , canEnterSubmenu , submenu . menu ) ; } ) ; return { type : 'menu' , trigger : menuTrigger , canEnter , menu , actionResponders , submenuResponders , } ; } function responderTree < Context > ( responder , indention = '' ) { let text = treeLine ( indention , responder . type , responder . trigger . source ) ; const subIndention = '  ' + indention ; for ( const action of responder . actionResponders ) { text += treeLine ( subIndention , action . type , action . trigger . source ) ; } for ( const submenu of responder . submenuResponders ) { text += responderTree ( submenu , subIndention ) ; } return text ; } function treeLine ( indention , type , regexSource ) { let text = indention + type ; const offset = Math . max ( 1 , 30 - text . length ) ; for ( let i = 0 ; i < offset ; i ++ ) { text += ' ' ; } text += regexSource . replace ( /\\\//g , '/' ) . replace ( /^\^/ , '' ) . replace ( /\$$/ , '' ) ; text += '\n' ; return text ; }