From d60b36ff0a34b5526311df467fb98ab428af70ce Mon Sep 17 00:00:00 2001 From: Skillz4Killz <23035000+Skillz4Killz@users.noreply.github.com> Date: Tue, 26 Oct 2021 01:29:25 +0000 Subject: [PATCH] message --- src/bot.ts | 6 + src/transformers/component.ts | 145 +++++++++++++++++ src/transformers/embed.ts | 149 ++++++++++++++++++ src/transformers/member.ts | 9 +- src/transformers/message.ts | 117 ++++++++++---- .../messages/components/button_component.ts | 18 +-- .../messages/components/select_option.ts | 18 +-- src/types/messages/message.ts | 2 +- 8 files changed, 412 insertions(+), 52 deletions(-) create mode 100644 src/transformers/component.ts create mode 100644 src/transformers/embed.ts diff --git a/src/bot.ts b/src/bot.ts index 9cbea7138..10ff0a7a1 100644 --- a/src/bot.ts +++ b/src/bot.ts @@ -311,6 +311,8 @@ import { DiscordenoPresence, transformPresence } from "./transformers/presence.t import { DiscordReady } from "./types/gateway/ready.ts"; import { urlToBase64 } from "./util/url_to_base64.ts"; import { transformAttachment } from "./transformers/attachment.ts"; +import { transformEmbed } from "./transformers/embed.ts"; +import { transformComponent } from "./transformers/component.ts"; export function createBot(options: CreateBotOptions) { return { @@ -1098,6 +1100,8 @@ export interface Transformers { activity: typeof transformActivity; presence: typeof transformPresence; attachment: typeof transformAttachment; + embed: typeof transformEmbed; + component: typeof transformComponent; } export function createTransformers(options: Partial) { @@ -1106,6 +1110,8 @@ export function createTransformers(options: Partial) { application: options.application || transformApplication, attachment: options.attachment || transformAttachment, channel: options.channel || transformChannel, + component: options.component || transformComponent, + embed: options.embed || transformEmbed, emoji: options.emoji || transformEmoji, guild: options.guild || transformGuild, integration: options.integration || transformIntegration, diff --git a/src/transformers/component.ts b/src/transformers/component.ts new file mode 100644 index 000000000..481877d56 --- /dev/null +++ b/src/transformers/component.ts @@ -0,0 +1,145 @@ +import { Bot } from "../bot.ts"; +import { ButtonStyles } from "../types/messages/components/button_styles.ts"; +import { DiscordMessageComponentTypes } from "../types/messages/components/message_component_types.ts"; +import { SnakeCasedPropertiesDeep } from "../types/util.ts"; + +export function transformComponent(bot: Bot, payload: SnakeCasedPropertiesDeep): DiscordenoComponent { + return { + type: payload.type, + customId: payload.custom_id, + disabled: payload.disabled, + style: payload.style, + label: payload.label, + emoji: payload.emoji + ? { + id: payload.emoji.id ? bot.transformers.snowflake(payload.emoji.id) : undefined, + name: payload.emoji.name, + animated: payload.emoji.animated, + } + : undefined, + url: payload.url, + options: payload.options?.map((option) => ({ + label: option.label, + value: option.value, + description: option.description, + emoji: option.emoji + ? { + id: option.emoji.id ? bot.transformers.snowflake(option.emoji.id) : undefined, + name: option.emoji.name, + animated: option.emoji.animated, + } + : undefined, + default: option.default, + })), + placeholder: payload.placeholder, + minValues: payload.min_values, + maxValues: payload.max_values, + components: payload.components?.map((component) => bot.transformers.component(bot, component)), + }; +} + +export interface Component { + /** component type */ + type: DiscordMessageComponentTypes; + /** a developer-defined identifier for the component, max 100 characters */ + customId?: string; + /** whether the component is disabled, default false */ + disabled?: boolean; + /** For different styles/colors of the buttons */ + style?: ButtonStyles; + /** text that appears on the button (max 80 characters) */ + label?: string; + /** Emoji object that includes fields of name, id, and animated supporting unicode and custom emojis. */ + emoji?: { + /** Emoji id */ + id?: string; + /** Emoji name */ + name?: string; + /** Whether this emoji is animated */ + animated?: boolean; + }; + /** optional url for link-style buttons that can navigate a user to the web. Only type 5 Link buttons can have a url */ + url?: string; + /** The choices! Maximum of 25 items. */ + options?: SelectOption[]; + /** A custom placeholder text if nothing is selected. Maximum 100 characters. */ + placeholder?: string; + /** The minimum number of items that must be selected. Default 1. Between 1-25. */ + minValues?: number; + /** The maximum number of items that can be selected. Default 1. Between 1-25. */ + maxValues?: number; + /** a list of child components */ + components?: Component[]; +} + +export interface SelectOption { + /** The user-facing name of the option. Maximum 25 characters. */ + label: string; + /** The dev-defined value of the option. Maximum 100 characters. */ + value: string; + /** An additional description of the option. Maximum 50 characters. */ + description?: string; + /** The id, name, and animated properties of an emoji. */ + emoji?: { + /** Emoji id */ + id?: string; + /** Emoji name */ + name?: string; + /** Whether this emoji is animated */ + animated?: boolean; + }; + /** Will render this option as already-selected by default. */ + default: boolean; +} + +export interface DiscordenoComponent { + /** component type */ + type: DiscordMessageComponentTypes; + /** a developer-defined identifier for the component, max 100 characters */ + customId?: string; + /** whether the component is disabled, default false */ + disabled?: boolean; + /** For different styles/colors of the buttons */ + style?: ButtonStyles; + /** text that appears on the button (max 80 characters) */ + label?: string; + /** Emoji object that includes fields of name, id, and animated supporting unicode and custom emojis. */ + emoji?: { + /** Emoji id */ + id?: bigint; + /** Emoji name */ + name?: string; + /** Whether this emoji is animated */ + animated?: boolean; + }; + /** optional url for link-style buttons that can navigate a user to the web. Only type 5 Link buttons can have a url */ + url?: string; + /** The choices! Maximum of 25 items. */ + options?: { + /** The user-facing name of the option. Maximum 25 characters. */ + label: string; + /** The dev-defined value of the option. Maximum 100 characters. */ + value: string; + /** An additional description of the option. Maximum 50 characters. */ + description?: string; + /** The id, name, and animated properties of an emoji. */ + emoji?: { + /** Emoji id */ + id?: bigint; + /** Emoji name */ + name?: string; + /** Whether this emoji is animated */ + animated?: boolean; + }; + /** Will render this option as already-selected by default. */ + default: boolean; + }[]; + /** A custom placeholder text if nothing is selected. Maximum 100 characters. */ + placeholder?: string; + /** The minimum number of items that must be selected. Default 1. Between 1-25. */ + minValues?: number; + /** The maximum number of items that can be selected. Default 1. Between 1-25. */ + maxValues?: number; + /** a list of child components */ + components?: Component[]; +} diff --git a/src/transformers/embed.ts b/src/transformers/embed.ts new file mode 100644 index 000000000..7c2e33ba0 --- /dev/null +++ b/src/transformers/embed.ts @@ -0,0 +1,149 @@ +import { Bot } from "../bot.ts"; +import { SnakeCasedPropertiesDeep } from "../types/util.ts"; +import { DiscordEmbedTypes } from "../types/embeds/embed_types.ts"; +import { Embed } from "../types/embeds/embed.ts"; + +export function transformEmbed(bot: Bot, payload: SnakeCasedPropertiesDeep): DiscordenoEmbed { + return { + title: payload.title, + type: payload.type, + description: payload.description, + url: payload.url, + timestamp: payload.timestamp ? Date.parse(payload.timestamp) : undefined, + color: payload.color, + footer: payload.footer + ? { + text: payload.footer.text, + iconUrl: payload.footer.icon_url, + proxyIconUrl: payload.footer.proxy_icon_url, + } + : undefined, + image: payload.image + ? { + url: payload.image.url, + proxyUrl: payload.image.proxy_url, + height: payload.image.height, + width: payload.image.width, + } + : undefined, + thumbnail: payload.thumbnail + ? { + url: payload.thumbnail.url, + proxyUrl: payload.thumbnail.proxy_url, + height: payload.thumbnail.height, + width: payload.thumbnail.width, + } + : undefined, + video: payload.video + ? { + url: payload.video.url, + proxyUrl: payload.video.proxy_url, + height: payload.video.height, + width: payload.video.width, + } + : undefined, + provider: payload.provider + ? { + name: payload.provider.name, + url: payload.provider.url, + } + : undefined, + author: payload.author + ? { + name: payload.author.name, + url: payload.author.url, + iconUrl: payload.author.icon_url, + proxyIconUrl: payload.author.proxy_icon_url, + } + : undefined, + fields: payload.fields?.map((field) => ({ + name: field.name, + value: field.value, + inline: field.inline, + })), + }; +} + +export interface DiscordenoEmbed { + /** Title of embed */ + title?: string; + /** Type of embed (always "rich" for webhook embeds) */ + type?: DiscordEmbedTypes; + /** Description of embed */ + description?: string; + /** Url of embed */ + url?: string; + /** Timestamp of embed content */ + timestamp?: number; + /** Color code of the embed */ + color?: number; + /** Footer information */ + footer?: { + /** Footer text */ + text: string; + /** Url of footer icon (only supports http(s) and attachments) */ + iconUrl?: string; + /** A proxied url of footer icon */ + proxyIconUrl?: string; + }; + /** Image information */ + image?: { + /** Source url of image (only supports http(s) and attachments) */ + url?: string; + /** A proxied url of the image */ + proxyUrl?: string; + /** Height of image */ + height?: number; + /** Width of image */ + width?: number; + }; + /** Thumbnail information */ + thumbnail?: { + /** Source url of thumbnail (only supports http(s) and attachments) */ + url?: string; + /** A proxied url of the thumbnail */ + proxyUrl?: string; + /** Height of thumbnail */ + height?: number; + /** Width of thumbnail */ + width?: number; + }; + /** Video information */ + video?: { + /** Source url of video */ + url?: string; + /** A proxied url of the video */ + proxyUrl?: string; + /** Height of video */ + height?: number; + /** Width of video */ + width?: number; + }; + /** Provider information */ + provider?: { + /** Name of provider */ + name?: string; + /** Url of provider */ + url?: string; + }; + /** Author information */ + author?: { + /** Name of author */ + name?: string; + /** Url of author */ + url?: string; + /** Url of author icon (only supports http(s) and attachments) */ + iconUrl?: string; + /** A proxied url of author icon */ + proxyIconUrl?: string; + }; + /** Fields information */ + fields?: { + /** Name of the field */ + name: string; + /** Value of the field */ + value: string; + /** Whether or not this field should display inline */ + inline?: boolean; + }[]; +} diff --git a/src/transformers/member.ts b/src/transformers/member.ts index 6a54d3fe8..77c28f06a 100644 --- a/src/transformers/member.ts +++ b/src/transformers/member.ts @@ -1,5 +1,5 @@ import type { Bot } from "../bot.ts"; -import type { GuildMemberWithUser } from "../types/members/guild_member.ts"; +import type { GuildMember, GuildMemberWithUser } from "../types/members/guild_member.ts"; import type { DiscordPremiumTypes } from "../types/users/premium_types.ts"; import type { User } from "../types/users/user.ts"; import type { DiscordUserFlags } from "../types/users/user_flags.ts"; @@ -41,11 +41,12 @@ export function transformUser(bot: Bot, payload: SnakeCasedPropertiesDeep) export function transformMember( bot: Bot, - payload: SnakeCasedPropertiesDeep, - guildId: bigint + payload: SnakeCasedPropertiesDeep, + guildId: bigint, + userId: bigint ): DiscordenoMember { return { - id: bot.transformers.snowflake(payload.user.id), + id: userId, guildId, nick: payload.nick ?? undefined, roles: payload.roles.map((id) => BigInt(id)), diff --git a/src/transformers/message.ts b/src/transformers/message.ts index 14dc54c4c..c71b0f9da 100644 --- a/src/transformers/message.ts +++ b/src/transformers/message.ts @@ -4,8 +4,20 @@ import { CHANNEL_MENTION_REGEX } from "../util/constants.ts"; import { SnakeCasedPropertiesDeep } from "../types/util.ts"; import { DiscordenoAttachment } from "./attachment.ts"; import { DiscordMessageStickerFormatTypes } from "../types/messages/message_sticker_format_types.ts"; +import { DiscordenoMember, DiscordenoUser } from "./member.ts"; +import { DiscordenoEmbed } from "./embed.ts"; +import { DiscordMessageTypes } from "../types/messages/message_types.ts"; +import { DiscordMessageActivityTypes } from "../types/messages/message_activity_types.ts"; +import { DiscordInteractionTypes } from "../types/interactions/interaction_types.ts"; +import { DiscordMessageComponentTypes } from "../types/messages/components/message_component_types.ts"; +import { ButtonStyles } from "../types/messages/components/button_styles.ts"; +import { DiscordenoComponent } from "./component.ts"; +import { Application } from "../types/applications/application.ts"; export function transformMessage(bot: Bot, data: SnakeCasedPropertiesDeep): DiscordenoMessage { + const guildId = data.guild_id ? bot.transformers.snowflake(data.guild_id) : undefined; + const userId = bot.transformers.snowflake(data.author.id); + return { // UNTRANSFORMED STUFF HERE content: data.content || "", @@ -15,15 +27,35 @@ export function transformMessage(bot: Bot, data: SnakeCasedPropertiesDeep bot.transformers.attachment(bot, attachment)), - embeds: data.embeds, - reactions: data.reactions, + embeds: data.embeds.map((embed) => bot.transformers.embed(bot, embed)), + reactions: data.reactions?.map((reaction) => ({ + me: reaction.me, + count: reaction.count, + emoji: { + id: reaction.emoji.id ? bot.transformers.snowflake(reaction.emoji.id) : undefined, + name: reaction.emoji.name, + animated: reaction.emoji.animated, + }, + })), type: data.type, - activity: data.activity, + activity: data.activity + ? { + type: data.activity.type, + partyId: data.activity.party_id, + } + : undefined, application: data.application, flags: data.flags, - interaction: data.interaction, + interaction: data.interaction + ? { + id: bot.transformers.snowflake(data.interaction.id), + type: data.interaction.type, + name: data.interaction.name, + user: bot.transformers.user(bot, data.interaction.user), + } + : undefined, thread: data.thread, - components: data.components, + components: data.components?.map((component) => bot.transformers.component(bot, component)), stickerItems: data.sticker_items?.map((sticker) => ({ id: bot.transformers.snowflake(sticker.id), name: sticker.name, @@ -32,10 +64,10 @@ export function transformMessage(bot: Bot, data: SnakeCasedPropertiesDeep { +export interface DiscordenoMessage { id: bigint; /** Whether or not this message was sent by a bot */ isBot: boolean; @@ -136,4 +150,53 @@ export interface DiscordenoMessage /** Type of sticker format */ formatType: DiscordMessageStickerFormatTypes; }[]; + + /** + * Member properties for this message's author + * Note: The member object exists in `MESSAGE_CREATE` and `MESSAGE_UPDATE` events from text-based guild channels. This allows bots to obtain real-time member data without requiring bots to store member state in memory. + */ + member?: DiscordenoMember; + /** Any embedded content */ + embeds: DiscordenoEmbed[]; + /** Reactions to the message */ + reactions?: { + me: boolean; + count: number; + emoji: { id?: bigint; name?: string; animated?: boolean }; + }[]; + /** Used for validating a message was sent */ + nonce?: number | string; + /** Type of message */ + type: DiscordMessageTypes; + /** Sent with Rich Presence-related chat embeds */ + activity?: { + /** Type of message activity */ + type: DiscordMessageActivityTypes; + /** `party_id` from a Rich Presence event */ + partyId?: string; + }; + /** Sent with Rich Presence-related chat embeds */ + application?: Partial>; + /** Message flags combined as a bitfield */ + flags?: number; + /** + * The message associated with the `message_reference` + * Note: This field is only returned for messages with a `type` of `19` (REPLY). If the message is a reply but the `referenced_message` field is not present, the backend did not attempt to fetch the message that was being replied to, so its state is unknown. If the field exists but is null, the referenced message was deleted. + */ + referencedMessage?: Message | null; + /** Sent if the message is a response to an Interaction */ + interaction?: { + /** Id of the interaction */ + id: bigint; + /** The type of interaction */ + type: DiscordInteractionTypes; + /** The name of the ApplicationCommand */ + name: string; + /** The user who invoked the interaction */ + user: DiscordenoUser; + }; + /** The thread that was started from this message, includes thread member object */ + thread?: Omit & { member: ThreadMember }; + /** The components related to this message */ + components?: DiscordenoComponent[]; } diff --git a/src/types/messages/components/button_component.ts b/src/types/messages/components/button_component.ts index 521f2fdcc..8c4997864 100644 --- a/src/types/messages/components/button_component.ts +++ b/src/types/messages/components/button_component.ts @@ -13,16 +13,14 @@ export interface ButtonComponent { /** For different styles/colors of the buttons */ style: ButtonStyles; /** Emoji object that includes fields of name, id, and animated supporting unicode and custom emojis. */ - emoji?: - | string - | { - /** Emoji id */ - id?: string; - /** Emoji name */ - name?: string; - /** Whether this emoji is animated */ - animated?: boolean; - }; + emoji?: { + /** Emoji id */ + id?: string; + /** Emoji name */ + name?: string; + /** Whether this emoji is animated */ + animated?: boolean; + }; /** optional url for link-style buttons that can navigate a user to the web. Only type 5 Link buttons can have a url */ url?: string; /** Whether or not this button is disabled */ diff --git a/src/types/messages/components/select_option.ts b/src/types/messages/components/select_option.ts index 8779761d5..474be51ce 100644 --- a/src/types/messages/components/select_option.ts +++ b/src/types/messages/components/select_option.ts @@ -7,16 +7,14 @@ export interface SelectOption { /** An additional description of the option. Maximum 50 characters. */ description?: string; /** The id, name, and animated properties of an emoji. */ - emoji?: - | string - | { - /** Emoji id */ - id?: string; - /** Emoji name */ - name?: string; - /** Whether this emoji is animated */ - animated?: boolean; - }; + emoji?: { + /** Emoji id */ + id?: string; + /** Emoji name */ + name?: string; + /** Whether this emoji is animated */ + animated?: boolean; + }; /** Will render this option as already-selected by default. */ default: boolean; } diff --git a/src/types/messages/message.ts b/src/types/messages/message.ts index d37fe4ef9..d481d5a8e 100644 --- a/src/types/messages/message.ts +++ b/src/types/messages/message.ts @@ -32,7 +32,7 @@ export interface Message { * Member properties for this message's author * Note: The member object exists in `MESSAGE_CREATE` and `MESSAGE_UPDATE` events from text-based guild channels. This allows bots to obtain real-time member data without requiring bots to store member state in memory. */ - member?: Partial; + member?: GuildMember; /** Contents of the message */ content?: string; /** When this message was sent */