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<