From 5401b5192907b8de8bb91749d30529a9cda588b8 Mon Sep 17 00:00:00 2001 From: Elysia <71698422+aiko-chan-ai@users.noreply.github.com> Date: Mon, 22 Jan 2024 19:12:59 +0700 Subject: [PATCH] feat: ClientUserSettingManager --- src/client/Client.js | 7 + src/client/websocket/handlers/READY.js | 3 + .../handlers/USER_SETTINGS_UPDATE.js | 5 + src/client/websocket/handlers/index.js | 1 + src/index.js | 1 - src/managers/ClientUserSettingManager.js | 371 ++++++++++++++++++ .../GuildApplicationCommandManager.js | 28 -- src/structures/Guild.js | 7 - src/structures/Presence.js | 2 +- typings/index.d.ts | 96 ++++- 10 files changed, 465 insertions(+), 56 deletions(-) create mode 100644 src/client/websocket/handlers/USER_SETTINGS_UPDATE.js create mode 100644 src/managers/ClientUserSettingManager.js delete mode 100644 src/managers/GuildApplicationCommandManager.js diff --git a/src/client/Client.js b/src/client/Client.js index febaee7..d3375b4 100644 --- a/src/client/Client.js +++ b/src/client/Client.js @@ -12,6 +12,7 @@ const { Error, TypeError, RangeError } = require('../errors'); const BaseGuildEmojiManager = require('../managers/BaseGuildEmojiManager'); const BillingManager = require('../managers/BillingManager'); const ChannelManager = require('../managers/ChannelManager'); +const ClientUserSettingManager = require('../managers/ClientUserSettingManager'); const GuildManager = require('../managers/GuildManager'); const PresenceManager = require('../managers/PresenceManager'); const RelationshipManager = require('../managers/RelationshipManager'); @@ -188,6 +189,12 @@ class Client extends BaseClient { */ this.billing = new BillingManager(this); + /** + * All of the settings {@link Object} + * @type {ClientUserSettingManager} + */ + this.settings = new ClientUserSettingManager(this); + Object.defineProperty(this, 'token', { writable: true }); if (!this.token && 'DISCORD_TOKEN' in process.env) { /** diff --git a/src/client/websocket/handlers/READY.js b/src/client/websocket/handlers/READY.js index dfb0c41..0a9481f 100644 --- a/src/client/websocket/handlers/READY.js +++ b/src/client/websocket/handlers/READY.js @@ -35,6 +35,9 @@ module.exports = (client, { d: data }, shard) => { // Relationship client.relationships._setup(data.relationships); + // ClientSetting + client.settings._patch(data.user_settings); + Promise.all( largeGuilds.map(async (guild, index) => { client.ws.broadcast({ diff --git a/src/client/websocket/handlers/USER_SETTINGS_UPDATE.js b/src/client/websocket/handlers/USER_SETTINGS_UPDATE.js new file mode 100644 index 00000000..b178c14 --- /dev/null +++ b/src/client/websocket/handlers/USER_SETTINGS_UPDATE.js @@ -0,0 +1,5 @@ +'use strict'; + +module.exports = (client, { d: data }) => { + client.settings._patch(data); +}; diff --git a/src/client/websocket/handlers/index.js b/src/client/websocket/handlers/index.js index 5281ef3..709d68f 100644 --- a/src/client/websocket/handlers/index.js +++ b/src/client/websocket/handlers/index.js @@ -73,6 +73,7 @@ const handlers = Object.fromEntries([ ['CALL_CREATE', require('./CALL_CREATE')], ['CALL_UPDATE', require('./CALL_UPDATE')], ['CALL_DELETE', require('./CALL_DELETE')], + ['USER_SETTINGS_UPDATE', require('./USER_SETTINGS_UPDATE')], ]); module.exports = handlers; diff --git a/src/index.js b/src/index.js index c3ac8c4..a085f2c 100644 --- a/src/index.js +++ b/src/index.js @@ -45,7 +45,6 @@ exports.CachedManager = require('./managers/CachedManager'); exports.ChannelManager = require('./managers/ChannelManager'); exports.ClientVoiceManager = require('./client/voice/ClientVoiceManager'); exports.DataManager = require('./managers/DataManager'); -exports.GuildApplicationCommandManager = require('./managers/GuildApplicationCommandManager'); exports.GuildBanManager = require('./managers/GuildBanManager'); exports.GuildChannelManager = require('./managers/GuildChannelManager'); exports.GuildEmojiManager = require('./managers/GuildEmojiManager'); diff --git a/src/managers/ClientUserSettingManager.js b/src/managers/ClientUserSettingManager.js new file mode 100644 index 00000000..6a1ae99 --- /dev/null +++ b/src/managers/ClientUserSettingManager.js @@ -0,0 +1,371 @@ +'use strict'; + +const { Collection } = require('@discordjs/collection'); +const BaseManager = require('./BaseManager'); +const { TypeError } = require('../errors/DJSError'); +const { CustomStatus } = require('../structures/RichPresence'); +const { ActivityTypes } = require('../util/Constants'); + +/** + * Manages API methods for users and stores their cache. + * @extends {BaseManager} + * @see {@link https://luna.gitlab.io/discord-unofficial-docs/user_settings.html} + */ +class ClientUserSettingManager extends BaseManager { + #rawSetting = {}; + constructor(client) { + super(client); + /** + * WHO CAN ADD YOU AS A FRIEND ? + * @type {?object} + * @see {@link https://luna.gitlab.io/discord-unofficial-docs/user_settings.html#friend-source-flags-structure} + */ + this.addFriendFrom = { + all: null, + mutual_friends: null, + mutual_guilds: null, + }; + } + /** + * Patch data file + * https://luna.gitlab.io/discord-unofficial-docs/docs/user_settings + * @private + * @param {Object} data Raw Data to patch + */ + _patch(data = {}) { + this.#rawSetting = Object.assign(this.#rawSetting, data); + if ('locale' in data) { + /** + * The user's chosen language option + * @type {?string} + * @see {@link https://discord.com/developers/docs/reference#locales} + */ + this.locale = data.locale; + } + if ('show_current_game' in data) { + /** + * Show playing status for detected/added games + * Setting => ACTIVITY SETTINGS => Activity Status => Display current activity as a status message + * @type {?boolean} + */ + this.activityDisplay = data.show_current_game; + } + if ('default_guilds_restricted' in data) { + /** + * Allow DMs from guild members by default on guild join + * @type {?boolean} + */ + this.allowDMsFromGuild = data.default_guilds_restricted; + } + if ('inline_attachment_media' in data) { + /** + * Display images and video when uploaded directly + * @type {?boolean} + */ + this.displayImage = data.inline_attachment_media; + } + if ('inline_embed_media' in data) { + /** + * Display images and video when linked + * @type {?boolean} + */ + this.linkedImageDisplay = data.inline_embed_media; + } + if ('gif_auto_play' in data) { + /** + * Play GIFs without hovering over them + * Setting => APP SETTINGS => Accessibility => Automatically play GIFs when Discord is focused. + * @type {?boolean} + */ + this.autoplayGIF = data.gif_auto_play; + } + if ('render_embeds' in data) { + /** + * Show embeds and preview website links pasted into chat + * @type {?boolean} + */ + this.previewLink = data.render_embeds; + } + if ('animate_emoji' in data) { + /** + * Play animated emoji without hovering over them + * Setting => APP SETTINGS => Accessibility => Play Animated Emojis + * @type {?boolean} + */ + this.animatedEmoji = data.animate_emoji; + } + if ('enable_tts_command' in data) { + /** + * Enable /tts command and playback + * Setting => APP SETTINGS => Accessibility => Text-to-speech => Allow playback + * @type {?boolean} + */ + this.allowTTS = data.enable_tts_command; + } + if ('message_display_compact' in data) { + /** + * Use compact mode + * Setting => APP SETTINGS => Appearance => Message Display => Compact Mode + * @type {?boolean} + */ + this.compactMode = data.message_display_compact; + } + if ('convert_emoticons' in data) { + /** + * Convert "old fashioned" emoticons to emojis + * Setting => APP SETTINGS => Text & Images => Emoji => Convert Emoticons + * @type {?boolean} + */ + this.convertEmoticons = data.convert_emoticons; + } + if ('explicit_content_filter' in data) { + /** + * Content filter level + * + * * `0`: Off + * * `1`: Friends excluded + * * `2`: Scan everyone + * + * @type {?number} + */ + this.DMScanLevel = data.explicit_content_filter; + } + if ('theme' in data) { + /** + * Client theme + * Setting => APP SETTINGS => Appearance => Theme + * * `dark` + * * `light` + * + * @type {?string} + */ + this.theme = data.theme; + } + if ('developer_mode' in data) { + /** + * Show the option to copy ids in right click menus + * @type {?boolean} + */ + this.developerMode = data.developer_mode; + } + if ('afk_timeout' in data) { + /** + * How many seconds being idle before the user is marked as "AFK"; this handles when push notifications are sent + * @type {?number} + */ + this.afkTimeout = data.afk_timeout; + } + if ('animate_stickers' in data) { + /** + * When stickers animate + * + * * `0`: Always + * * `1`: On hover/focus + * * `2`: Never + * + * @type {?number} + */ + this.stickerAnimationMode = data.animate_stickers; + } + if ('render_reactions' in data) { + /** + * Display reactions + * Setting => APP SETTINGS => Text & Images => Emoji => Show emoji reactions + * @type {?boolean} + */ + this.showEmojiReactions = data.render_reactions; + } + if ('status' in data) { + this.client.presence.status = data.status; + if (!('custom_status' in data)) { + this.client.emit('debug', '[SETTING] Sync status'); + this.client.user.setStatus(data.status); + } + } + if ('custom_status' in data) { + this.customStatus = data.custom_status; + const activities = this.client.presence.activities.filter( + a => ![ActivityTypes.CUSTOM, 'CUSTOM'].includes(a.type), + ); + if (data.custom_status) { + const custom = new CustomStatus(); + custom.setState(data.custom_status.text); + let emoji; + if (data.custom_status.emoji_id) { + emoji = this.client.emojis.cache.get(data.custom_status.emoji_id); + } else if (data.custom_status.emoji_name) { + emoji = `:${data.custom_status.emoji_name}:`; + } + if (emoji) custom.setEmoji(emoji); + activities.push(custom); + } + this.client.emit('debug', '[SETTING] Sync activities & status'); + this.client.user.setPresence({ activities }); + } + if ('friend_source_flags' in data) { + // Todo + } + if ('restricted_guilds' in data) { + /** + * Disable Direct Message from servers + * @type {Collection} + */ + this.disableDMfromGuilds = new Collection( + data.restricted_guilds.map(guildId => [guildId, this.client.guilds.cache.get(guildId)]), + ); + } + } + + /** + * Raw data + * @type {Object} + */ + get raw() { + return this.#rawSetting; + } + + async fetch() { + const data = await this.client.api.users('@me').settings.get(); + this._patch(data); + return this; + } + + /** + * Edit data + * @param {any} data Data to edit + */ + async edit(data) { + const res = await this.client.api.users('@me').settings.patch({ data }); + this._patch(res); + return this; + } + + /** + * Toggle compact mode + * @returns {Promise} + */ + toggleCompactMode() { + return this.edit({ message_display_compact: !this.compactMode }); + } + /** + * Discord Theme + * @param {string} value Theme to set (dark | light) + * @returns {Promise} + */ + setTheme(value) { + const validValues = ['dark', 'light']; + if (!validValues.includes(value)) { + throw new TypeError('INVALID_TYPE', 'value', 'dark | light', true); + } + return this.edit({ theme: value }); + } + + /** + * CustomStatus Object + * @typedef {Object} CustomStatusOption + * @property {string | null} text Text to set + * @property {string | null} status The status to set: 'online', 'idle', 'dnd', 'invisible' or null. + * @property {EmojiResolvable | null} emoji UnicodeEmoji, DiscordEmoji, or null. + * @property {number | null} expires The number of seconds until the status expires, or null. + */ + + /** + * Set custom status + * @param {?CustomStatus | CustomStatusOption} options CustomStatus + * @returns {Promise} + */ + setCustomStatus(options) { + if (typeof options !== 'object') { + return this.edit({ custom_status: null }); + } else if (options instanceof CustomStatus) { + options = options.toJSON(); + let data = { + emoji_name: null, + expires_at: null, + text: null, + }; + if (typeof options.state === 'string') { + data.text = options.state; + } + if (options.emoji) { + if (options.emoji?.id) { + data.emoji_name = options.emoji?.name; + data.emoji_id = options.emoji?.id; + } else { + data.emoji_name = typeof options.emoji?.name === 'string' ? options.emoji?.name : null; + } + } + return this.edit({ custom_status: data }); + } else { + let data = { + emoji_name: null, + expires_at: null, + text: null, + }; + if (typeof options.text === 'string') { + if (options.text.length > 128) { + throw new RangeError('[INVALID_VALUE] Custom status text must be less than 128 characters'); + } + data.text = options.text; + } + if (options.emoji) { + const emoji = this.client.emojis.resolve(options.emoji); + if (emoji) { + data.emoji_name = emoji.name; + data.emoji_id = emoji.id; + } else { + data.emoji_name = typeof options.emoji === 'string' ? options.emoji : null; + } + } + if (typeof options.expires === 'number') { + if (options.expires < Date.now()) { + throw new RangeError(`[INVALID_VALUE] Custom status expiration must be greater than ${Date.now()}`); + } + data.expires_at = new Date(options.expires).toISOString(); + } + if (['online', 'idle', 'dnd', 'invisible'].includes(options.status)) this.edit({ status: options.status }); + return this.edit({ custom_status: data }); + } + } + + /** + * Restricted guilds setting + * @param {boolean} status Restricted status + * @returns {Promise} + */ + restrictedGuilds(status) { + if (typeof status !== 'boolean') { + throw new TypeError('INVALID_TYPE', 'status', 'boolean', true); + } + return this.edit({ + default_guilds_restricted: status, + restricted_guilds: status ? this.client.guilds.cache.map(v => v.id) : [], + }); + } + /** + * Add a guild to the list of restricted guilds. + * @param {GuildIDResolve} guildId The guild to add + * @returns {Promise} + */ + addRestrictedGuild(guildId) { + const temp = Object.assign( + [], + this.disableDMfromServer.map((v, k) => k), + ); + if (temp.includes(guildId)) throw new Error('Guild is already restricted'); + temp.push(guildId); + return this.edit({ restricted_guilds: temp }); + } + + /** + * Remove a guild from the list of restricted guilds. + * @param {GuildIDResolve} guildId The guild to remove + * @returns {Promise} + */ + removeRestrictedGuild(guildId) { + if (!this.disableDMfromServer.delete(guildId)) throw new Error('Guild is already restricted'); + return this.edit({ restricted_guilds: this.disableDMfromServer.map((v, k) => k) }); + } +} + +module.exports = ClientUserSettingManager; diff --git a/src/managers/GuildApplicationCommandManager.js b/src/managers/GuildApplicationCommandManager.js deleted file mode 100644 index 97fea5e..00000000 --- a/src/managers/GuildApplicationCommandManager.js +++ /dev/null @@ -1,28 +0,0 @@ -'use strict'; - -const ApplicationCommandManager = require('./ApplicationCommandManager'); -const ApplicationCommandPermissionsManager = require('./ApplicationCommandPermissionsManager'); - -/** - * An extension for guild-specific application commands. - * @extends {ApplicationCommandManager} - */ -class GuildApplicationCommandManager extends ApplicationCommandManager { - constructor(guild, iterable) { - super(guild.client, iterable); - - /** - * The guild that this manager belongs to - * @type {Guild} - */ - this.guild = guild; - - /** - * The manager for permissions of arbitrary commands on this guild - * @type {ApplicationCommandPermissionsManager} - */ - this.permissions = new ApplicationCommandPermissionsManager(this); - } -} - -module.exports = GuildApplicationCommandManager; diff --git a/src/structures/Guild.js b/src/structures/Guild.js index 0de5780..7bf2e4d 100644 --- a/src/structures/Guild.js +++ b/src/structures/Guild.js @@ -11,7 +11,6 @@ const Webhook = require('./Webhook'); const WelcomeScreen = require('./WelcomeScreen'); const { Error } = require('../errors'); const AutoModerationRuleManager = require('../managers/AutoModerationRuleManager'); -const GuildApplicationCommandManager = require('../managers/GuildApplicationCommandManager'); const GuildBanManager = require('../managers/GuildBanManager'); const GuildChannelManager = require('../managers/GuildChannelManager'); const GuildEmojiManager = require('../managers/GuildEmojiManager'); @@ -58,12 +57,6 @@ class Guild extends AnonymousGuild { constructor(client, data) { super(client, data, false); - /** - * A manager of the application commands belonging to this guild - * @type {GuildApplicationCommandManager} - */ - this.commands = new GuildApplicationCommandManager(this); - /** * A manager of the members belonging to this guild * @type {GuildMemberManager} diff --git a/src/structures/Presence.js b/src/structures/Presence.js index b31d1e7..c9724b3 100644 --- a/src/structures/Presence.js +++ b/src/structures/Presence.js @@ -90,7 +90,7 @@ class Presence extends Base { if ('activities' in data) { /** - * The activities of this presence + * The activities of this presence (Always `Activity[]` if not ClientUser) * @type {Activity[]|CustomStatus[]|RichPresence[]|SpotifyRPC[]} */ this.activities = data.activities.map(activity => { diff --git a/typings/index.d.ts b/typings/index.d.ts index 2be280b..d0c099c 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -788,6 +788,7 @@ export class Client extends BaseClient { public voiceStates: VoiceStateManager; public presences: PresenceManager; public billing: BillingManager; + public settings: ClientUserSettingManager; public destroy(): void; public fetchGuildPreview(guild: GuildResolvable): Promise; public fetchInvite(invite: InviteResolvable, options?: ClientFetchInviteOptions): Promise; @@ -1174,7 +1175,6 @@ export class Guild extends AnonymousGuild { public available: boolean; public bans: GuildBanManager; public channels: GuildChannelManager; - public commands: GuildApplicationCommandManager; public defaultMessageNotifications: DefaultMessageNotificationLevel | number; /** @deprecated This will be removed in the next major version, see https://github.com/discordjs/discord.js/issues/7091 */ public deleted: boolean; @@ -3678,8 +3678,8 @@ export class ApplicationCommandPermissionsManager< GuildType, CommandIdType, > extends BaseManager { - private constructor(manager: ApplicationCommandManager | GuildApplicationCommandManager | ApplicationCommand); - private manager: ApplicationCommandManager | GuildApplicationCommandManager | ApplicationCommand; + private constructor(manager: ApplicationCommandManager | ApplicationCommand); + private manager: ApplicationCommandManager | ApplicationCommand; public client: Client; public commandId: CommandIdType; @@ -3767,22 +3767,80 @@ export class UserNoteManager extends BaseManager { export type FetchGuildApplicationCommandFetchOptions = Omit; -export class GuildApplicationCommandManager extends ApplicationCommandManager { - private constructor(guild: Guild, iterable?: Iterable); - public guild: Guild; - public create(command: ApplicationCommandDataResolvable): Promise; - public delete(command: ApplicationCommandResolvable): Promise; - public edit( - command: ApplicationCommandResolvable, - data: Partial, - ): Promise; - public fetch(id: Snowflake, options?: FetchGuildApplicationCommandFetchOptions): Promise; - public fetch(options: FetchGuildApplicationCommandFetchOptions): Promise>; - public fetch( - id?: undefined, - options?: FetchGuildApplicationCommandFetchOptions, - ): Promise>; - public set(commands: ApplicationCommandDataResolvable[]): Promise>; +export class ClientUserSettingManager extends BaseManager { + private constructor(client: Client); + public readonly raw: RawUserSettingsData; + public locale?: string; + public activityDisplay?: boolean; + public allowDMsFromGuild?: boolean; + public displayImage?: boolean; + public linkedImageDisplay?: boolean; + public autoplayGIF?: boolean; + public previewLink?: boolean; + public animatedEmoji?: boolean; + public allowTTS?: boolean; + public compactMode?: boolean; + public convertEmoticons?: boolean; + public DMScanLevel?: 0 | 1 | 2; + public theme?: 'dark' | 'light'; + public developerMode?: boolean; + public afkTimeout?: number; + public stickerAnimationMode?: 0 | 1 | 2; + public showEmojiReactions?: boolean; + public disableDMfromGuilds: Collection; + public fetch(): Promise; + public edit(data: Partial): Promise; + public toggleCompactMode(): Promise; + public setTheme(value: 'dark' | 'light'): Promise; + public setCustomStatus(value?: CustomStatusOption | CustomStatus): Promise; + public restrictedGuilds(status: boolean): Promise; + public addRestrictedGuild(guildId: GuildResolvable): Promise; + public removeRestrictedGuild(guildId: GuildResolvable): Promise; +} + +export interface CustomStatusOption { + text?: string | null; + expires_at?: string | null; + emoji?: EmojiIdentifierResolvable | null; + status?: PresenceStatusData | null; +} + +/** + * @see {@link https://luna.gitlab.io/discord-unofficial-docs/user_settings.html} + */ +export interface RawUserSettingsData { + afk_timeout?: number; + allow_accessibility_detection?: boolean; + animate_emoji?: boolean; + animate_stickers?: number; + contact_sync_enabled?: boolean; + convert_emoticons?: boolean; + custom_status?: { text?: string; expires_at?: string | null; emoji_name?: string; emoji_id?: Snowflake | null }; + default_guilds_restricted?: boolean; + detect_platform_accounts?: boolean; + developer_mode?: boolean; + disable_games_tab?: boolean; + enable_tts_command?: boolean; + explicit_content_filter?: number; + friend_discovery_flags?: number; + friend_source_flags?: { all?: boolean; mutual_friends?: boolean; mututal_guilds?: boolean }; + gif_auto_play?: boolean; + guild_folders?: { id?: Snowflake; guild_ids?: Snowflake[]; name?: string }[]; + guild_positions?: number[]; + inline_attachment_media?: boolean; + inline_embed_media?: boolean; + locale?: string; + message_display_compact?: boolean; + native_phone_integration_enabled?: boolean; + render_embeds?: boolean; + render_reactions?: boolean; + restricted_guilds?: any[]; + show_current_game?: boolean; + status?: PresenceStatusData; + stream_notifications_enabled?: boolean; + theme?: 'dark' | 'light'; + timezone_offset?: number; + view_nsfw_guilds?: boolean; } export type MappedGuildChannelTypes = EnumValueMapped<