diff --git a/src/discord/audit-log.ts b/src/discord/audit-log.ts new file mode 100644 index 0000000..bacb9b6 --- /dev/null +++ b/src/discord/audit-log.ts @@ -0,0 +1,53 @@ +import { fetchDiscord } from "./core"; + +export interface AuditLog { + application_commands: any[]; + audit_log_entries: AuditLogEntry[]; + auto_moderation_rules: any[]; + guild_scheduled_events: any[]; + integrations: any[]; + threads: any[]; + users: any[]; + webhooks: any[]; +} + +export interface AuditLogEntry { + target_id: string | null; + changes?: AuditLogChange[]; + user_id: string | null; + id: string; + action_type: number; + options?: any; + reason?: string; +} + +export interface AuditLogChange { + new_value?: any; + old_value?: any; + key: string; +} + +export const getAuditLog = async (options: { + guildId: string; + userId?: string; + actionType?: number; + before?: string; + after?: string; + limit?: number; +}): Promise => { + const { guildId, userId, actionType, before, after, limit } = options; + const query = new URLSearchParams(); + if (userId) query.append("user_id", userId); + if (actionType) query.append("action_type", String(actionType)); + if (before) query.append("before", before); + if (after) query.append("after", after); + if (limit) query.append("limit", String(limit)); + + const queryString = query.toString(); + const path = `/guilds/${guildId}/audit-logs${ + queryString ? `?${queryString}` : "" + }`; + + const response = await fetchDiscord(path); + return response.json(); +}; diff --git a/src/discord/core.ts b/src/discord/core.ts new file mode 100644 index 0000000..a1445b7 --- /dev/null +++ b/src/discord/core.ts @@ -0,0 +1,21 @@ +import { Config } from "../config"; + +export const fetchDiscord = async ( + path: string, + init?: RequestInit, +): Promise => { + const response = await fetch(`${Config.DISCORD_API}${path}`, { + ...init, + headers: { + Authorization: `Bot ${Config.BOT_TOKEN}`, + "Content-Type": "application/json", + ...(init?.headers || {}), + }, + }); + if (response.status === 429) { + const data = await response.json().catch(() => ({}) as any); + const retry = (data?.retry_after ? Number(data.retry_after) : 1) * 1000; + await new Promise((r) => setTimeout(r, retry)); + } + return response; +}; diff --git a/src/discord/guild.ts b/src/discord/guild.ts new file mode 100644 index 0000000..6602ad8 --- /dev/null +++ b/src/discord/guild.ts @@ -0,0 +1,252 @@ +import { fetchDiscord } from "./core"; + +/** + * Represents a Discord guild. + * This is a partial interface based on the Discord API documentation. + */ +export interface Guild { + id: string; + name: string; + icon: string | null; + owner_id: string; + verification_level: number; + roles: any[]; // Array of role objects + emojis: any[]; // Array of emoji objects + features: string[]; +} + +/** + * Represents a Guild Member. + * This is a partial interface. + */ +export interface GuildMember { + user?: any; // User object + nick?: string | null; + roles: string[]; + joined_at: string; // ISO8601 timestamp + deaf: boolean; + mute: boolean; +} + +/** + * Represents a Role. + */ +export interface Role { + id: string; + name: string; + color: number; + hoist: boolean; + position: number; + permissions: string; + managed: boolean; + mentionable: boolean; +} + +/** + * Represents a Ban. + */ +export interface Ban { + reason: string | null; + user: any; // User object +} + +/** + * Get a guild by ID. + * @param guildId The ID of the guild. + * @param withCounts Whether to include approximate member and presence counts. + * @returns A guild object. + */ +export const getGuild = async ( + guildId: string, + withCounts?: boolean, +): Promise => { + const path = `/guilds/${guildId}${withCounts ? "?with_counts=true" : ""}`; + const res = await fetchDiscord(path); + return res.json(); +}; + +/** + * Modify a guild's settings. + * @param guildId The ID of the guild. + * @param data The new guild data. + * @returns The updated guild object. + */ +export const modifyGuild = async ( + guildId: string, + data: any, +): Promise => { + const path = `/guilds/${guildId}`; + const res = await fetchDiscord(path, { + method: "PATCH", + body: JSON.stringify(data), + }); + return res.json(); +}; + +/** + * Get a list of guild channel objects. + * @param guildId The ID of the guild. + * @returns A list of channel objects. + */ +export const getGuildChannels = async (guildId: string): Promise => { + const path = `/guilds/${guildId}/channels`; + const res = await fetchDiscord(path); + return res.json(); +}; + +/** + * Create a new channel object for the guild. + * @param guildId The ID of the guild. + * @param data The channel data. + * @returns The new channel object. + */ +export const createGuildChannel = async ( + guildId: string, + data: any, +): Promise => { + const path = `/guilds/${guildId}/channels`; + const res = await fetchDiscord(path, { + method: "POST", + body: JSON.stringify(data), + }); + return res.json(); +}; + +/** + * Returns a list of guild member objects that are members of the guild. + * @param guildId The ID of the guild. + * @param options Options for listing members. + * @returns A list of guild member objects. + */ +export const listGuildMembers = async ( + guildId: string, + options?: { limit?: number; after?: string }, +): Promise => { + const query = new URLSearchParams(); + if (options?.limit) query.append("limit", String(options.limit)); + if (options?.after) query.append("after", options.after); + const queryString = query.toString(); + const path = `/guilds/${guildId}/members${queryString ? `?${queryString}` : ""}`; + const res = await fetchDiscord(path); + return res.json(); +}; + +/** + * Returns a guild member object for the specified user. + * @param guildId The ID of the guild. + * @param userId The ID of the user. + * @returns A guild member object. + */ +export const getGuildMember = async ( + guildId: string, + userId: string, +): Promise => { + const path = `/guilds/${guildId}/members/${userId}`; + const res = await fetchDiscord(path); + return res.json(); +}; + +/** + * Modify attributes of a guild member. + * @param guildId The ID of the guild. + * @param userId The ID of the user. + * @param data The data to update. + * @returns The updated guild member. + */ +export const modifyGuildMember = async ( + guildId: string, + userId: string, + data: any, +): Promise => { + const path = `/guilds/${guildId}/members/${userId}`; + const res = await fetchDiscord(path, { + method: "PATCH", + body: JSON.stringify(data), + }); + return res.json(); +}; + +/** + * Remove a member from a guild. + * @param guildId The ID of the guild. + * @param userId The ID of the user to remove. + */ +export const removeGuildMember = async ( + guildId: string, + userId: string, +): Promise => { + const path = `/guilds/${guildId}/members/${userId}`; + await fetchDiscord(path, { + method: "DELETE", + }); +}; + +/** + * Returns a list of role objects for the guild. + * @param guildId The ID of the guild. + * @returns A list of role objects. + */ +export const getGuildRoles = async (guildId: string): Promise => { + const path = `/guilds/${guildId}/roles`; + const res = await fetchDiscord(path); + return res.json(); +}; + +/** + * Returns a list of ban objects for the users banned from this guild. + * @param guildId The ID of the guild. + * @returns A list of ban objects. + */ +export const getGuildBans = async (guildId: string): Promise => { + const path = `/guilds/${guildId}/bans`; + const res = await fetchDiscord(path); + return res.json(); +}; + +/** + * Create a guild ban, and optionally delete previous messages sent by the banned user. + * @param guildId The ID of the guild. + * @param userId The ID of the user to ban. + * @param options Options for the ban. + */ +export const createGuildBan = async ( + guildId: string, + userId: string, + options?: { delete_message_seconds?: number }, +): Promise => { + const path = `/guilds/${guildId}/bans/${userId}`; + await fetchDiscord(path, { + method: "PUT", + body: JSON.stringify(options), + }); +}; + +/** + * Remove the ban for a user. + * @param guildId The ID of the guild. + * @param userId The ID of the user to unban. + */ +export const removeGuildBan = async ( + guildId: string, + userId: string, +): Promise => { + const path = `/guilds/${guildId}/bans/${userId}`; + await fetchDiscord(path, { + method: "DELETE", + }); +}; + +/** + * Removes a role from a guild member. + * @param guildId The ID of the guild. + * @param userId The ID of the user. + * @param roleId The ID of the role to remove. + */ +export const removeGuildMemberRole = async ( + guildId: string, + userId: string, + roleId: string, +): Promise => { + const path = `/guilds/${guildId}/members/${userId}/roles/${roleId}`; + await fetchDiscord(path, { method: "DELETE" }); +}; diff --git a/src/discord/index.ts b/src/discord/index.ts new file mode 100644 index 0000000..a879e85 --- /dev/null +++ b/src/discord/index.ts @@ -0,0 +1,5 @@ +export * from "./audit-log"; +export * from "./core"; +export * from "./guild"; +export * from "./message"; +export * from "./poll"; diff --git a/src/discord/message.ts b/src/discord/message.ts new file mode 100644 index 0000000..ee4b989 --- /dev/null +++ b/src/discord/message.ts @@ -0,0 +1,219 @@ +import { fetchDiscord } from "./core"; + +/** + * Represents a message sent in a channel within Discord. + * This is a partial interface based on the Discord API documentation. + */ +export interface Message { + id: string; + channel_id: string; + author: any; // User object + content: string; + timestamp: string; // ISO8601 timestamp + edited_timestamp: string | null; // ISO8601 timestamp + tts: boolean; + mention_everyone: boolean; + mentions: any[]; // array of user objects + mention_roles: string[]; // array of role object ids + mention_channels?: any[]; // array of channel mention objects + attachments: any[]; // array of attachment objects + embeds: any[]; // array of embed objects + reactions?: any[]; // array of reaction objects + nonce?: number | string; + pinned: boolean; + webhook_id?: string; + type: number; + activity?: any; // message activity object + application?: any; // partial application object + application_id?: string; + flags?: number; + message_reference?: any; // message reference object + referenced_message?: Message | null; + interaction?: any; // message interaction object + thread?: any; // channel object + components?: any[]; // array of message components + sticker_items?: any[]; // array of message sticker item objects + stickers?: any[]; // array of sticker objects + position?: number; + role_subscription_data?: any; // role subscription data object + resolved?: any; +} + +/** + * Retrieves the messages in a channel. + * @param channelId The ID of the channel. + * @param options Options for fetching messages. + * @returns An array of message objects. + */ +export const getChannelMessages = async ( + channelId: string, + options?: { + around?: string; + before?: string; + after?: string; + limit?: number; + }, +): Promise => { + const query = new URLSearchParams(); + if (options?.around) query.append("around", options.around); + if (options?.before) query.append("before", options.before); + if (options?.after) query.append("after", options.after); + if (options?.limit) query.append("limit", String(options.limit)); + + const queryString = query.toString(); + const path = `/channels/${channelId}/messages${queryString ? `?${queryString}` : ""}`; + + const res = await fetchDiscord(path); + return res.json(); +}; + +/** + * Retrieves a specific message in the channel. + * @param channelId The ID of the channel. + * @param messageId The ID of the message. + * @returns A message object. + */ +export const getChannelMessage = async ( + channelId: string, + messageId: string, +): Promise => { + const path = `/channels/${channelId}/messages/${messageId}`; + const res = await fetchDiscord(path); + return res.json(); +}; + +export interface CreateMessageParams { + content?: string; + tts?: boolean; + embeds?: any[]; + allowed_mentions?: any; + message_reference?: any; + components?: any[]; + sticker_ids?: string[]; + files?: any[]; + payload_json?: string; + attachments?: any[]; + flags?: number; +} + +/** + * Post a message to a guild text or DM channel. + * @param channelId The ID of the channel. + * @param data The message data. + * @returns The created message object. + */ +export const createMessage = async ( + channelId: string, + data: CreateMessageParams, +): Promise => { + const path = `/channels/${channelId}/messages`; + const res = await fetchDiscord(path, { + method: "POST", + body: JSON.stringify(data), + }); + return res.json(); +}; + +export interface EditMessageParams { + content?: string; + embeds?: any[]; + flags?: number; + allowed_mentions?: any; + components?: any[]; + files?: any[]; + payload_json?: string; + attachments?: any[]; +} + +/** + * Edit a previously sent message. + * @param channelId The ID of the channel. + * @param messageId The ID of the message to edit. + * @param data The new message data. + * @returns The updated message object. + */ +export const editMessage = async ( + channelId: string, + messageId: string, + data: EditMessageParams, +): Promise => { + const path = `/channels/${channelId}/messages/${messageId}`; + const res = await fetchDiscord(path, { + method: "PATCH", + body: JSON.stringify(data), + }); + return res.json(); +}; + +/** + * Delete a message. + * @param channelId The ID of the channel. + * @param messageId The ID of the message to delete. + */ +export const deleteMessage = async ( + channelId: string, + messageId: string, +): Promise => { + const path = `/channels/${channelId}/messages/${messageId}`; + await fetchDiscord(path, { + method: "DELETE", + }); +}; + +/** + * Delete multiple messages in a single request. + * @param channelId The ID of the channel. + * @param messageIds An array of message IDs to delete. + */ +export const bulkDeleteMessages = async ( + channelId: string, + messageIds: string[], +): Promise => { + const path = `/channels/${channelId}/messages/bulk-delete`; + await fetchDiscord(path, { + method: "POST", + body: JSON.stringify({ messages: messageIds }), + }); +}; + +/** + * Create a reaction for the message. + * @param channelId The ID of the channel. + * @param messageId The ID of the message. + * @param emoji The emoji to react with. + */ +export const addReaction = async ( + channelId: string, + messageId: string, + emoji: string, +): Promise => { + const path = `/channels/${channelId}/messages/${messageId}/reactions/${encodeURIComponent( + emoji, + )}/@me`; + await fetchDiscord(path, { method: "PUT" }); +}; + +/** + * Get a list of users that reacted with this emoji. + * @param channelId The ID of the channel. + * @param messageId The ID of the message. + * @param emoji The emoji. + * @param options Options for fetching reactions. + * @returns A list of user objects. + */ +export const getReactions = async ( + channelId: string, + messageId: string, + emoji: string, + options?: { after?: string; limit?: number }, +): Promise => { + const query = new URLSearchParams(); + if (options?.after) query.append("after", options.after); + if (options?.limit) query.append("limit", String(options.limit ?? 100)); + const queryString = query.toString(); + const path = `/channels/${channelId}/messages/${messageId}/reactions/${encodeURIComponent( + emoji, + )}${queryString ? `?${queryString}` : ""}`; + const res = await fetchDiscord(path); + return res.json(); +}; diff --git a/src/discord/poll.ts b/src/discord/poll.ts new file mode 100644 index 0000000..fc666b4 --- /dev/null +++ b/src/discord/poll.ts @@ -0,0 +1,81 @@ +import { fetchDiscord } from "./core"; +import { Message } from "./message"; + +export interface PollMedia { + text?: string; + emoji?: any; // Partial Emoji +} + +export interface PollAnswer { + answer_id: number; + poll_media: PollMedia; +} + +export interface PollAnswerCount { + id: number; + count: number; + me_voted: boolean; +} + +export interface PollResults { + is_finalized: boolean; + answer_counts: PollAnswerCount[]; +} + +export interface Poll { + question: PollMedia; + answers: PollAnswer[]; + expiry: string | null; // ISO8601 timestamp + allow_multiselect: boolean; + layout_type: number; + results?: PollResults; +} + +export interface PollCreateRequest { + question: PollMedia; + answers: PollAnswer[]; + duration?: number; + allow_multiselect?: boolean; + layout_type?: number; +} + +/** + * Get a list of users that voted for a specific answer. + * @param channelId The ID of the channel. + * @param messageId The ID of the message with the poll. + * @param answerId The ID of the answer. + * @param options Options for fetching voters. + * @returns A list of user objects. + */ +export const getAnswerVoters = async ( + channelId: string, + messageId: string, + answerId: number, + options?: { after?: string; limit?: number }, +): Promise<{ users: any[] }> => { + const query = new URLSearchParams(); + if (options?.after) query.append("after", options.after); + if (options?.limit) query.append("limit", String(options.limit)); + const queryString = query.toString(); + + const path = `/channels/${channelId}/polls/${messageId}/answers/${answerId}${queryString ? `?${queryString}` : ""}`; + const res = await fetchDiscord(path); + return res.json(); +}; + +/** + * Immediately ends a poll. + * @param channelId The ID of the channel. + * @param messageId The ID of the message with the poll. + * @returns The updated message object. + */ +export const endPoll = async ( + channelId: string, + messageId: string, +): Promise => { + const path = `/channels/${channelId}/polls/${messageId}/expire`; + const res = await fetchDiscord(path, { + method: "POST", + }); + return res.json(); +};