diff --git a/README.md b/README.md index 02682c7..d1cf420 100644 --- a/README.md +++ b/README.md @@ -12,6 +12,8 @@ yarn add grammy-pseudo-update ## Use +### With composer middleware + ```ts // Monkey patch grammy. import "grammy-pseudo-update" @@ -20,9 +22,9 @@ import { Bot } from "grammy" const bot = new Bot(process.env.BOT_TOKEN) -bot.pseudo(async (ctx) => { - console.log(ctx.pseudo) // Payload. - await ctx.reply(`Got pseudo update`) +bot.pseudo(async (ctx, next) => { + // Access payload with ctx.pseudo or ctx.update.pseudo.payload + await ctx.reply(`External event occured: ${ctx.pseudo}`) }) some_external_event_listener((chat_id, payload) => { @@ -31,3 +33,37 @@ some_external_event_listener((chat_id, payload) => { bot.start() ``` + +### With ad-hoc middleware + +```ts +// Monkey patch grammy. +import "grammy-pseudo-update" + +import { Bot } from "grammy" + +const bot = new Bot(process.env.BOT_TOKEN) + +some_external_event_listener((chat_id, payload) => { + bot.handlePseudoUpdate({ chat_id }, (ctx) => { + // Note: this will only be called if no other middleware handles the update + await ctx.reply(`External event occured: ${payload}`) + }) +}) + +bot.start() +``` + +### Typescript + +Augment the payload interface: + +```ts +declare module "grammy-pseudo-update" { + interface PseudoUpdatePayload { + foo?: FooData + } +} +``` + +Note that it is marked as interface (not type) so that multiple plugins could use the same payload type/data object with their dedicated keys. diff --git a/src/index.ts b/src/index.ts index 02963b9..7b0ac98 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,31 +1,8 @@ import { Chat } from "@grammyjs/types" import { Bot, Composer, Context, MiddlewareFn } from "grammy" -declare module "grammy" { - interface Bot { - handlePseudoUpdate(args: PseudoUpdateArg): Promise - } - interface Composer { - pseudo>( - handler: MiddlewareFn - ): Composer - } - interface Context extends PseudoUpdateFlavor {} -} - -declare module "@grammyjs/types" { - interface Update { - pseudo?: PseudoUpdate - } -} - -export interface PseudoUpdate { - chat: Chat - payload: PseudoUpdatePayload -} - /** -Pseudo update payload +Pseudo update payload (ctx.pseudo, ctx.update.pseudo.payload) Extend with: @@ -43,6 +20,13 @@ export interface PseudoUpdatePayload { // Empty - for external augmentation } +/** ctx.update.pseudo */ +export interface PseudoUpdate { + chat: Chat + payload?: PseudoUpdatePayload +} + +/** To be merged with Context */ export interface PseudoUpdateFlavor { readonly pseudo?: PseudoUpdatePayload } @@ -51,18 +35,52 @@ export type PseudoUpdateFlavoredContext = C & PseudoUpdateFlavor type PseudoUpdateArg = ({ chat_id: number } | { chat: Chat }) & { - payload: PseudoUpdatePayload + payload?: PseudoUpdatePayload } -Bot.prototype.handlePseudoUpdate = async function ( - this: Bot, - update: PseudoUpdateArg +declare module "grammy" { + interface Bot { + handlePseudoUpdate( + arg: PseudoUpdateArg, + middleware?: MiddlewareFn + ): Promise + } + interface Composer { + pseudo>( + handler: MiddlewareFn + ): Composer + } + interface Context extends PseudoUpdateFlavor {} +} + +declare module "@grammyjs/types" { + interface Update { + pseudo?: PseudoUpdate + } +} + +Bot.prototype.handlePseudoUpdate = async function ( + this: Bot, + update: PseudoUpdateArg, + middleware?: MiddlewareFn ): Promise { const chat = "chat" in update ? update.chat : await this.api.getChat(update.chat_id) + const thisAsAny = this as any + const thisHandler = thisAsAny.handler + if (middleware) { + // Oy vey! + // Patch this.handler so that this.middleware() inside handleUpdate calls us + thisAsAny.handler = new Composer(thisHandler, middleware).middleware() + } return this.handleUpdate({ update_id: 0, pseudo: { chat, payload: update.payload }, + }).finally(() => { + if (middleware) { + // Restore this.handler + thisAsAny.handler = thisHandler + } }) } diff --git a/tests/main.test.ts b/tests/main.test.ts index d3a322e..a95494d 100644 --- a/tests/main.test.ts +++ b/tests/main.test.ts @@ -1,5 +1,65 @@ import "grammy-pseudo-update" +import { Chat } from "@grammyjs/types" +import { Bot } from "grammy" +import { PseudoUpdatePayload } from "grammy-pseudo-update" import tap from "tap" -tap.ok(true, "it works") +declare module "grammy-pseudo-update" { + interface PseudoUpdatePayload { + test?: true + } +} + +function init() { + const bot = new Bot("invalid_token") + bot.botInfo = {} as any + const log: string[] = [] + bot.pseudo((ctx, next) => { + log.push("mw") + if (!ctx.pseudo?.test) { + return next() + } + }) + return { bot, log } +} + +const chat: Chat = { type: "private", id: 12345, first_name: "John" } +const payload: PseudoUpdatePayload = { test: true } + +tap.test("simple", async (tap) => { + const { bot, log } = init() + await bot.handlePseudoUpdate({ chat }) + tap.same(log, ["mw"]) +}) + +tap.test("simple with payload", async (tap) => { + const { bot, log } = init() + await bot.handlePseudoUpdate({ chat, payload }) + tap.same(log, ["mw"]) +}) + +tap.test("final handler called when no payload", async (tap) => { + const { bot, log } = init() + await bot.handlePseudoUpdate({ chat }, () => { + log.push("final") + }) + tap.same(log, ["mw", "final"]) +}) + +tap.test("final handler ignored when payload provided", async (tap) => { + const { bot, log } = init() + await bot.handlePseudoUpdate({ chat, payload }, () => { + log.push("final") + }) + tap.same(log, ["mw"]) +}) + +tap.test("final handler ignored when payload provided", async (tap) => { + const { bot, log } = init() + await bot.handlePseudoUpdate({ chat }, () => { + log.push("final") + }) + await bot.handlePseudoUpdate({ chat }) + tap.same(log, ["mw", "final", "mw"]) +})