Skip to content

Commit

Permalink
feat: explicit ad-hoc middleware executor
Browse files Browse the repository at this point in the history
BREAKING CHANGE: You will need to explicitly add executor middleware to your stack.
  • Loading branch information
IlyaSemenov committed Jan 21, 2022
1 parent 8928e10 commit 09cfc97
Show file tree
Hide file tree
Showing 3 changed files with 94 additions and 60 deletions.
10 changes: 7 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,16 +37,20 @@ bot.start()
### With ad-hoc middleware

```ts
// Monkey patch grammy.
import "grammy-pseudo-update"
// This will monkey-patch grammy.
import { pseudoUpdate } from "grammy-pseudo-update"

import { Bot } from "grammy"

const bot = new Bot(process.env.BOT_TOKEN)
bot.use(session(...))
// Add ad-hoc middleware executor after you've prepared the context.
bot.use(pseudoUpdate)
bot.command("start", ...)

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
// This will be executed by `pseudoUpdate` executor above.
await ctx.reply(`External event occured: ${payload}`)
})
})
Expand Down
35 changes: 22 additions & 13 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ export interface PseudoUpdatePayload {
export interface PseudoUpdate {
chat: Chat
payload?: PseudoUpdatePayload
middleware?: MiddlewareFn<any>
}

/** To be merged with Context */
Expand Down Expand Up @@ -67,24 +68,32 @@ Bot.prototype.handlePseudoUpdate = async function <C extends Context>(
): Promise<void> {
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: update.update_id || 0,
pseudo: { chat, payload: update.payload },
}).finally(() => {
if (middleware) {
// Restore this.handler
thisAsAny.handler = thisHandler
}
pseudo: { chat, payload: update.payload, middleware },
})
}

/**
*
* Middleware to process ad-hoc pseudo updates.
*
* Usage:
*
* bot.use(pseudoUpdate)
* ...
* bot.handlePseudoUpdate({ chat_id }, ctx => ctx.reply("Update."))
*
* */
export const pseudoUpdate: MiddlewareFn = (ctx, next) => {
const mw = ctx.update.pseudo?.middleware
if (mw) {
return mw(ctx, next)
} else {
return next()
}
}

// FIXME: replace `any` with C/C2 from the interface declaration
Composer.prototype.pseudo = function (
this: Composer<any>,
Expand Down
109 changes: 65 additions & 44 deletions tests/main.test.ts
Original file line number Diff line number Diff line change
@@ -1,73 +1,94 @@
import "grammy-pseudo-update"

import { Chat } from "@grammyjs/types"
import { Bot } from "grammy"
import { PseudoUpdatePayload } from "grammy-pseudo-update"
import { Bot, Context } from "grammy"
import { pseudoUpdate, PseudoUpdatePayload } from "grammy-pseudo-update"
import tap from "tap"

declare module "grammy-pseudo-update" {
interface PseudoUpdatePayload {
test?: true
test?: string
}
}

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 }
class TestBot<C extends Context = Context> extends Bot<C> {
constructor() {
super("invalid_token")
this.botInfo = {} as any
}
}

const chat: Chat = { type: "private", id: 12345, first_name: "John" }
const payload: PseudoUpdatePayload = { test: true }
const payload: PseudoUpdatePayload = { test: "hello" }

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()
const bot = new TestBot()
const log: string[] = []
bot.pseudo((ctx) => {
log.push(`mw: ${ctx.pseudo?.test}`)
})
await bot.handlePseudoUpdate({ chat, payload })
tap.same(log, ["mw"])
tap.same(log, ["mw: hello"])
})

tap.test("final handler called when no payload", async (tap) => {
const { bot, log } = init()
await bot.handlePseudoUpdate({ chat }, () => {
log.push("final")
tap.test("middleware flow", async (tap) => {
const bot = new TestBot()
const log: string[] = []
bot.pseudo((ctx, next) => {
log.push(`mw1: ${ctx.pseudo?.test}`)
return next()
})
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")
bot.pseudo((ctx) => {
log.push(`mw2: ${ctx.pseudo?.test}`)
})
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")
bot.pseudo((ctx) => {
log.push(`mw3: ${ctx.pseudo?.test}`)
})
await bot.handlePseudoUpdate({ chat })
tap.same(log, ["mw", "final", "mw"])
await bot.handlePseudoUpdate({ chat, payload })
tap.same(log, ["mw1: hello", "mw2: hello"])
})

tap.test("custom update_id", async (tap) => {
const { bot } = init()
const bot = new TestBot()
const update_id = 4 // https://xkcd.com/221/
await bot.handlePseudoUpdate({ chat, update_id }, (ctx) => {
tap.same(ctx.update.update_id, update_id)
})
})

tap.test("ad-hoc handler", async (tap) => {
function init() {
const bot = new TestBot<Context & { foo: string }>()
const log: string[] = []
bot.use((ctx, next) => {
ctx.foo = "a"
return next()
})
bot.pseudo((ctx, next) => {
log.push(`mw1: ${ctx.foo} ${ctx.pseudo?.test}`)
return next()
})
bot.use(pseudoUpdate)
bot.use((ctx, next) => {
ctx.foo = "b"
return next()
})
bot.pseudo((ctx, next) => {
log.push(`mw2: ${ctx.foo} ${ctx.pseudo?.test}`)
return next()
})
return { bot, log }
}
tap.test("with ad-hoc middleware", async (tap) => {
const { bot, log } = init()
await bot.handlePseudoUpdate({ chat, payload }, (ctx) => {
log.push(`ad-hoc: ${ctx.foo}`)
})
tap.same(log, ["mw1: a hello", "ad-hoc: a"])
})
tap.test("without ad-hoc middleware", async (tap) => {
const { bot, log } = init()
await bot.handlePseudoUpdate({ chat, payload })
tap.same(log, ["mw1: a hello", "mw2: b hello"])
})
})

0 comments on commit 09cfc97

Please sign in to comment.