From 397d390da9d05f9d8ef6fcb94e5168961663bacd Mon Sep 17 00:00:00 2001 From: KnorpelSenf Date: Fri, 16 Aug 2024 14:14:54 +0200 Subject: [PATCH] feat: support Bot API 7.9 (#625) * 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 --- README.md | 2 +- package.json | 2 +- src/context.ts | 152 +++++++++++++++++++++++++++++++++---------- src/core/api.ts | 68 +++++++++++++++++-- src/filter.ts | 1 + src/types.deno.ts | 4 +- src/types.web.ts | 4 +- test/context.test.ts | 18 ++++- 8 files changed, 201 insertions(+), 50 deletions(-) diff --git a/README.md b/README.md index 9ad8c7bf..cb977c62 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ -[![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-) diff --git a/package.json b/package.json index 2baa006c..9e52612a 100644 --- a/package.json +++ b/package.json @@ -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" diff --git a/src/context.ts b/src/context.ts index 2b334df2..020e5860 100644 --- a/src/context.ts +++ b/src/context.ts @@ -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 (ctx: C): ctx is ReactionContext => { 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) { @@ -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; }; @@ -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 @@ -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"][] = []; @@ -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; @@ -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 @@ -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 @@ -771,6 +798,8 @@ export class Context implements RenamedUpdate { customEmojiAdded, customEmojiKept, customEmojiRemoved, + paid, + paidAdded, }; } @@ -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 @@ -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 * @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 diff --git a/src/core/api.ts b/src/core/api.ts index 7e1ede69..6f996ff4 100644 --- a/src/core/api.ts +++ b/src/core/api.ts @@ -655,7 +655,7 @@ export class Api { } /** - * Use this method to send paid media to channel chats. On success, the sent Message is returned. + * Use this method to send paid media. On success, the sent Message is returned. * * @param chat_id Unique identifier for the target chat or username of the target channel (in the format @channelusername) * @param star_count The number of Telegram Stars that must be paid to buy access to the media @@ -782,11 +782,11 @@ export class Api { } /** - * 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. + * 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 chat_id Unique identifier for the target chat or username of the target channel (in the format @channelusername) * @param message_id Identifier of the target message - * @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 * @@ -1117,7 +1117,7 @@ export class Api { } /** - * 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. + * 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 chat_id Unique identifier for the target chat or username of the target channel (in the format @channelusername) * @param invite_link The invite link to edit @@ -1139,7 +1139,61 @@ export class Api { } /** - * 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. + * 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 chat_id Unique identifier for the target channel chat or username of the target channel (in the format @channelusername) + * @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( + chat_id: number | string, + subscription_period: number, + subscription_price: number, + other?: Other< + R, + "createChatSubscriptionInviteLink", + "chat_id" | "subscription_period" | "subscription_price" + >, + signal?: AbortSignal, + ) { + return this.raw.createChatSubscriptionInviteLink( + { chat_id, subscription_period, subscription_price, ...other }, + signal, + ); + } + + /** + * 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 chat_id Unique identifier for the target chat or username of the target channel (in the format @channelusername) + * @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( + chat_id: number | string, + invite_link: string, + other?: Other< + R, + "editChatSubscriptionInviteLink", + "chat_id" | "invite_link" + >, + signal?: AbortSignal, + ) { + return this.raw.editChatSubscriptionInviteLink( + { chat_id, invite_link, ...other }, + signal, + ); + } + + /** + * 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 chat_id Unique identifier of the target chat or username of the target channel (in the format @channelusername) * @param invite_link The invite link to revoke @@ -1437,7 +1491,7 @@ export class Api { } /** - * 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. + * 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 chat_id Unique identifier for the target chat or username of the target supergroup (in the format @supergroupusername) * @param message_thread_id Unique identifier for the target message thread of the forum topic @@ -1536,7 +1590,7 @@ export class Api { } /** - * 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. + * 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 chat_id Unique identifier for the target chat or username of the target supergroup (in the format @supergroupusername) * @param name New topic name, 1-128 characters diff --git a/src/filter.ts b/src/filter.ts index a651ee2e..10eea068 100644 --- a/src/filter.ts +++ b/src/filter.ts @@ -256,6 +256,7 @@ const STICKER_KEYS = { const REACTION_KEYS = { emoji: {}, custom_emoji: {}, + paid: {}, } as const; // L2 diff --git a/src/types.deno.ts b/src/types.deno.ts index efc3bf08..55f3ff46 100644 --- a/src/types.deno.ts +++ b/src/types.deno.ts @@ -14,13 +14,13 @@ import { type InputPaidMediaVideo as InputPaidMediaVideoF, type InputSticker as InputStickerF, type Opts as OptsF, -} from "https://deno.land/x/grammy_types@v3.12.0/mod.ts"; +} from "https://deno.land/x/grammy_types@v3.13.0/mod.ts"; import { debug as d, isDeno } from "./platform.deno.ts"; const debug = d("grammy:warn"); // === Export all API types -export * from "https://deno.land/x/grammy_types@v3.12.0/mod.ts"; +export * from "https://deno.land/x/grammy_types@v3.13.0/mod.ts"; /** A value, or a potentially async function supplying that value */ type MaybeSupplier = T | (() => T | Promise); diff --git a/src/types.web.ts b/src/types.web.ts index a22824c8..3973ef35 100644 --- a/src/types.web.ts +++ b/src/types.web.ts @@ -13,10 +13,10 @@ import { type InputPaidMediaVideo as InputPaidMediaVideoF, type InputSticker as InputStickerF, type Opts as OptsF, -} from "https://deno.land/x/grammy_types@v3.12.0/mod.ts"; +} from "https://deno.land/x/grammy_types@v3.13.0/mod.ts"; // === Export all API types -export * from "https://deno.land/x/grammy_types@v3.12.0/mod.ts"; +export * from "https://deno.land/x/grammy_types@v3.13.0/mod.ts"; /** Something that looks like a URL. */ interface URLLike { diff --git a/test/context.test.ts b/test/context.test.ts index 2a08f960..23a697ea 100644 --- a/test/context.test.ts +++ b/test/context.test.ts @@ -324,6 +324,7 @@ describe("Context", () => { { type: "emoji", emoji: "🎉" }, { type: "custom_emoji", custom_emoji_id: "id" }, { type: "emoji", emoji: "👍" }, + { type: "paid" }, ], }, }; @@ -337,9 +338,12 @@ describe("Context", () => { assert(ctx.hasReaction({ type: "emoji", emoji: "👍" })); assert(Context.has.reaction([{ type: "emoji", emoji: "👍" }])(ctx)); assert(ctx.hasReaction([{ type: "emoji", emoji: "👍" }])); + assert(Context.has.reaction({ type: "paid" })); + assert(ctx.hasReaction({ type: "paid" })); assertFalse(Context.has.reaction("👎")(ctx)); assertFalse(ctx.hasReaction("👎")); + const removed = { type: "paid" as const }; const added = { type: "custom_emoji" as const, custom_emoji_id: "id_new", @@ -351,6 +355,7 @@ describe("Context", () => { date: 42, message_id: 2, old_reaction: [ + removed, { type: "emoji", emoji: "🎉" }, { type: "custom_emoji", custom_emoji_id: "id" }, ], @@ -367,6 +372,8 @@ describe("Context", () => { assert(ctx.hasReaction(added)); assert(Context.has.reaction(["🏆", added])(ctx)); assert(ctx.hasReaction(["🏆", added])); + assertFalse(Context.has.reaction(removed)(ctx)); + assertFalse(ctx.hasReaction(removed)); }); it("should be able to check for chat types", () => { @@ -613,10 +620,11 @@ describe("Context", () => { const cye = { type: "custom_emoji", custom_emoji_id: "id-ye" }; const cno = { type: "custom_emoji", custom_emoji_id: "id-no" }; const cok = { type: "custom_emoji", custom_emoji_id: "id-ok" }; + const p = { type: "paid" }; let up = { message_reaction: { - old_reaction: [ye, no, cye, cno], - new_reaction: [ok, no, cok, cno], + old_reaction: [ye, no, p, cye, cno], + new_reaction: [ok, no, p, cok, cno], }, } as Update; let ctx = new Context(up, api, me); @@ -629,6 +637,8 @@ describe("Context", () => { customEmojiRemoved, customEmojiKept, customEmojiAdded, + paid, + paidAdded, } = ctx.reactions(); assertEquals(emoji, [ok.emoji, no.emoji]); assertEquals(emojiRemoved, [ye.emoji]); @@ -638,6 +648,8 @@ describe("Context", () => { assertEquals(customEmojiRemoved, [cye.custom_emoji_id]); assertEquals(customEmojiKept, [cno.custom_emoji_id]); assertEquals(customEmojiAdded, [cok.custom_emoji_id]); + assertEquals(paid, true); + assertEquals(paidAdded, false); up = { message: update.message } as Update; ctx = new Context(up, api, me); @@ -650,6 +662,8 @@ describe("Context", () => { customEmojiRemoved: [], customEmojiKept: [], customEmojiAdded: [], + paid: false, + paidAdded: false, }); }); });