Skip to content

Commit

Permalink
feat: add bot.preCheckoutQuery and bot.shippingQuery (#608)
Browse files Browse the repository at this point in the history
Co-authored-by: Acer <>
  • Loading branch information
grammyz authored Jun 27, 2024
1 parent 086605a commit 49230d2
Show file tree
Hide file tree
Showing 5 changed files with 281 additions and 3 deletions.
79 changes: 79 additions & 0 deletions src/composer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@ import {
type HearsContext,
type InlineQueryContext,
type MaybeArray,
type PreCheckoutQueryContext,
type ReactionContext,
type ShippingQueryContext,
type StringWithSuggestions,
} from "./context.ts";
import { type Filter, type FilterQuery } from "./filter.ts";
Expand Down Expand Up @@ -586,6 +588,61 @@ export class Composer<C extends Context> implements MiddlewareObj<C> {
);
}

/**
* Registers middleware for pre-checkout queries. Telegram sends a pre-checkout
* query to your bot whenever a user has confirmed their payment and shipping
* details. You bot will then receive all information about the order and
* has to respond within 10 seconds with a confirmation of whether everything
* is alright (goods are available, etc.) and the bot is ready to proceed
* with the order. Check out https://core.telegram.org/bots/api#precheckoutquery
* to read more about pre-checkout queries.
*
* ```ts
* bot.preCheckoutQuery('invoice_payload', async ctx => {
* // Answer the pre-checkout query, confer https://core.telegram.org/bots/api#answerprecheckoutquery
* await ctx.answerPreCheckoutQuery( ... )
* })
* ```
*
* @param trigger The string to look for in the invoice payload
* @param middleware The middleware to register
*/
preCheckoutQuery(
trigger: MaybeArray<string | RegExp>,
...middleware: Array<PreCheckoutQueryMiddleware<C>>
): Composer<PreCheckoutQueryContext<C>> {
return this.filter(
Context.has.preCheckoutQuery(trigger),
...middleware,
);
}

/**
* Registers middleware for shipping queries. If you sent an invoice requesting
* a shipping address and the parameter _is_flexible_ was specified, Telegram
* will send a shipping query to your bot whenever a user has confirmed their
* shipping details. You bot will then receive the shipping information and
* can respond with a confirmation of whether delivery to the specified address
* is possible. Check out https://core.telegram.org/bots/api#shippingquery to
* read more about shipping queries.
*
* ```ts
* bot.shippingQuery('invoice_payload', async ctx => {
* // Answer the shipping query, confer https://core.telegram.org/bots/api#answershippingquery
* await ctx.answerShippingQuery( ... )
* })
* ```
*
* @param trigger The string to look for in the invoice payload
* @param middleware The middleware to register
*/
shippingQuery(
trigger: MaybeArray<string | RegExp>,
...middleware: Array<ShippingQueryMiddleware<C>>
): Composer<ShippingQueryContext<C>> {
return this.filter(Context.has.shippingQuery(trigger), ...middleware);
}

