Skip to content

Commit

Permalink
feat: run ad-hoc middleware with pseudo update
Browse files Browse the repository at this point in the history
BREAKING CHANGE: payload is now typed optional
  • Loading branch information
IlyaSemenov committed Jan 14, 2022
1 parent 6ad9c21 commit 77492c1
Show file tree
Hide file tree
Showing 3 changed files with 146 additions and 32 deletions.
42 changes: 39 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ yarn add grammy-pseudo-update

## Use

### With composer middleware

```ts
// Monkey patch grammy.
import "grammy-pseudo-update"
Expand All @@ -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) => {
Expand All @@ -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.
74 changes: 46 additions & 28 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -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<void>
}
interface Composer<C extends Context> {
pseudo<C2 extends PseudoUpdateFlavoredContext<C>>(
handler: MiddlewareFn<C2>
): Composer<C2>
}
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:
Expand All @@ -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
}
Expand All @@ -51,18 +35,52 @@ export type PseudoUpdateFlavoredContext<C extends Context = Context> = 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<C extends Context = Context> {
handlePseudoUpdate(
arg: PseudoUpdateArg,
middleware?: MiddlewareFn<C>
): Promise<void>
}
interface Composer<C extends Context> {
pseudo<C2 extends PseudoUpdateFlavoredContext<C>>(
handler: MiddlewareFn<C2>
): Composer<C2>
}
interface Context extends PseudoUpdateFlavor {}
}

declare module "@grammyjs/types" {
interface Update {
pseudo?: PseudoUpdate
}
}

Bot.prototype.handlePseudoUpdate = async function <C extends Context>(
this: Bot<C>,
update: PseudoUpdateArg,
middleware?: MiddlewareFn<C>
): 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: 0,
pseudo: { chat, payload: update.payload },
}).finally(() => {
if (middleware) {
// Restore this.handler
thisAsAny.handler = thisHandler
}
})
}

Expand Down
62 changes: 61 additions & 1 deletion tests/main.test.ts
Original file line number Diff line number Diff line change
@@ -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"])
})

0 comments on commit 77492c1

Please sign in to comment.