feat: ClientUserSettingManager

This commit is contained in:
Elysia 2024-01-22 19:12:59 +07:00
parent 59e8332987
commit 5401b51929
10 changed files with 465 additions and 56 deletions

View File

@ -12,6 +12,7 @@ const { Error, TypeError, RangeError } = require('../errors');
const BaseGuildEmojiManager = require('../managers/BaseGuildEmojiManager'); const BaseGuildEmojiManager = require('../managers/BaseGuildEmojiManager');
const BillingManager = require('../managers/BillingManager'); const BillingManager = require('../managers/BillingManager');
const ChannelManager = require('../managers/ChannelManager'); const ChannelManager = require('../managers/ChannelManager');
const ClientUserSettingManager = require('../managers/ClientUserSettingManager');
const GuildManager = require('../managers/GuildManager'); const GuildManager = require('../managers/GuildManager');
const PresenceManager = require('../managers/PresenceManager'); const PresenceManager = require('../managers/PresenceManager');
const RelationshipManager = require('../managers/RelationshipManager'); const RelationshipManager = require('../managers/RelationshipManager');
@ -188,6 +189,12 @@ class Client extends BaseClient {
*/ */
this.billing = new BillingManager(this); this.billing = new BillingManager(this);
/**
* All of the settings {@link Object}
* @type {ClientUserSettingManager}
*/
this.settings = new ClientUserSettingManager(this);
Object.defineProperty(this, 'token', { writable: true }); Object.defineProperty(this, 'token', { writable: true });
if (!this.token && 'DISCORD_TOKEN' in process.env) { if (!this.token && 'DISCORD_TOKEN' in process.env) {
/** /**

View File

@ -35,6 +35,9 @@ module.exports = (client, { d: data }, shard) => {
// Relationship // Relationship
client.relationships._setup(data.relationships); client.relationships._setup(data.relationships);
// ClientSetting
client.settings._patch(data.user_settings);
Promise.all( Promise.all(
largeGuilds.map(async (guild, index) => { largeGuilds.map(async (guild, index) => {
client.ws.broadcast({ client.ws.broadcast({

View File

@ -0,0 +1,5 @@
'use strict';
module.exports = (client, { d: data }) => {
client.settings._patch(data);
};

View File

@ -73,6 +73,7 @@ const handlers = Object.fromEntries([
['CALL_CREATE', require('./CALL_CREATE')], ['CALL_CREATE', require('./CALL_CREATE')],
['CALL_UPDATE', require('./CALL_UPDATE')], ['CALL_UPDATE', require('./CALL_UPDATE')],
['CALL_DELETE', require('./CALL_DELETE')], ['CALL_DELETE', require('./CALL_DELETE')],
['USER_SETTINGS_UPDATE', require('./USER_SETTINGS_UPDATE')],
]); ]);
module.exports = handlers; module.exports = handlers;

View File

@ -45,7 +45,6 @@ exports.CachedManager = require('./managers/CachedManager');
exports.ChannelManager = require('./managers/ChannelManager'); exports.ChannelManager = require('./managers/ChannelManager');
exports.ClientVoiceManager = require('./client/voice/ClientVoiceManager'); exports.ClientVoiceManager = require('./client/voice/ClientVoiceManager');
exports.DataManager = require('./managers/DataManager'); exports.DataManager = require('./managers/DataManager');
exports.GuildApplicationCommandManager = require('./managers/GuildApplicationCommandManager');
exports.GuildBanManager = require('./managers/GuildBanManager'); exports.GuildBanManager = require('./managers/GuildBanManager');
exports.GuildChannelManager = require('./managers/GuildChannelManager'); exports.GuildChannelManager = require('./managers/GuildChannelManager');
exports.GuildEmojiManager = require('./managers/GuildEmojiManager'); exports.GuildEmojiManager = require('./managers/GuildEmojiManager');

View File

@ -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
* <info>Setting => ACTIVITY SETTINGS => Activity Status => Display current activity as a status message</info>
* @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
* <info>Setting => APP SETTINGS => Accessibility => Automatically play GIFs when Discord is focused.</info>
* @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
* <info>Setting => APP SETTINGS => Accessibility => Play Animated Emojis</info>
* @type {?boolean}
*/
this.animatedEmoji = data.animate_emoji;
}
if ('enable_tts_command' in data) {
/**
* Enable /tts command and playback
* <info>Setting => APP SETTINGS => Accessibility => Text-to-speech => Allow playback</info>
* @type {?boolean}
*/
this.allowTTS = data.enable_tts_command;
}
if ('message_display_compact' in data) {
/**
* Use compact mode
* <info>Setting => APP SETTINGS => Appearance => Message Display => Compact Mode</info>
* @type {?boolean}
*/
this.compactMode = data.message_display_compact;
}
if ('convert_emoticons' in data) {
/**
* Convert "old fashioned" emoticons to emojis
* <info>Setting => APP SETTINGS => Text & Images => Emoji => Convert Emoticons</info>
* @type {?boolean}
*/
this.convertEmoticons = data.convert_emoticons;
}
if ('explicit_content_filter' in data) {
/**
* Content filter level
* <info>
* * `0`: Off
* * `1`: Friends excluded
* * `2`: Scan everyone
* </info>
* @type {?number}
*/
this.DMScanLevel = data.explicit_content_filter;
}
if ('theme' in data) {
/**
* Client theme
* <info>Setting => APP SETTINGS => Appearance => Theme
* * `dark`
* * `light`
* </info>
* @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
* <info>
* * `0`: Always
* * `1`: On hover/focus
* * `2`: Never
* </info>
* @type {?number}
*/
this.stickerAnimationMode = data.animate_stickers;
}
if ('render_reactions' in data) {
/**
* Display reactions
* <info>Setting => APP SETTINGS => Text & Images => Emoji => Show emoji reactions</info>
* @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<Snowflake, Guild>}
*/
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<this>}
*/
toggleCompactMode() {
return this.edit({ message_display_compact: !this.compactMode });
}
/**
* Discord Theme
* @param {string} value Theme to set (dark | light)
* @returns {Promise<this>}
*/
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<this>}
*/
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;

View File

@ -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;

View File

@ -11,7 +11,6 @@ const Webhook = require('./Webhook');
const WelcomeScreen = require('./WelcomeScreen'); const WelcomeScreen = require('./WelcomeScreen');
const { Error } = require('../errors'); const { Error } = require('../errors');
const AutoModerationRuleManager = require('../managers/AutoModerationRuleManager'); const AutoModerationRuleManager = require('../managers/AutoModerationRuleManager');
const GuildApplicationCommandManager = require('../managers/GuildApplicationCommandManager');
const GuildBanManager = require('../managers/GuildBanManager'); const GuildBanManager = require('../managers/GuildBanManager');
const GuildChannelManager = require('../managers/GuildChannelManager'); const GuildChannelManager = require('../managers/GuildChannelManager');
const GuildEmojiManager = require('../managers/GuildEmojiManager'); const GuildEmojiManager = require('../managers/GuildEmojiManager');
@ -58,12 +57,6 @@ class Guild extends AnonymousGuild {
constructor(client, data) { constructor(client, data) {
super(client, data, false); 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 * A manager of the members belonging to this guild
* @type {GuildMemberManager} * @type {GuildMemberManager}

View File

@ -90,7 +90,7 @@ class Presence extends Base {
if ('activities' in data) { if ('activities' in data) {
/** /**
* The activities of this presence * The activities of this presence (Always `Activity[]` if not ClientUser)
* @type {Activity[]|CustomStatus[]|RichPresence[]|SpotifyRPC[]} * @type {Activity[]|CustomStatus[]|RichPresence[]|SpotifyRPC[]}
*/ */
this.activities = data.activities.map(activity => { this.activities = data.activities.map(activity => {

96
typings/index.d.ts vendored
View File

@ -788,6 +788,7 @@ export class Client<Ready extends boolean = boolean> extends BaseClient {
public voiceStates: VoiceStateManager; public voiceStates: VoiceStateManager;
public presences: PresenceManager; public presences: PresenceManager;
public billing: BillingManager; public billing: BillingManager;
public settings: ClientUserSettingManager;
public destroy(): void; public destroy(): void;
public fetchGuildPreview(guild: GuildResolvable): Promise<GuildPreview>; public fetchGuildPreview(guild: GuildResolvable): Promise<GuildPreview>;
public fetchInvite(invite: InviteResolvable, options?: ClientFetchInviteOptions): Promise<Invite>; public fetchInvite(invite: InviteResolvable, options?: ClientFetchInviteOptions): Promise<Invite>;
@ -1174,7 +1175,6 @@ export class Guild extends AnonymousGuild {
public available: boolean; public available: boolean;
public bans: GuildBanManager; public bans: GuildBanManager;
public channels: GuildChannelManager; public channels: GuildChannelManager;
public commands: GuildApplicationCommandManager;
public defaultMessageNotifications: DefaultMessageNotificationLevel | number; public defaultMessageNotifications: DefaultMessageNotificationLevel | number;
/** @deprecated This will be removed in the next major version, see https://github.com/discordjs/discord.js/issues/7091 */ /** @deprecated This will be removed in the next major version, see https://github.com/discordjs/discord.js/issues/7091 */
public deleted: boolean; public deleted: boolean;
@ -3678,8 +3678,8 @@ export class ApplicationCommandPermissionsManager<
GuildType, GuildType,
CommandIdType, CommandIdType,
> extends BaseManager { > extends BaseManager {
private constructor(manager: ApplicationCommandManager | GuildApplicationCommandManager | ApplicationCommand); private constructor(manager: ApplicationCommandManager | ApplicationCommand);
private manager: ApplicationCommandManager | GuildApplicationCommandManager | ApplicationCommand; private manager: ApplicationCommandManager | ApplicationCommand;
public client: Client; public client: Client;
public commandId: CommandIdType; public commandId: CommandIdType;
@ -3767,22 +3767,80 @@ export class UserNoteManager extends BaseManager {
export type FetchGuildApplicationCommandFetchOptions = Omit<FetchApplicationCommandOptions, 'guildId'>; export type FetchGuildApplicationCommandFetchOptions = Omit<FetchApplicationCommandOptions, 'guildId'>;
export class GuildApplicationCommandManager extends ApplicationCommandManager<ApplicationCommand, {}, Guild> { export class ClientUserSettingManager extends BaseManager {
private constructor(guild: Guild, iterable?: Iterable<RawApplicationCommandData>); private constructor(client: Client);
public guild: Guild; public readonly raw: RawUserSettingsData;
public create(command: ApplicationCommandDataResolvable): Promise<ApplicationCommand>; public locale?: string;
public delete(command: ApplicationCommandResolvable): Promise<ApplicationCommand | null>; public activityDisplay?: boolean;
public edit( public allowDMsFromGuild?: boolean;
command: ApplicationCommandResolvable, public displayImage?: boolean;
data: Partial<ApplicationCommandDataResolvable>, public linkedImageDisplay?: boolean;
): Promise<ApplicationCommand>; public autoplayGIF?: boolean;
public fetch(id: Snowflake, options?: FetchGuildApplicationCommandFetchOptions): Promise<ApplicationCommand>; public previewLink?: boolean;
public fetch(options: FetchGuildApplicationCommandFetchOptions): Promise<Collection<Snowflake, ApplicationCommand>>; public animatedEmoji?: boolean;
public fetch( public allowTTS?: boolean;
id?: undefined, public compactMode?: boolean;
options?: FetchGuildApplicationCommandFetchOptions, public convertEmoticons?: boolean;
): Promise<Collection<Snowflake, ApplicationCommand>>; public DMScanLevel?: 0 | 1 | 2;
public set(commands: ApplicationCommandDataResolvable[]): Promise<Collection<Snowflake, ApplicationCommand>>; public theme?: 'dark' | 'light';
public developerMode?: boolean;
public afkTimeout?: number;
public stickerAnimationMode?: 0 | 1 | 2;
public showEmojiReactions?: boolean;
public disableDMfromGuilds: Collection<Snowflake, Guild>;
public fetch(): Promise<this>;
public edit(data: Partial<RawUserSettingsData>): Promise<this>;
public toggleCompactMode(): Promise<this>;
public setTheme(value: 'dark' | 'light'): Promise<this>;
public setCustomStatus(value?: CustomStatusOption | CustomStatus): Promise<this>;
public restrictedGuilds(status: boolean): Promise<void>;
public addRestrictedGuild(guildId: GuildResolvable): Promise<void>;
public removeRestrictedGuild(guildId: GuildResolvable): Promise<void>;
}
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< export type MappedGuildChannelTypes = EnumValueMapped<