Skip to content

Commit

Permalink
feat: support Bot API 7.9 (#625)
Browse files Browse the repository at this point in the history
* feat: support Bot API 7.9

* test: add tests for paid emoji reactions

* docs: mention support in README

* fix: drop paidRemoved because star reactions cannot be removed
  • Loading branch information
KnorpelSenf committed Aug 16, 2024
1 parent 9ace076 commit 397d390
Show file tree
Hide file tree
Showing 8 changed files with 201 additions and 50 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@

<!-- deno-fmt-ignore-start -->

[![Bot API](https://img.shields.io/badge/Bot%20API-7.8-blue?logo=telegram&style=flat&labelColor=000&color=3b82f6)](https://core.telegram.org/bots/api)
[![Bot API](https://img.shields.io/badge/Bot%20API-7.9-blue?logo=telegram&style=flat&labelColor=000&color=3b82f6)](https://core.telegram.org/bots/api)
[![Deno](https://shield.deno.dev/x/grammy)](https://deno.land/x/grammy)
[![npm](https://img.shields.io/npm/v/grammy?logo=npm&style=flat&labelColor=000&color=3b82f6)](https://www.npmjs.org/package/grammy)
[![All Contributors](https://img.shields.io/github/all-contributors/grammyjs/grammy?style=flat&labelColor=000&color=3b82f6)](#contributors-)
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
"backport": "deno2node tsconfig.json"
},
"dependencies": {
"@grammyjs/types": "3.12.0",
"@grammyjs/types": "3.13.0",
"abort-controller": "^3.0.0",
"debug": "^4.3.4",
"node-fetch": "^2.7.0"
Expand Down
152 changes: 117 additions & 35 deletions src/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -231,10 +231,21 @@ const checker: StaticHas = {
: (Array.isArray(reaction) ? reaction : [reaction]).map((emoji) =>
typeof emoji === "string" ? { type: "emoji", emoji } : emoji
);
const emoji = new Set(
normalized.filter((r) => r.type === "emoji")
.map((r) => r.emoji),
);
const customEmoji = new Set(
normalized.filter((r) => r.type === "custom_emoji")
.map((r) => r.custom_emoji_id),
);
const paid = normalized.some((r) => r.type === "paid");
return <C extends Context>(ctx: C): ctx is ReactionContext<C> => {
if (!hasMessageReaction(ctx)) return false;
const { old_reaction, new_reaction } = ctx.messageReaction;
// try to find a wanted reaction that is new and not old
for (const reaction of new_reaction) {
// first check if the reaction existed previously
let isOld = false;
if (reaction.type === "emoji") {
for (const old of old_reaction) {
Expand All @@ -252,32 +263,29 @@ const checker: StaticHas = {
break;
}
}
} else if (reaction.type === "paid") {
for (const old of old_reaction) {
if (old.type !== "paid") continue;
isOld = true;
break;
}
} else {
// always regard unsupported emoji types as new
}
if (!isOld) {
if (reaction.type === "emoji") {
for (const wanted of normalized) {
if (wanted.type !== "emoji") continue;
if (wanted.emoji === reaction.emoji) {
return true;
}
}
} else if (reaction.type === "custom_emoji") {
for (const wanted of normalized) {
if (wanted.type !== "custom_emoji") continue;
if (
wanted.custom_emoji_id ===
reaction.custom_emoji_id
) {
return true;
}
}
} else {
// always regard unsupported emoji types as new
return true;
}
// disregard reaction if it is not new
if (isOld) continue;
// check if the new reaction is wanted and short-circuit
if (reaction.type === "emoji") {
if (emoji.has(reaction.emoji)) return true;
} else if (reaction.type === "custom_emoji") {
if (customEmoji.has(reaction.custom_emoji_id)) return true;
} else if (reaction.type === "paid") {
if (paid) return true;
} else {
// always regard unsupported emoji types as new
return true;
}
// new reaction not wanted, check next one
}
return false;
};
Expand Down Expand Up @@ -667,15 +675,18 @@ export class Context implements RenamedUpdate {
* customEmojiAdded: [],
* customEmojiKept: [],
* customEmojiRemoved: ['id0123'],
* paid: true,
* paidAdded: false,
* paidRemoved: false,
* }
* ```
* In the above example, a tada reaction was added by the user, and a custom
* emoji reaction with the custom emoji 'id0123' was removed in the same
* update. The user had already reacted with a thumbs up reaction, which
* they left unchanged. As a result, the current reaction by the user is
* thumbs up and tada. Note that the current reaction (both emoji and custom
* emoji in one list) can also be obtained from
* `ctx.messageReaction.new_reaction`.
* update. The user had already reacted with a thumbs up reaction and a paid
* star reaction, which they left both unchanged. As a result, the current
* reaction by the user is thumbs up, tada, and a paid reaction. Note that
* the current reaction (all emoji reactions regardless of type in one list)
* can also be obtained from `ctx.messageReaction.new_reaction`.
*
* Remember that reaction updates only include information about the
* reaction of a specific user. The respective message may have many more
Expand All @@ -700,6 +711,16 @@ export class Context implements RenamedUpdate {
customEmojiKept: string[];
/** Custom emoji removed from this user's reaction */
customEmojiRemoved: string[];
/**
* `true` if a paid reaction is currently present in this user's
* reaction, and `false` otherwise
*/
paid: boolean;
/**
* `true` if a paid reaction was newly added to this user's reaction,
* and `false` otherwise
*/
paidAdded: boolean;
} {
const emoji: ReactionTypeEmoji["emoji"][] = [];
const emojiAdded: ReactionTypeEmoji["emoji"][] = [];
Expand All @@ -709,6 +730,8 @@ export class Context implements RenamedUpdate {
const customEmojiAdded: string[] = [];
const customEmojiKept: string[] = [];
const customEmojiRemoved: string[] = [];
let paid = false;
let paidAdded = false;
const r = this.messageReaction;
if (r !== undefined) {
const { old_reaction, new_reaction } = r;
Expand All @@ -718,6 +741,8 @@ export class Context implements RenamedUpdate {
emoji.push(reaction.emoji);
} else if (reaction.type === "custom_emoji") {
customEmoji.push(reaction.custom_emoji_id);
} else if (reaction.type === "paid") {
paid = paidAdded = true;
}
}
// temporarily move all old emoji to the *Removed arrays
Expand All @@ -726,6 +751,8 @@ export class Context implements RenamedUpdate {
emojiRemoved.push(reaction.emoji);
} else if (reaction.type === "custom_emoji") {
customEmojiRemoved.push(reaction.custom_emoji_id);
} else if (reaction.type === "paid") {
paidAdded = false;
}
}
// temporarily move all new emoji to the *Added arrays
Expand Down Expand Up @@ -771,6 +798,8 @@ export class Context implements RenamedUpdate {
customEmojiAdded,
customEmojiKept,
customEmojiRemoved,
paid,
paidAdded,
};
}

Expand Down Expand Up @@ -1327,7 +1356,7 @@ export class Context implements RenamedUpdate {
}

/**
* Context-aware alias for `api.sendPaidMedia`. Use this method to send paid media to channel chats. On success, the sent Message is returned.
* Context-aware alias for `api.sendPaidMedia`. Use this method to send paid media. On success, the sent Message is returned.
*
* @param star_count The number of Telegram Stars that must be paid to buy access to the media
* @param media An array describing the media to be sent; up to 10 items
Expand Down Expand Up @@ -1495,9 +1524,9 @@ export class Context implements RenamedUpdate {
}

/**
* Context-aware alias for `api.setMessageReaction`. Use this method to change the chosen reactions on a message. Service messages can't be reacted to. Automatically forwarded messages from a channel to its discussion group have the same available reactions as messages in the channel. In albums, bots must react to the first message. Returns True on success.
* Context-aware alias for `api.setMessageReaction`. Use this method to change the chosen reactions on a message. Service messages can't be reacted to. Automatically forwarded messages from a channel to its discussion group have the same available reactions as messages in the channel. Bots can't use paid reactions. Returns True on success.
*
* @param reaction A list of reaction types to set on the message. Currently, as non-premium users, bots can set up to one reaction per message. A custom emoji reaction can be used if it is either already present on the message or explicitly allowed by chat administrators.
* @param reaction A list of reaction types to set on the message. Currently, as non-premium users, bots can set up to one reaction per message. A custom emoji reaction can be used if it is either already present on the message or explicitly allowed by chat administrators. Paid reactions can't be used by bots.
* @param other Optional remaining parameters, confer the official reference below
* @param signal Optional `AbortSignal` to cancel the request
*
Expand Down Expand Up @@ -1564,7 +1593,7 @@ export class Context implements RenamedUpdate {
}

/**
* Context-aware alias for `api.getBusinessConnection`. Use this method to get information about the connection of the bot with a business account. Returns a BusinessConnection object on success.
* Context-aware alias for `api.getBusinessConnection`. Use this method to get information about the connection of the bot with a business account. Returns a BusinessConnection object on success.
* @param signal Optional `AbortSignal` to cancel the request
*
* **Official reference:** https://core.telegram.org/bots/api#getbusinessconnection
Expand Down Expand Up @@ -1904,7 +1933,7 @@ export class Context implements RenamedUpdate {
}

/**
* Context-aware alias for `api.editChatInviteLink`. Use this method to edit a non-primary invite link created by the bot. The bot must be an administrator in the chat for this to work and must have the appropriate administrator rights. Returns the edited invite link as a ChatInviteLink object.
* Context-aware alias for `api.editChatInviteLink`. Use this method to edit a non-primary invite link created by the bot. The bot must be an administrator in the chat for this to work and must have the appropriate administrator rights. Returns the edited invite link as a ChatInviteLink object.
*
* @param invite_link The invite link to edit
* @param other Optional remaining parameters, confer the official reference below
Expand All @@ -1926,7 +1955,60 @@ export class Context implements RenamedUpdate {
}

/**
* Context-aware alias for `api.revokeChatInviteLink`. Use this method to revoke an invite link created by the bot. If the primary link is revoked, a new link is automatically generated. The bot must be an administrator in the chat for this to work and must have the appropriate administrator rights. Returns the revoked invite link as ChatInviteLink object.
* Context-aware alias for `api.createChatSubscriptionInviteLink`. Use this method to create a subscription invite link for a channel chat. The bot must have the can_invite_users administrator rights. The link can be edited using the method editChatSubscriptionInviteLink or revoked using the method revokeChatInviteLink. Returns the new invite link as a ChatInviteLink object.
*
* @param subscription_period The number of seconds the subscription will be active for before the next payment. Currently, it must always be 2592000 (30 days).
* @param subscription_price The amount of Telegram Stars a user must pay initially and after each subsequent subscription period to be a member of the chat; 1-2500
* @param other Optional remaining parameters, confer the official reference below
* @param signal Optional `AbortSignal` to cancel the request
*
* **Official reference:** https://core.telegram.org/bots/api#createchatsubscriptioninvitelink
*/
createChatSubscriptionInviteLink(
subscription_period: number,
subscription_price: number,
other?: Other<
"createChatSubscriptionInviteLink",
"chat_id" | "subscription_period" | "subscription_price"
>,
signal?: AbortSignal,
) {
return this.api.createChatSubscriptionInviteLink(
orThrow(this.chatId, "createChatSubscriptionInviteLink"),
subscription_period,
subscription_price,
other,
signal,
);
}

/**
* Context-aware alias for `api.editChatSubscriptionInviteLink`. Use this method to edit a subscription invite link created by the bot. The bot must have the can_invite_users administrator rights. Returns the edited invite link as a ChatInviteLink object.
*
* @param invite_link The invite link to edit
* @param other Optional remaining parameters, confer the official reference below
* @param signal Optional `AbortSignal` to cancel the request
*
* **Official reference:** https://core.telegram.org/bots/api#editchatsubscriptioninvitelink
*/
editChatSubscriptionInviteLink(
invite_link: string,
other?: Other<
"editChatSubscriptionInviteLink",
"chat_id" | "invite_link"
>,
signal?: AbortSignal,
) {
return this.api.editChatSubscriptionInviteLink(
orThrow(this.chatId, "editChatSubscriptionInviteLink"),
invite_link,
other,
signal,
);
}

/**
* Context-aware alias for `api.revokeChatInviteLink`. Use this method to revoke an invite link created by the bot. If the primary link is revoked, a new link is automatically generated. The bot must be an administrator in the chat for this to work and must have the appropriate administrator rights. Returns the revoked invite link as ChatInviteLink object.
*
* @param invite_link The invite link to revoke
* @param signal Optional `AbortSignal` to cancel the request
Expand Down Expand Up @@ -2232,7 +2314,7 @@ export class Context implements RenamedUpdate {
}

/**
* Context-aware alias for `api.editForumTopic`. Use this method to edit name and icon of a topic in a forum supergroup chat. The bot must be an administrator in the chat for this to work and must have can_manage_topics administrator rights, unless it is the creator of the topic. Returns True on success.
* Context-aware alias for `api.editForumTopic`. Use this method to edit name and icon of a topic in a forum supergroup chat. The bot must be an administrator in the chat for this to work and must have the can_manage_topics administrator rights, unless it is the creator of the topic. Returns True on success.
*
* @param other Optional remaining parameters, confer the official reference below
* @param signal Optional `AbortSignal` to cancel the request
Expand Down Expand Up @@ -2308,7 +2390,7 @@ export class Context implements RenamedUpdate {
}

/**
* Context-aware alias for `api.editGeneralForumTopic`. Use this method to edit the name of the 'General' topic in a forum supergroup chat. The bot must be an administrator in the chat for this to work and must have can_manage_topics administrator rights. Returns True on success.
* Context-aware alias for `api.editGeneralForumTopic`. Use this method to edit the name of the 'General' topic in a forum supergroup chat. The bot must be an administrator in the chat for this to work and must have the can_manage_topics administrator rights. Returns True on success.
*
* @param name New topic name, 1-128 characters
* @param signal Optional `AbortSignal` to cancel the request
Expand Down
Loading

0 comments on commit 397d390

Please sign in to comment.