diff --git a/packages/biscuit/structures/Member.ts b/packages/biscuit/structures/Member.ts index a4d084f..7cfe4e1 100644 --- a/packages/biscuit/structures/Member.ts +++ b/packages/biscuit/structures/Member.ts @@ -10,9 +10,8 @@ import User from "./User.ts"; import * as Routes from "../Routes.ts"; /** - * Represents a guild member - * TODO: add a `guild` property somehow * @link https://discord.com/developers/docs/resources/guild#guild-member-object + * Represents a guild member */ export class Member implements Model { constructor(session: Session, data: DiscordMemberWithUser, guildId: Snowflake) { @@ -21,6 +20,7 @@ export class Member implements Model { this.guildId = guildId; this.avatarHash = data.avatar ? Util.iconHashToBigInt(data.avatar) : undefined; this.nickname = data.nick ? data.nick : undefined; + this.premiumSince = data.premium_since ? Number.parseInt(data.premium_since) : undefined; this.joinedTimestamp = Number.parseInt(data.joined_at); this.roles = data.roles; this.deaf = !!data.deaf; @@ -31,16 +31,40 @@ export class Member implements Model { : undefined; } + /** the session that instantiated this member */ readonly session: Session; + + /** the user this guild member represents */ user: User; + + /** the choosen guild id */ guildId: Snowflake; + + /** the member's guild avatar hash optimized as a bigint */ avatarHash?: bigint; + + /** this user's guild nickname */ nickname?: string; + + /** when the user started boosting the guild */ + premiumSince?: number; + + /** when the user joined the guild */ joinedTimestamp: number; + + /** array of role object ids */ roles: Snowflake[]; + + /** whether the user is deafened in voice channels */ deaf: boolean; + + /** whether the user is muted in voice channels */ mute: boolean; + + /** whether the user has not yet passed the guild's Membership Screening requirements */ pending: boolean; + + /** when the user's timeout will expire and the user will be able to communicate in the guild again, null or a time in the past if the user is not timed out */ communicationDisabledUntilTimestamp?: number; /** shorthand to User.id */ @@ -48,30 +72,36 @@ export class Member implements Model { return this.user.id; } + /** gets the nickname or the username */ get nicknameOrUsername(): string { return this.nickname ?? this.user.username; } + /** gets the joinedAt timestamp as a Date */ get joinedAt(): Date { return new Date(this.joinedTimestamp); } + /** bans a member from this guild and delete previous messages sent by the member */ async ban(options: CreateGuildBan): Promise { await Guild.prototype.banMember.call({ id: this.guildId, session: this.session }, this.user.id, options); return this; } + /** kicks a member from this guild */ async kick(options: { reason?: string }): Promise { await Guild.prototype.kickMember.call({ id: this.guildId, session: this.session }, this.user.id, options); return this; } + /** unbans a member from this guild */ async unban(): Promise { await Guild.prototype.unbanMember.call({ id: this.guildId, session: this.session }, this.user.id); } + /** edits member's nickname, roles, etc */ async edit(options: ModifyGuildMember): Promise { const member = await Guild.prototype.editMember.call( { id: this.guildId, session: this.session }, @@ -82,10 +112,12 @@ export class Member implements Model { return member; } + /** adds a role to this member */ async addRole(roleId: Snowflake, options: { reason?: string } = {}): Promise { await Guild.prototype.addRole.call({ id: this.guildId, session: this.session }, this.user.id, roleId, options); } + /** removes a role from this member */ async removeRole(roleId: Snowflake, options: { reason?: string } = {}): Promise { await Guild.prototype.removeRole.call( { id: this.guildId, session: this.session }, @@ -95,7 +127,7 @@ export class Member implements Model { ); } - /** gets the user's avatar */ + /** gets the members's guild avatar, not to be confused with Member.user.avatarURL() */ avatarURL(options: { format?: ImageFormat; size?: ImageSize } = { size: 128 }): string { let url: string; diff --git a/packages/biscuit/structures/Message.ts b/packages/biscuit/structures/Message.ts index 94b79c3..a6a9f94 100644 --- a/packages/biscuit/structures/Message.ts +++ b/packages/biscuit/structures/Message.ts @@ -10,17 +10,21 @@ import type { MessageActivityTypes, MessageTypes, } from "../../discordeno/mod.ts"; +import type { Channel } from "./channels.ts"; import type { Component } from "./components/Component.ts"; import type { GetReactions } from "../Routes.ts"; +import type { MessageInteraction } from "./interactions/InteractionFactory.ts"; import { MessageFlags } from "../Util.ts"; import { Snowflake } from "../Snowflake.ts"; -import { ThreadChannel } from "./channels.ts"; +import { ChannelFactory, ThreadChannel } from "./channels.ts"; import Util from "../Util.ts"; import User from "./User.ts"; import Member from "./Member.ts"; import Attachment from "./Attachment.ts"; import ComponentFactory from "./components/ComponentFactory.ts"; import MessageReaction from "./MessageReaction.ts"; +import Application, { NewTeam } from "./Application.ts"; +import InteractionFactory from "./interactions/InteractionFactory.ts"; import * as Routes from "../Routes.ts"; import { StickerItem } from "./Sticker.ts"; @@ -34,6 +38,10 @@ export interface AllowedMentions { users?: Snowflake[]; } +/** + * @link https://github.com/denoland/deno_doc/blob/main/lib/types.d.ts + * channelId is optional when creating a reply, but will always be present when receiving an event/response that includes this data model. + * */ export interface CreateMessageReference { messageId: Snowflake; channelId?: Snowflake; @@ -43,6 +51,7 @@ export interface CreateMessageReference { /** * @link https://discord.com/developers/docs/resources/channel#create-message-json-params + * Posts a message to a guild text or DM channel. Returns a message object. Fires a Message Create Gateway event. */ export interface CreateMessage { embeds?: DiscordEmbed[]; @@ -56,16 +65,24 @@ export interface CreateMessage { /** * @link https://discord.com/developers/docs/resources/channel#edit-message-json-params + * Edit a previously sent message. + * Returns a {@link Message} object. Fires a Message Update Gateway event. */ export interface EditMessage extends Partial { flags?: MessageFlags; } -export type ReactionResolvable = string | { +/** + * Represents a guild or unicode {@link Emoji} + */ +export type EmojiResolvable = string | { name: string; id: Snowflake; }; +/** + * A partial {@link User} to represent the author of a message sent by a webhook + * */ export interface WebhookAuthor { id: string; username: string; @@ -74,8 +91,8 @@ export interface WebhookAuthor { } /** - * Represents a message * @link https://discord.com/developers/docs/resources/channel#message-object + * Represents a message */ export class Message implements Model { constructor(session: Session, data: DiscordMessage) { @@ -87,6 +104,12 @@ export class Message implements Model { this.guildId = data.guild_id; this.applicationId = data.application_id; + this.mentions = { + users: data.mentions?.map((user) => new User(session, user)) ?? [], + roleIds: data.mention_roles ?? [], + channels: data.mention_channels?.map((channel) => ChannelFactory.from(session, channel)) ?? [], + } + if (!data.webhook_id) { this.author = new User(session, data.author); } @@ -105,6 +128,10 @@ export class Message implements Model { this.attachments = data.attachments.map((attachment) => new Attachment(session, attachment)); this.embeds = data.embeds; + if (data.interaction) { + this.interaction = InteractionFactory.fromMessage(session, data.interaction, data.guild_id); + } + if (data.thread && data.guild_id) { this.thread = new ThreadChannel(session, data.thread, data.guild_id); } @@ -142,69 +169,173 @@ export class Message implements Model { }; }); } + + if (data.application) { + const application: Partial = { + id: data.application.id, + icon: data.application.icon ? data.application.icon : undefined, + name: data.application.name, + guildId: data.application.guild_id, + flags: data.application.flags, + botPublic: data.application.bot_public, + owner: data.application.owner ? new User(session, data.application.owner as DiscordUser) : undefined, + botRequireCodeGrant: data.application.bot_require_code_grant, + coverImage: data.application.cover_image, + customInstallURL: data.application.custom_install_url, + description: data.application.description, + installParams: data.application.install_params, + tags: data.application.tags, + verifyKey: data.application.verify_key, + team: data.application.team ? NewTeam(session, data.application.team) : undefined, + primarySkuId: data.application.primary_sku_id, + privacyPolicyURL: data.application.privacy_policy_url, + rpcOrigins: data.application.rpc_origins, + slug: data.application.slug, + }; + + Object.setPrototypeOf(application, Application.prototype); + + this.application = application; + } } + /** Reference to the client that instantiated this Message */ readonly session: Session; + + /** id of the message */ readonly id: Snowflake; + /** type of message */ type: MessageTypes; + + /** id of the channel the message was sent in */ channelId: Snowflake; + + /** id of the guild the message was sent in, this should exist on MESSAGE_CREATE and MESSAGE_UPDATE events */ guildId?: Snowflake; + + /** if the message is an Interaction or application-owned webhook, this is the id of the application */ applicationId?: Snowflake; + + /** mentions if any */ + mentions: { + /** users specifically mentioned in the message */ + users: User[]; + + /** roles specifically mentioned in this message */ + roleIds: Snowflake[]; + + /** channels specifically mentioned in the message */ + channels: Channel[]; + }; + + /** sent if the message is a response to an Interaction */ + interaction?: MessageInteraction; + + /** the author of this message, this field is **not** sent on webhook messages */ author!: User; + + /** message flags combined as a bitfield */ flags?: MessageFlags; + + /** whether this message is pinned */ pinned: boolean; + + /** whether this was a TTS message */ tts: boolean; + + /** contents of the message */ content: string; + + /** used for validating a message was sent */ nonce?: string | number; + + /** whether this message mentions everyone */ mentionEveryone: boolean; + /** when this message was sent */ timestamp: number; + + /** when this message was edited */ editedTimestamp?: number; + /** + * sent if the message contains stickers + * **this contains sticker items not stickers** + */ stickers?: StickerItem[]; + + /** reactions to the message */ reactions: MessageReaction[]; + + /** any attached files */ attachments: Attachment[]; + + /** any embedded content */ embeds: DiscordEmbed[]; + + /** member properties for this message's author */ member?: Member; + + /** the thread that was started from this message, includes {@link ThreadMember} */ thread?: ThreadChannel; + + /** sent if the message contains components like buttons, action rows, or other interactive components */ components: Component[]; + /** if the message is generated by a webhook, this is the webhook's author data */ webhook?: WebhookAuthor; + + /** sent with Rich Presence-related chat embeds */ + application?: Partial; + + /** sent with Rich Presence-related chat embeds */ activity?: { partyId?: Snowflake; type: MessageActivityTypes; }; + /** gets the timestamp of this message, this does not requires the timestamp field */ get createdTimestamp(): number { return Snowflake.snowflakeToTimestamp(this.id); } + /** gets the timestamp of this message as a Date */ get createdAt(): Date { return new Date(this.createdTimestamp); } + /** gets the timestamp of this message (sent by the API) */ get sentAt(): Date { return new Date(this.timestamp); } + /** gets the edited timestamp as a Date */ get editedAt(): Date | undefined { return this.editedTimestamp ? new Date(this.editedTimestamp) : undefined; } + /** whether this message was edited */ get edited(): number | undefined { return this.editedTimestamp; } + /** gets the url of the message that points to the message */ get url(): string { return `https://discord.com/channels/${this.guildId ?? "@me"}/${this.channelId}/${this.id}`; } - /** Compatibility with Discordeno */ + /** + * Compatibility with Discordeno + * same as Message.author.bot + */ get isBot(): boolean { return this.author.bot; } + /** + * Pins this message + * */ async pin(): Promise { await this.session.rest.runMethod( this.session.rest, @@ -213,6 +344,9 @@ export class Message implements Model { ); } + /** + * Unpins this message + * */ async unpin(): Promise { await this.session.rest.runMethod( this.session.rest, @@ -243,6 +377,7 @@ export class Message implements Model { return message; } + /** edits the current message flags to supress its embeds */ async suppressEmbeds(suppress: true): Promise; async suppressEmbeds(suppress: false): Promise; async suppressEmbeds(suppress = true) { @@ -255,6 +390,7 @@ export class Message implements Model { return message; } + /** deletes this message */ async delete({ reason }: { reason: string }): Promise { await this.session.rest.runMethod( this.session.rest, @@ -266,7 +402,7 @@ export class Message implements Model { return this; } - /** Replies directly in the channel the message was sent */ + /** Replies directly in the channel where the message was sent */ async reply(options: CreateMessage): Promise { const message = await this.session.rest.runMethod( this.session.rest, @@ -298,14 +434,13 @@ export class Message implements Model { return new Message(this.session, message); } - /** - * alias for Message.addReaction - */ + /** alias for Message.addReaction */ get react() { return this.addReaction; } - async addReaction(reaction: ReactionResolvable): Promise { + /** adds a Reaction */ + async addReaction(reaction: EmojiResolvable): Promise { const r = typeof reaction === "string" ? reaction : `${reaction.name}:${reaction.id}`; await this.session.rest.runMethod( @@ -316,7 +451,8 @@ export class Message implements Model { ); } - async removeReaction(reaction: ReactionResolvable, options?: { userId: Snowflake }): Promise { + /** removes a reaction from someone */ + async removeReaction(reaction: EmojiResolvable, options?: { userId: Snowflake }): Promise { const r = typeof reaction === "string" ? reaction : `${reaction.name}:${reaction.id}`; await this.session.rest.runMethod( @@ -335,8 +471,9 @@ export class Message implements Model { /** * Get users who reacted with this emoji + * not recommended since the cache handles reactions already */ - async fetchReactions(reaction: ReactionResolvable, options?: GetReactions): Promise { + async fetchReactions(reaction: EmojiResolvable, options?: GetReactions): Promise { const r = typeof reaction === "string" ? reaction : `${reaction.name}:${reaction.id}`; const users = await this.session.rest.runMethod( @@ -348,7 +485,10 @@ export class Message implements Model { return users.map((user) => new User(this.session, user)); } - async removeReactionEmoji(reaction: ReactionResolvable): Promise { + /** + * same as Message.removeReaction but removes using a unicode emoji + * */ + async removeReactionEmoji(reaction: EmojiResolvable): Promise { const r = typeof reaction === "string" ? reaction : `${reaction.name}:${reaction.id}`; await this.session.rest.runMethod( @@ -358,6 +498,7 @@ export class Message implements Model { ); } + /** nukes every reaction on the message */ async nukeReactions(): Promise { await this.session.rest.runMethod( this.session.rest, @@ -366,6 +507,7 @@ export class Message implements Model { ); } + /** publishes/crossposts a message to all followers */ async crosspost(): Promise { const message = await this.session.rest.runMethod( this.session.rest, @@ -376,6 +518,7 @@ export class Message implements Model { return new Message(this.session, message); } + /** fetches this message, meant to be used with Function.call since its redundant */ async fetch(): Promise<(Message | undefined)> { const message = await this.session.rest.runMethod( this.session.rest, @@ -388,9 +531,7 @@ export class Message implements Model { return new Message(this.session, message); } - /* - * alias of Message.crosspost - * */ + /** alias of Message.crosspost */ get publish() { return this.crosspost; } diff --git a/packages/biscuit/structures/User.ts b/packages/biscuit/structures/User.ts index 969979b..62628e1 100644 --- a/packages/biscuit/structures/User.ts +++ b/packages/biscuit/structures/User.ts @@ -1,14 +1,14 @@ import type { Model } from "./Base.ts"; import type { Snowflake } from "../Snowflake.ts"; import type { Session } from "../Session.ts"; -import type { DiscordUser } from "../../discordeno/mod.ts"; +import type { DiscordUser, UserFlags, PremiumTypes } from "../../discordeno/mod.ts"; import type { ImageFormat, ImageSize } from "../Util.ts"; import Util from "../Util.ts"; import * as Routes from "../Routes.ts"; /** - * Represents a user * @link https://discord.com/developers/docs/resources/user#user-object + * Represents a user */ export class User implements Model { constructor(session: Session, data: DiscordUser) { @@ -21,25 +21,72 @@ export class User implements Model { this.accentColor = data.accent_color; this.bot = !!data.bot; this.system = !!data.system; - this.banner = data.banner; + this.banner = data.banner ? Util.iconHashToBigInt(data.banner) : undefined; + this.mfaEnabled = !!data.mfa_enabled; + this.locale = data.locale; + this.email = data.email ? data.email : undefined; + this.verified = data.verified; + this.flags = data.flags; } + /** the session that instantiated this User */ readonly session: Session; + + /** the user's id */ readonly id: Snowflake; + /** the user's username, not unique across the platform */ username: string; + + /** the user's 4-digit discord-tag */ discriminator: string; + + /** the user's avatar hash optimized as a bigint */ avatarHash?: bigint; + + /** the user's banner color encoded as an integer representation of hexadecimal color code */ accentColor?: number; + + /** whether the user belongs to an OAuth2 application */ bot: boolean; + + /** whether the user is an Official Discord System user (part of the urgent message system) */ system: boolean; - banner?: string; + + /** the user's banner hash optimized as a bigint */ + banner?: bigint; + + /** whether the user has two factor enabled on their account */ + mfaEnabled: boolean; + + /** the user's chosen language option */ + locale?: string; + + /** the user's email */ + email?: string; + + /** the flags on a user's account */ + flags?: UserFlags; + + /** whether the email on this account has been verified */ + verified?: boolean; + + /** the type of Nitro subscription on a user's account */ + premiumType?: PremiumTypes; + + /** the public flags on a user's account */ + publicFlags?: UserFlags; /** gets the user's username#discriminator */ get tag(): string { return `${this.username}#${this.discriminator}}`; } + /** fetches this user */ + fetch() { + return this.session.fetchUser(this.id); + } + /** gets the user's avatar */ avatarURL(options: { format?: ImageFormat; size?: ImageSize } = { size: 128 }): string { let url: string; diff --git a/packages/biscuit/structures/channels.ts b/packages/biscuit/structures/channels.ts index f0982c9..1bfc048 100644 --- a/packages/biscuit/structures/channels.ts +++ b/packages/biscuit/structures/channels.ts @@ -25,7 +25,7 @@ import { urlToBase64 } from "../util/urlToBase64.ts"; /** Classes and routes */ import * as Routes from "../Routes.ts"; -import Message, { CreateMessage, EditMessage, ReactionResolvable } from "./Message.ts"; +import Message, { CreateMessage, EditMessage, EmojiResolvable } from "./Message.ts"; import Invite from "./Invite.ts"; import Webhook from "./Webhook.ts"; import User from "./User.ts"; @@ -225,7 +225,7 @@ export class TextChannel { await Message.prototype.unpin.call({ id: messageId, channelId: this.id, session: this.session }); } - async addReaction(messageId: Snowflake, reaction: ReactionResolvable): Promise { + async addReaction(messageId: Snowflake, reaction: EmojiResolvable): Promise { await Message.prototype.addReaction.call( { channelId: this.id, id: messageId, session: this.session }, reaction, @@ -234,7 +234,7 @@ export class TextChannel { async removeReaction( messageId: Snowflake, - reaction: ReactionResolvable, + reaction: EmojiResolvable, options?: { userId: Snowflake }, ): Promise { await Message.prototype.removeReaction.call( @@ -244,7 +244,7 @@ export class TextChannel { ); } - async removeReactionEmoji(messageId: Snowflake, reaction: ReactionResolvable): Promise { + async removeReactionEmoji(messageId: Snowflake, reaction: EmojiResolvable): Promise { await Message.prototype.removeReactionEmoji.call( { channelId: this.id, id: messageId, session: this.session }, reaction, @@ -257,7 +257,7 @@ export class TextChannel { async fetchReactions( messageId: Snowflake, - reaction: ReactionResolvable, + reaction: EmojiResolvable, options?: Routes.GetReactions, ): Promise { const users = await Message.prototype.fetchReactions.call( diff --git a/packages/biscuit/structures/interactions/InteractionFactory.ts b/packages/biscuit/structures/interactions/InteractionFactory.ts index aeb5ab3..67eb5a0 100644 --- a/packages/biscuit/structures/interactions/InteractionFactory.ts +++ b/packages/biscuit/structures/interactions/InteractionFactory.ts @@ -1,12 +1,31 @@ import type { Session } from "../../Session.ts"; -import type { DiscordInteraction } from "../../../discordeno/mod.ts"; +import type { Snowflake } from "../../Snowflake.ts"; +import type { DiscordInteraction, DiscordMessageInteraction } from "../../../discordeno/mod.ts"; import { InteractionTypes } from "../../../discordeno/mod.ts"; +import User from "../User.ts"; +import Member from "../Member.ts"; import CommandInteraction from "./CommandInteraction.ts"; import ComponentInteraction from "./ComponentInteraction.ts"; import PingInteraction from "./PingInteraction.ts"; import AutoCompleteInteraction from "./AutoCompleteInteraction.ts"; import ModalSubmitInteraction from "./ModalSubmitInteraction.ts"; +/** + * @link https://discord.com/developers/docs/interactions/receiving-and-responding#message-interaction-object-message-interaction-structure + * */ +export interface MessageInteraction { + /** id of the interaction */ + id: Snowflake; + /** type of interaction */ + type: InteractionTypes + /** name of the application command, including subcommands and subcommand groups */ + name: string + /** user who invoked the interaction */ + user: User; + /** member who invoked the interaction in the guild */ + member?: Partial; +} + export type Interaction = | CommandInteraction | ComponentInteraction @@ -29,6 +48,18 @@ export class InteractionFactory { return new ModalSubmitInteraction(session, interaction); } } + + static fromMessage(session: Session, interaction: DiscordMessageInteraction, _guildId?: Snowflake): MessageInteraction { + const obj = { + id: interaction.id, + type: interaction.type, + name: interaction.name, + user: new User(session, interaction.user), + // TODO: Parse member somehow with the guild id passed in message + }; + + return obj; + } } export default InteractionFactory;