/**
* > This is an advanced method of grammY.
*
Expand Down Expand Up @@ -947,6 +1004,28 @@ export type InlineQueryMiddleware<C extends Context> = Middleware<
export type ChosenInlineResultMiddleware<C extends Context> = Middleware<
ChosenInlineResultContext<C>
>;
/**
* Type of the middleware that can be passed to `bot.preCheckoutQuery`.
*
* This helper type can be used to annotate middleware functions that are
* defined in one place, so that they have the correct type when passed to
* `bot.preCheckoutQuery` in a different place. For instance, this allows for more
* modular code where handlers are defined in separate files.
*/
export type PreCheckoutQueryMiddleware<C extends Context> = Middleware<
PreCheckoutQueryContext<C>
>;
/**
* Type of the middleware that can be passed to `bot.shippingQuery`.
*
* This helper type can be used to annotate middleware functions that are
* defined in one place, so that they have the correct type when passed to
* `bot.shippingQuery` in a different place. For instance, this allows for more
* modular code where handlers are defined in separate files.
*/
export type ShippingQueryMiddleware<C extends Context> = Middleware<
ShippingQueryContext<C>
>;
/**
* Type of the middleware that can be passed to `bot.chatType`.
*
Expand Down
93 changes: 92 additions & 1 deletion src/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,28 @@ interface StaticHas {
chosenInlineResult(
trigger: MaybeArray<string | RegExp>,
): <C extends Context>(ctx: C) => ctx is ChosenInlineResultContext<C>;
/**
* Generates a predicate function that can test context objects for
* containing the given pre-checkout query, or for the pre-checkout query
* payload to match the given regular expression. This uses the same logic
* as `bot.preCheckoutQuery`.
*
* @param trigger The string or regex to match
*/
preCheckoutQuery(
trigger: MaybeArray<string | RegExp>,
): <C extends Context>(ctx: C) => ctx is PreCheckoutQueryContext<C>;
/**
* Generates a predicate function that can test context objects for
* containing the given shipping query, or for the shipping query to
* match the given regular expression. This uses the same logic as
* `bot.shippingQuery`.
*
* @param trigger The string or regex to match
*/
shippingQuery(
trigger: MaybeArray<string | RegExp>,
): <C extends Context>(ctx: C) => ctx is ShippingQueryContext<C>;
}
const checker: StaticHas = {
filterQuery<Q extends FilterQuery>(filter: Q | Q[]) {
Expand Down Expand Up @@ -293,6 +315,20 @@ const checker: StaticHas = {
hasChosenInlineResult(ctx) &&
match(ctx, ctx.chosenInlineResult.result_id, trg);
},
preCheckoutQuery(trigger) {
const hasPreCheckoutQuery = checker.filterQuery("pre_checkout_query");
const trg = triggerFn(trigger);
return <C extends Context>(ctx: C): ctx is PreCheckoutQueryContext<C> =>
hasPreCheckoutQuery(ctx) &&
match(ctx, ctx.preCheckoutQuery.invoice_payload, trg);
},
shippingQuery(trigger) {
const hasShippingQuery = checker.filterQuery("shipping_query");
const trg = triggerFn(trigger);
return <C extends Context>(ctx: C): ctx is ShippingQueryContext<C> =>
hasShippingQuery(ctx) &&
match(ctx, ctx.shippingQuery.invoice_payload, trg);
},
};

// === Context class
Expand Down Expand Up @@ -842,7 +878,6 @@ export class Context implements RenamedUpdate {
): this is InlineQueryContextCore {
return Context.has.inlineQuery(trigger)(this);
}

/**
* Returns `true` if this context object contains the chosen inline result, or
* if the contained chosen inline result matches the given regular expression. It
Expand All @@ -855,6 +890,30 @@ export class Context implements RenamedUpdate {
): this is ChosenInlineResultContextCore {
return Context.has.chosenInlineResult(trigger)(this);
}
/**
* Returns `true` if this context object contains the given pre-checkout query,
* or if the contained pre-checkout query matches the given regular expression.
* It returns `false` otherwise. This uses the same logic as `bot.preCheckoutQuery`.
*
* @param trigger The string or regex to match
*/
hasPreCheckoutQuery(
trigger: MaybeArray<string | RegExp>,
): this is PreCheckoutQueryContextCore {
return Context.has.preCheckoutQuery(trigger)(this);
}
/**
* Returns `true` if this context object contains the given shipping query,
* or if the contained shipping query matches the given regular expression.
* It returns `false` otherwise. This uses the same logic as `bot.shippingQuery`.
*
* @param trigger The string or regex to match
*/
hasShippingQuery(
trigger: MaybeArray<string | RegExp>,
): this is ShippingQueryContextCore {
return Context.has.shippingQuery(trigger)(this);
}

// API

Expand Down Expand Up @@ -2908,6 +2967,38 @@ export type ChosenInlineResultContext<C extends Context> = Filter<
"chosen_inline_result"
>;

type PreCheckoutQueryContextCore = FilterCore<"pre_checkout_query">;
/**
* Type of the context object that is available inside the handlers for
* `bot.preCheckoutQuery`.
*
* This helper type can be used to narrow down context objects the same way how
* annotate `bot.preCheckoutQuery` does it. This allows you to context objects in
* middleware that is not directly passed to `bot.preCheckoutQuery`, hence not
* inferring the correct type automatically. That way, handlers can be defined
* in separate files and still have the correct types.
*/
export type PreCheckoutQueryContext<C extends Context> = Filter<
NarrowMatch<C, string | RegExpMatchArray>,
"pre_checkout_query"
>;

type ShippingQueryContextCore = FilterCore<"shipping_query">;
/**
* Type of the context object that is available inside the handlers for
* `bot.shippingQuery`.
*
* This helper type can be used to narrow down context objects the same way how
* annotate `bot.shippingQuery` does it. This allows you to context objects in
* middleware that is not directly passed to `bot.shippingQuery`, hence not
* inferring the correct type automatically. That way, handlers can be defined
* in separate files and still have the correct types.
*/
export type ShippingQueryContext<C extends Context> = Filter<
NarrowMatch<C, string | RegExpMatchArray>,
"shipping_query"
>;

type ChatTypeContextCore<T extends Chat["type"]> =
& Record<"update", ChatTypeUpdate<T>> // ctx.update
& ChatType<T> // ctx.chat
Expand Down
58 changes: 58 additions & 0 deletions test/composer.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -294,6 +294,64 @@ describe("Composer", () => {
});
});

describe(".preCheckoutQuery", () => {
const c = new Context(
// deno-lint-ignore no-explicit-any
{ pre_checkout_query: { invoice_payload: "test" } } as any,
// deno-lint-ignore no-explicit-any
0 as any,
// deno-lint-ignore no-explicit-any
0 as any,
);
it("should check for pre-checkout query data", async () => {
composer.preCheckoutQuery("no-data", middleware);
composer.preCheckoutQuery(["nope", "test"], middleware);
await exec(c);
assertEquals(middleware.calls.length, 1);
assertEquals(middleware.calls[0].args[0], c);
});
it("should allow chaining pre-checkout query data checks", async () => {
composer.preCheckoutQuery(["nope"])
.preCheckoutQuery(["test", "nei"], middleware); // nope
composer.preCheckoutQuery(["nope", "test"])
.preCheckoutQuery(["nei"], middleware); // nope
composer.preCheckoutQuery(["nope", /test/])
.preCheckoutQuery(["test", "nei"], middleware);
await exec(c);
assertEquals(middleware.calls.length, 1);
assertEquals(middleware.calls[0].args[0], c);
});
});

describe(".shippingQuery", () => {
const c = new Context(
// deno-lint-ignore no-explicit-any
{ shipping_query: { invoice_payload: "test" } } as any,
// deno-lint-ignore no-explicit-any
0 as any,
// deno-lint-ignore no-explicit-any
0 as any,
);
it("should check for shipping query data", async () => {
composer.shippingQuery("no-data", middleware);
composer.shippingQuery(["nope", "test"], middleware);
await exec(c);
assertEquals(middleware.calls.length, 1);
assertEquals(middleware.calls[0].args[0], c);
});
it("should allow chaining shipping query data checks", async () => {
composer.shippingQuery(["nope"])
.shippingQuery(["test", "nei"], middleware); // nope
composer.shippingQuery(["nope", "test"])
.shippingQuery(["nei"], middleware); // nope
composer.shippingQuery(["nope", /test/])
.shippingQuery(["test", "nei"], middleware);
await exec(c);
assertEquals(middleware.calls.length, 1);
assertEquals(middleware.calls[0].args[0], c);
});
});

describe(".filter", () => {
const t = () => true;
const f = () => false;
Expand Down
26 changes: 26 additions & 0 deletions test/composer.type.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -304,6 +304,32 @@ describe("Composer types", () => {
});
});

describe(".preCheckoutQuery", () => {
it("should have correct type for properties", () => {
composer.preCheckoutQuery("test", (ctx) => {
const invoicePayload = ctx.preCheckoutQuery.invoice_payload;
const match = ctx.match;
assertType<IsExact<typeof invoicePayload, string>>(true);
assertType<IsExact<typeof match, RegExpMatchArray | string>>(
true,
);
});
});
});

describe(".shippingQuery", () => {
it("should have correct type for properties", () => {
composer.shippingQuery("test", (ctx) => {
const invoicePayload = ctx.shippingQuery.invoice_payload;
const match = ctx.match;
assertType<IsExact<typeof invoicePayload, string>>(true);
assertType<IsExact<typeof match, RegExpMatchArray | string>>(
true,
);
});
});
});

describe(".filter", () => {
it("should have correct type for properties", () => {
type TmpCtx = Context & { prop: number };
Expand Down
28 changes: 26 additions & 2 deletions test/context.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,8 +71,8 @@ describe("Context", () => {
from: u,
inline_message_id: "y",
},
shipping_query: { id: "d", from: u },
pre_checkout_query: { id: "e", from: u },
shipping_query: { id: "d", from: u, invoice_payload: "sq" },
pre_checkout_query: { id: "e", from: u, invoice_payload: "pcq" },
poll: { id: "f" },
poll_answer: { poll_id: "g" },
my_chat_member: { date: 1, from: u, chat: c },
Expand Down Expand Up @@ -491,6 +491,30 @@ describe("Context", () => {
assertFalse(ctx.hasChosenInlineResult("q"));
});

it("should be able to check for pre-checkout queries", () => {
const ctx = new Context(update, api, me);

assert(Context.has.preCheckoutQuery("pcq")(ctx));
assert(ctx.hasPreCheckoutQuery("pcq"));
assert(Context.has.preCheckoutQuery(/^p.q/)(ctx));
assertEquals(ctx.match, "pcq".match(/^p.q/));
assert(ctx.hasPreCheckoutQuery(/^p.q/));
assertFalse(Context.has.preCheckoutQuery("pq")(ctx));
assertFalse(ctx.hasPreCheckoutQuery("pq"));
});

it("should be able to check for shipping queries", () => {
const ctx = new Context(update, api, me);

assert(Context.has.shippingQuery("sq")(ctx));
assert(ctx.hasShippingQuery("sq"));
assert(Context.has.shippingQuery(/^s./)(ctx));
assertEquals(ctx.match, "sq".match(/^s./));
assert(ctx.hasShippingQuery(/^s./));
assertFalse(Context.has.shippingQuery("sp")(ctx));
assertFalse(ctx.hasShippingQuery("sp"));
});

it("should be able to match filter queries", () => {
const ctx = new Context(update, api, me);

Expand Down

0 comments on commit 49230d2

Please sign in to comment.