diff --git a/client/src/modules/discordapi.js b/client/src/modules/discordapi.js deleted file mode 100644 index 40a86b83..00000000 --- a/client/src/modules/discordapi.js +++ /dev/null @@ -1,335 +0,0 @@ -/** - * BetterDiscord Discord API - * Copyright (c) 2015-present Jiiks/JsSucks - https://github.com/Jiiks / https://github.com/JsSucks - * All rights reserved. - * https://betterdiscord.net - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. -*/ - -import { List } from 'structs'; -import { User, Channel, Guild, Message } from 'discordstructs'; -import Reflection from './reflection/index'; - -export const Modules = { - _getModule(name) { - const foundModule = Reflection.module.byName(name); - if (!foundModule) return null; - delete this[name]; - return this[name] = foundModule; - }, - - get ChannelSelector() { return this._getModule('ChannelSelector'); }, - get MessageActions() { return this._getModule('MessageActions'); }, - get MessageParser() { return this._getModule('MessageParser'); }, - get MessageStore() { return this._getModule('MessageStore'); }, - get EmojiUtils() { return this._getModule('EmojiUtils'); }, - get PermissionUtils() { return this._getModule('Permissions'); }, - get SortedGuildStore() { return this._getModule('SortedGuildStore'); }, - get PrivateChannelActions() { return this._getModule('PrivateChannelActions'); }, - get GuildMemberStore() { return this._getModule('GuildMemberStore'); }, - get GuildChannelsStore() { return this._getModule('GuildChannelsStore'); }, - get MemberCountStore() { return this._getModule('MemberCountStore'); }, - get GuildActions() { return this._getModule('GuildActions'); }, - get NavigationUtils() { return this._getModule('NavigationUtils'); }, - get GuildPermissions() { return this._getModule('GuildPermissions'); }, - get DiscordConstants() { return this._getModule('DiscordConstants'); }, - get ChannelStore() { return this._getModule('ChannelStore'); }, - get GuildStore() { return this._getModule('GuildStore'); }, - get SelectedGuildStore() { return this._getModule('SelectedGuildStore'); }, - get SelectedChannelStore() { return this._getModule('SelectedChannelStore'); }, - get UserStore() { return this._getModule('UserStore'); }, - get RelationshipStore() { return this._getModule('RelationshipStore'); }, - get RelationshipManager() { return this._getModule('RelationshipManager'); }, - get ChangeNicknameModal() { return this._getModule('ChangeNicknameModal'); }, - get UserSettingsStore() { return this._getModule('UserSettingsStore'); }, - get UserSettingsWindow() { return this._getModule('UserSettingsWindow'); }, - get UserStatusStore() { return this._getModule('UserStatusStore'); }, - get ChannelSettingsWindow() { return this._getModule('ChannelSettingsWindow'); }, - get GuildSettingsWindow() { return this._getModule('GuildSettingsWindow'); }, - get CreateChannelModal() { return this._getModule('CreateChannelModal'); }, - get PruneMembersModal() { return this._getModule('PruneMembersModal'); }, - get NotificationSettingsModal() { return this._getModule('NotificationSettingsModal'); }, - get PrivacySettingsModal() { return this._getModule('PrivacySettingsModal'); }, - get UserProfileModal() { return this._getModule('UserProfileModal'); }, - get APIModule() { return this._getModule('APIModule'); }, - get UserNoteStore() { return this._getModule('UserNoteStore'); }, - - get DiscordPermissions() { return this.DiscordConstants.Permissions; } -}; - -export default class DiscordApi { - - static get modules() { return Modules } - static get User() { return User } - static get Channel() { return Channel } - static get Guild() { return Guild } - static get Message() { return Message } - - /** - * A list of loaded guilds. - * @type {List} - */ - static get guilds() { - const guilds = Modules.GuildStore.getGuilds(); - return List.from(Object.entries(guilds), ([i, g]) => Guild.from(g)); - } - - /** - * A list of loaded channels. - * @type {List} - */ - static get channels() { - const channels = Modules.ChannelStore.getChannels(); - return List.from(Object.entries(channels), ([i, c]) => Channel.from(c)); - } - - /** - * A list of loaded users. - * @type {List} - */ - static get users() { - const users = Modules.UserStore.getUsers(); - return List.from(Object.entries(users), ([i, u]) => User.from(u)); - } - - /** - * An object mapping guild IDs to their member counts. - * @type {Object} - */ - static get memberCounts() { - return Modules.MemberCountStore.getMemberCounts(); - } - - /** - * A list of guilds in the order they appear in the server list. - * @type {List} - */ - static get sortedGuilds() { - const guilds = Modules.SortedGuildStore.getSortedGuilds(); - return List.from(guilds, g => Guild.from(g)); - } - - /** - * An array of guild IDs in the order they appear in the server list. - * @type {Number[]} - */ - static get guildPositions() { - return Modules.SortedGuildStore.guildPositions; - } - - /** - * The currently selected guild. - * @type {Guild} - */ - static get currentGuild() { - const guild = Modules.GuildStore.getGuild(Modules.SelectedGuildStore.getGuildId()); - if (guild) return Guild.from(guild); - return null; - } - - /** - * The currently selected channel. - * @type {Channel} - */ - static get currentChannel() { - const channel = Modules.ChannelStore.getChannel(Modules.SelectedChannelStore.getChannelId()); - if (channel) return Channel.from(channel); - return null; - } - - /** - * The current user. - * @type {User} - */ - static get currentUser() { - const user = Modules.UserStore.getCurrentUser(); - if (user) return User.from(user); - return null; - } - - /** - * A list of the current user's friends. - * @type {List} - */ - static get friends() { - const friends = Modules.RelationshipStore.getFriendIDs(); - return List.from(friends, id => User.fromId(id)); - } - /** - * User settings - */ - static get UserSettings() { - return UserSettings; - } - -} - -export class UserSettings { - /** - * Opens Discord's settings UI. - */ - static open(section = 'ACCOUNT') { - Modules.UserSettingsWindow.setSection(section); - Modules.UserSettingsWindow.open(); - } - - /** - * The user's current status. Either "online", "idle", "dnd" or "invisible". - * @type {String} - */ - static get status() { return Modules.UserSettingsStore.status } - - /** - * The user's selected explicit content filter level. - * 0 == off, 1 == everyone except friends, 2 == everyone - * Configurable in the privacy and safety panel. - * @type {Number} - */ - static get explicitContentFilter() { return Modules.UserSettingsStore.explicitContentFilter } - - /** - * Whether to disallow direct messages from server members by default. - * @type {Boolean} - */ - static get defaultGuildsRestricted() { return Modules.UserSettingsStore.defaultGuildsRestricted } - - /** - * An array of guilds to disallow direct messages from their members. - * This is bypassed if the member is has another mutual guild with this disabled, or the member is friends with the current user. - * Configurable in each server's privacy settings. - * @type {Guild[]} - */ - static get restrictedGuildIds() { return Modules.UserSettingsStore.restrictedGuilds } - - static get restrictedGuilds() { - return List.from(this.restrictedGuildIds, id => Guild.fromId(id) || id); - } - - /** - * An array of flags specifying who should be allowed to add the current user as a friend. - * If everyone is checked, this will only have one item, "all". Otherwise it has either "mutual_friends", "mutual_guilds", both or neither. - * Configurable in the privacy and safety panel. - * @type {Array} - */ - static get friendSourceFlags() { return Object.keys(Modules.UserSettingsStore.friendSourceFlags) } - static get friendSourceEveryone() { return this.friendSourceFlags.include('all') } - static get friendSourceMutual_friends() { return this.friendSourceFlags.include('all') || this.friendSourceFlags.include('mutual_friends') } - static get friendSourceMutual_guilds() { return this.friendSourceFlags.include('all') || this.friendSourceFlags.include('mutual_guilds') } - static get friendSourceAnyone() { return this.friendSourceFlags.length > 0 } - - /** - * Whether to automatically add accounts from other platforms running on the user's computer. - * Configurable in the connections panel. - * @type {Boolean} - */ - static get detectPlatformAccounts() { return Modules.UserSettingsStore.detectPlatformAccounts } - - /** - * The number of seconds Discord will wait for activity before sending mobile push notifications. - * Configurable in the notifications panel. - * @type {Number} - */ - static get afkTimeout() { return Modules.UserSettingsStore.afkTimeout } - - /** - * Whether to display the currently running game as a status message. - * Configurable in the games panel. - * @type {Boolean} - */ - static get showCurrentGame() { return Modules.UserSettingsStore.showCurrentGame } - - /** - * Whether to show images uploaded directly to Discord. - * Configurable in the text and images panel. - * @type {Boolean} - */ - static get inlineAttachmentMedia() { return Modules.UserSettingsStore.inlineAttachmentMedia } - - /** - * Whether to show images linked in Discord. - * Configurable in the text and images panel. - * @type {Boolean} - */ - static get inlineEmbedMedia() { return Modules.UserSettingsStore.inlineEmbedMedia } - - /** - * Whether to automatically play GIFs when the Discord window is active without having to hover the mouse over the image. - * Configurable in the text and images panel. - * @type {Boolean} - */ - static get autoplayGifs() { return Modules.UserSettingsStore.gifAutoPlay } - - /** - * Whether to show content from HTTP[s] links as embeds. - * Configurable in the text and images panel. - * @type {Boolean} - */ - static get showEmbeds() { return Modules.UserSettingsStore.renderEmbeds } - - /** - * Whether to show a message's reactions. - * Configurable in the text and images panel. - * @type {Boolean} - */ - static get showReactions() { return Modules.UserSettingsStore.renderReactions } - - /** - * Whether to play animated emoji. - * Configurable in the text and images panel. - * @type {Boolean} - */ - static get animateEmoji() { return Modules.UserSettingsStore.animateEmoji } - - /** - * Whether to convert ASCII emoticons to emoji. - * Configurable in the text and images panel. - * @type {Boolean} - */ - static get convertEmoticons() { return Modules.UserSettingsStore.convertEmoticons } - - /** - * Whether to allow playing text-to-speech messages. - * Configurable in the text and images panel. - * @type {Boolean} - */ - static get allowTts() { return Modules.UserSettingsStore.enableTTSCommand } - - /** - * The user's selected theme. Either "dark" or "light". - * Configurable in the appearance panel. - * @type {String} - */ - static get theme() { return Modules.UserSettingsStore.theme } - - /** - * Whether the user has enabled compact mode. - * `true` if compact mode is enabled, `false` if cozy mode is enabled. - * Configurable in the appearance panel. - * @type {Boolean} - */ - static get displayCompact() { return Modules.UserSettingsStore.messageDisplayCompact } - - /** - * Whether the user has enabled developer mode. - * Currently only adds a "Copy ID" option to the context menu on users, guilds and channels. - * Configurable in the appearance panel. - * @type {Boolean} - */ - static get developerMode() { return Modules.UserSettingsStore.developerMode } - - /** - * The user's selected language code. - * Configurable in the language panel. - * @type {String} - */ - static get locale() { return Modules.UserSettingsStore.locale } - - /** - * The user's timezone offset in hours. - * This is not configurable. - * @type {Number} - */ - static get timezoneOffset() { return Modules.UserSettingsStore.timezoneOffset } -} diff --git a/client/src/modules/discordapi/index.js b/client/src/modules/discordapi/index.js new file mode 100644 index 00000000..42b758f7 --- /dev/null +++ b/client/src/modules/discordapi/index.js @@ -0,0 +1,184 @@ +/** + * BetterDiscord Discord API + * Copyright (c) 2015-present Jiiks/JsSucks - https://github.com/Jiiks / https://github.com/JsSucks + * All rights reserved. + * https://betterdiscord.net + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. +*/ + +import { List } from 'structs'; +import { User, Channel, Guild, Message } from 'discordstructs'; +import Reflection from '../reflection'; +import UserSettings from './usersettings'; + +export { UserSettings }; + +export const Modules = { + _getModule(name) { + const foundModule = Reflection.module.byName(name); + if (!foundModule) return null; + delete this[name]; + return this[name] = foundModule; + }, + + get ChannelSelector() { return this._getModule('ChannelSelector'); }, + get MessageActions() { return this._getModule('MessageActions'); }, + get MessageParser() { return this._getModule('MessageParser'); }, + get MessageStore() { return this._getModule('MessageStore'); }, + get EmojiUtils() { return this._getModule('EmojiUtils'); }, + get PermissionUtils() { return this._getModule('Permissions'); }, + get SortedGuildStore() { return this._getModule('SortedGuildStore'); }, + get PrivateChannelActions() { return this._getModule('PrivateChannelActions'); }, + get GuildMemberStore() { return this._getModule('GuildMemberStore'); }, + get GuildChannelsStore() { return this._getModule('GuildChannelsStore'); }, + get MemberCountStore() { return this._getModule('MemberCountStore'); }, + get GuildActions() { return this._getModule('GuildActions'); }, + get NavigationUtils() { return this._getModule('NavigationUtils'); }, + get GuildPermissions() { return this._getModule('GuildPermissions'); }, + get DiscordConstants() { return this._getModule('DiscordConstants'); }, + get ChannelStore() { return this._getModule('ChannelStore'); }, + get GuildStore() { return this._getModule('GuildStore'); }, + get SelectedGuildStore() { return this._getModule('SelectedGuildStore'); }, + get SelectedChannelStore() { return this._getModule('SelectedChannelStore'); }, + get UserStore() { return this._getModule('UserStore'); }, + get RelationshipStore() { return this._getModule('RelationshipStore'); }, + get RelationshipManager() { return this._getModule('RelationshipManager'); }, + get ChangeNicknameModal() { return this._getModule('ChangeNicknameModal'); }, + get UserInfoStore() { return this._getModule('UserInfoStore'); }, + get UserSettingsStore() { return this._getModule('UserSettingsStore'); }, + get UserSettingsUpdater() { return this._getModule('UserSettingsUpdater'); }, + get AccessibilityStore() { return this._getModule('AccessibilityStore'); }, + get AccessibilitySettingsUpdater() { return this._getModule('AccessibilitySettingsUpdater'); }, + get UserSettingsWindow() { return this._getModule('UserSettingsWindow'); }, + get UserStatusStore() { return this._getModule('UserStatusStore'); }, + get ChannelSettingsWindow() { return this._getModule('ChannelSettingsWindow'); }, + get GuildSettingsWindow() { return this._getModule('GuildSettingsWindow'); }, + get CreateChannelModal() { return this._getModule('CreateChannelModal'); }, + get PruneMembersModal() { return this._getModule('PruneMembersModal'); }, + get NotificationSettingsModal() { return this._getModule('NotificationSettingsModal'); }, + get PrivacySettingsModal() { return this._getModule('PrivacySettingsModal'); }, + get UserProfileModal() { return this._getModule('UserProfileModal'); }, + get APIModule() { return this._getModule('APIModule'); }, + get UserNoteStore() { return this._getModule('UserNoteStore'); }, + get KeyboardCombosModal() { return this._getModule('KeyboardCombosModal'); }, + + get DiscordPermissions() { return this.DiscordConstants.Permissions; } +}; + +export default class DiscordApi { + + static get modules() { return Modules } + static get User() { return User } + static get Channel() { return Channel } + static get Guild() { return Guild } + static get Message() { return Message } + + /** + * A list of loaded guilds. + * @type {List} + */ + static get guilds() { + const guilds = Modules.GuildStore.getGuilds(); + return List.from(Object.values(guilds), g => Guild.from(g)); + } + + /** + * A list of loaded channels. + * @type {List} + */ + static get channels() { + const channels = Modules.ChannelStore.getChannels(); + return List.from(Object.values(channels), c => Channel.from(c)); + } + + /** + * A list of loaded users. + * @type {List} + */ + static get users() { + const users = Modules.UserStore.getUsers(); + return List.from(Object.values(users), u => User.from(u)); + } + + /** + * An object mapping guild IDs to their member counts. + * @type {Object} + */ + static get memberCounts() { + return Modules.MemberCountStore.getMemberCounts(); + } + + /** + * A list of guilds in the order they appear in the server list. + * @type {List} + */ + static get sortedGuilds() { + const guilds = Modules.SortedGuildStore.getSortedGuilds(); + return List.from(guilds, g => Guild.from(g)); + } + + /** + * An array of guild IDs in the order they appear in the server list. + * @type {Number[]} + */ + static get guildPositions() { + return Modules.SortedGuildStore.guildPositions; + } + + /** + * The currently selected guild. + * @type {Guild} + */ + static get currentGuild() { + const guild = Modules.GuildStore.getGuild(Modules.SelectedGuildStore.getGuildId()); + return guild ? Guild.from(guild) : null; + } + + /** + * The currently selected channel. + * @type {Channel} + */ + static get currentChannel() { + const channel = Modules.ChannelStore.getChannel(Modules.SelectedChannelStore.getChannelId()); + return channel ? Channel.from(channel) : null; + } + + /** + * The current user. + * @type {User} + */ + static get currentUser() { + const user = Modules.UserStore.getCurrentUser(); + return user ? User.from(user) : null; + } + + /** + * A list of the current user's friends. + * @type {List} + */ + static get friends() { + const friends = Modules.RelationshipStore.getFriendIDs(); + return List.from(friends, id => User.fromId(id)); + } + + /** + * Whether a user is logged in. + */ + static get authenticated() { + return !Modules.UserInfoStore.isGuest(); + } + + /** + * User settings. + */ + static get UserSettings() { + return UserSettings; + } + + static showKeyboardCombosModal() { + Modules.KeyboardCombosModal.show(); + } + +} diff --git a/client/src/modules/discordapi/usersettings.js b/client/src/modules/discordapi/usersettings.js new file mode 100644 index 00000000..b562d7b6 --- /dev/null +++ b/client/src/modules/discordapi/usersettings.js @@ -0,0 +1,629 @@ +/** + * BetterDiscord Discord API + * Copyright (c) 2015-present Jiiks/JsSucks - https://github.com/Jiiks / https://github.com/JsSucks + * All rights reserved. + * https://betterdiscord.net + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. +*/ + +import DiscordApi, { Modules } from '.'; +import EventEmitter from 'events'; +import { Utils } from 'common'; +import { List } from 'structs'; +import { User, Channel, Guild, Message } from 'discordstructs'; +import Events from '../events'; + +const remoteSettingsKeys = { + showCurrentGame: 'show_current_game', + inlineAttachmentMedia: 'inline_attachment_media', + inlineEmbedMedia: 'inline_embed_media', + gifAutoPlay: 'gif_auto_play', + renderEmbeds: 'render_embeds', + renderReactions: 'render_reactions', + renderSpoilers: 'render_spoilers', + showInAppNotifications: 'show_in_app_notifications', + animateEmoji: 'animate_emoji', + sync: 'sync', + theme: 'theme', + enableTTSCommand: 'enable_tts_command', + messageDisplayCompact: 'message_display_compact', + locale: 'locale', + convertEmoticons: 'convert_emoticons', + restrictedGuilds: 'restricted_guilds', + friendSourceFlags: 'friend_source_flags', + developerMode: 'developer_mode', + guildPositions: 'guild_positions', + detectPlatformAccounts: 'detect_platform_accounts', + status: 'status', + explicitContentFilter: 'explicit_content_filter', + disableGamesTab: 'disable_games_tab', + defaultGuildsRestricted: 'default_guilds_restricted', + afkTimeout: 'afk_timeout', + timezoneOffset: 'timezone_offset' +}; + +export default new class UserSettings extends EventEmitter { + + init() { + for (const [key, discordKey] of Object.entries({ + 'status': 'status', + 'explicitContentFilter': 'explicitContentFilter', + 'defaultGuildsRestricted': 'defaultGuildsRestricted', + 'restrictedGuildIds': 'restrictedGuilds', + // 'friendSourceFlags', + 'detectPlatformAccounts': 'detectPlatformAccounts', + 'afkTimeout': 'afkTimeout', + 'showCurrentGame': 'showCurrentGame', + 'inlineAttachmentMedia': 'inlineAttachmentMedia', + 'inlineEmbedMedia': 'inlineEmbedMedia', + 'autoplayGifs': 'gifAutoPlay', + 'showEmbeds': 'renderEmbeds', + 'showReactions': 'renderReactions', + 'showSpoilers': 'renderSpoilers', + 'animateEmoji': 'animateEmoji', + 'convertEmoticons': 'convertEmoticons', + 'allowTts': 'enableTTSCommand', + 'theme': 'theme', + 'displayCompact': 'messageDisplayCompact', + // 'disableGamesTab', + 'developerMode': 'developerMode', + 'locale': 'locale', + 'timezoneOffset': 'timezoneOffset', + })) { + this['_' + discordKey] = this.key; + } + + this._friendSourceFlags = Modules.UserSettingsStore.friendSourceFlags; + this._disableGamesTab = !this.showActivityTab; + + Events.on('discord-dispatch:USER_SETTINGS_UPDATE', event => { + for (const k of Object.keys(event.settings)) { + // No change + if (Utils.compare(event.settings[k], this['_' + k])) continue; + + if (this['_update_' + k]) this['_update_' + k](); + } + }); + + this._colourblindMode = this.colourblindMode; + + Events.on('discord-dispatch:ACCESSIBILITY_COLORBLIND_TOGGLE', () => { + if (Utils.compare(this.colourblindMode, this._colourblindMode)) return; + + this.emit('colourblind-mode', this.colourblindMode, this._colourblindMode); + this._colourblindMode = this.colourblindMode; + }); + } + + /** + * Opens Discord's settings UI. + */ + open(section = 'ACCOUNT') { + Modules.UserSettingsWindow.open(); + Modules.UserSettingsWindow.setSection(section); + } + + /** + * Updates settings. + * @param {Object} data + * @param {boolean} [save=true] + * @return {Promise} + */ + async updateSettings(data, save = true) { + Modules.UserSettingsUpdater.updateLocalSettings(data); + + if (save && DiscordApi.authenticated) { + await Modules.APIModule.patch({ + url: Modules.DiscordConstants.Endpoints.SETTINGS, + body: this.toRemoteKeys(data) + }); + } + } + + toRemoteKeys(data) { + const body = {}; + for (const k of Object.keys(data)) { + body[remoteSettingsKeys[k] || k] = data[k]; + } + return body; + } + + /** + * Updates settings locally. + * @param {Object} data + */ + localUpdateSettings(data) { + this.updateSettings(data, false); + } + + /** + * The user's current status. Either "online", "idle", "dnd" or "invisible". + * @type {string} + */ + get status() { + // Reading _status tells Vue to watch it + return this._status, Modules.UserSettingsStore.status; + } + + set status(status) { + if (!['online', 'idle', 'dnd', 'invisible'].includes(status)) throw new Error('Invalid status.'); + this.updateSettings({status}); + } + + _update_status() { + this.emit('status', this.status, this._status); + this._status = this.status; + } + + get StatusOnline() { return 'online' } + get StatusIdle() { return 'idle' } + get StatusDND() { return 'dnd' } + get StatusInvisible() { return 'invisible' } + + /** + * The user's selected explicit content filter level. + * 0 == off, 1 == everyone except friends, 2 == everyone + * Configurable in the privacy and safety panel. + * @type {number} + */ + get explicitContentFilter() { return this._explicitContentFilter, Modules.UserSettingsStore.explicitContentFilter } + + set explicitContentFilter(explicitContentFilter) { + if (![0, 1, 2].includes(explicitContentFilter)) throw new Error('Invalid explicit content filter level.'); + this.updateSettings({explicitContentFilter}); + } + + _update_explicitContentFilter() { + this.emit('explicit-content-filter', this.explicitContentFilter, this._explicitContentFilter); + this._explicitContentFilter = this.explicitContentFilter; + } + + get ExplicitContentFilterDisabled() { return 0 } + get ExplicitContentFilterExceptFriends() { return 1 } + get ExplicitContentFilterEnabled() { return 2 } + + /** + * Whether to disallow direct messages from server members by default. + * @type {boolean} + */ + get defaultGuildsRestricted() { return this._defaultGuildsRestricted, Modules.UserSettingsStore.defaultGuildsRestricted } + + set defaultGuildsRestricted(defaultGuildsRestricted) { + this.updateSettings({defaultGuildsRestricted: !!defaultGuildsRestricted}); + } + + _update_defaultGuildsRestricted() { + this.emit('default-guilds-restricted', this.defaultGuildsRestricted, this._defaultGuildsRestricted); + this._defaultGuildsRestricted = this.defaultGuildsRestricted; + } + + /** + * An array of guilds to disallow direct messages from their members. + * This is bypassed if the member is has another mutual guild with this disabled, or the member is friends with the current user. + * Configurable in each server's privacy settings. + * @type {Guild[]} + */ + get restrictedGuilds() { + return List.from(this.restrictedGuildIds, id => Guild.fromId(id) || id); + } + + get restrictedGuildIds() { return this._restrictedGuilds, Modules.UserSettingsStore.restrictedGuilds } + + _update_restrictedGuilds() { + this.emit('restricted-guilds', this.restrictedGuildIds, this._restrictedGuilds); + this._restrictedGuilds = this.restrictedGuildIds; + } + + /** + * An array of flags specifying who should be allowed to add the current user as a friend. + * If everyone is checked, this will only have one item, "all". Otherwise it has either "mutual_friends", "mutual_guilds", both or neither. + * Configurable in the privacy and safety panel. + * @type {string[]} + */ + get friendSourceFlags() { return this._friendSourceFlags, Object.keys(Modules.UserSettingsStore.friendSourceFlags).filter(f => Modules.UserSettingsStore.friendSourceFlags[f]) } + get friendSourceEveryone() { return this.friendSourceFlags.includes('all') } + get friendSourceMutualFriends() { return this.friendSourceFlags.includes('all') || this.friendSourceFlags.includes('mutual_friends') } + get friendSourceMutualGuilds() { return this.friendSourceFlags.includes('all') || this.friendSourceFlags.includes('mutual_guilds') } + get friendSourceAnyone() { return this.friendSourceFlags.length > 0 } + + set friendSourceFlags(friendSourceFlags) { + this.updateSettings({friendSourceFlags: { + all: friendSourceFlags.includes('all'), + mutual_friends: friendSourceFlags.includes('all') || friendSourceFlags.includes('mutual_friends'), + mutual_guilds: friendSourceFlags.includes('all') || friendSourceFlags.includes('mutual_guilds') + }}); + } + + set friendSourceEveryone(friendSourceEveryone) { + if (!!friendSourceEveryone === this.friendSourceEveryone) return; + this.friendSourceFlags = friendSourceEveryone ? ['all'] : ['mutual_friends', 'mutual_guilds']; + } + + set friendSourceMutualFriends(friendSourceMutualFriends) { + if (!!friendSourceMutualFriends === this.friendSourceMutualFriends) return; + this.friendSourceFlags = friendSourceMutualFriends ? this.friendSourceFlags.concat(['mutual_friends']) : + this.friendSourceFlags.includes('all') ? ['mutual_guilds'] : + this.friendSourceFlags.filter(f => f !== 'mutual_friends'); + } + + set friendSourceMutualGuilds(friendSourceMutualGuilds) { + if (!!friendSourceMutualGuilds === this.friendSourceMutualGuilds) return; + this.friendSourceFlags = friendSourceMutualGuilds ? this.friendSourceFlags.concat(['mutual_guilds']) : + this.friendSourceFlags.includes('all') ? ['mutual_friends'] : + this.friendSourceFlags.filter(f => f !== 'mutual_guilds'); + } + + set friendSourceAnyone(friendSourceAnyone) { + if (!!friendSourceAnyone === this.friendSourceAnyone) return; + this.friendSourceFlags = friendSourceAnyone ? ['mutual_friends', 'mutual_guilds'] : []; + } + + _update_friendSourceFlags() { + this.emit('friend-source-flags', this.friendSourceFlags, Object.keys(this._friendSourceFlags).filter(f => this._friendSourceFlags[f])); + this._friendSourceFlags = this.friendSourceFlags; + } + + /** + * Whether to automatically add accounts from other platforms running on the user's computer. + * Configurable in the connections panel. + * @type {boolean} + */ + get detectPlatformAccounts() { return this._detectPlatformAccounts, Modules.UserSettingsStore.detectPlatformAccounts } + + set detectPlatformAccounts(detectPlatformAccounts) { + this.updateSettings({detectPlatformAccounts: !!detectPlatformAccounts}); + } + + _update_detectPlatformAccounts() { + this.emit('detect-platform-accounts', this.detectPlatformAccounts, this._detectPlatformAccounts); + this._detectPlatformAccounts = this.detectPlatformAccounts; + } + + /** + * The number of seconds Discord will wait for activity before sending mobile push notifications. + * Configurable in the notifications panel. + * @type {number} + */ + get afkTimeout() { return this._afkTimeout, Modules.UserSettingsStore.afkTimeout } + + set afkTimeout(afkTimeout) { + this.updateSettings({afkTimeout: parseInt(afkTimeout)}); + } + + _update_afkTimeout() { + this.emit('afk-timeout', this.afkTimeout, this._afkTimeout); + this._afkTimeout = this.afkTimeout; + } + + get AfkTimeout1Minute() { return 60 } + get AfkTimeout2Minutes() { return 120 } + get AfkTimeout3Minutes() { return 180 } + get AfkTimeout4Minutes() { return 240 } + get AfkTimeout5Minutes() { return 300 } + get AfkTimeout6Minutes() { return 360 } + get AfkTimeout7Minutes() { return 420 } + get AfkTimeout8Minutes() { return 480 } + get AfkTimeout9Minutes() { return 540 } + get AfkTimeout10Minutes() { return 600 } + + /** + * Whether to display the currently running game as a status message. + * Configurable in the games panel. + * @type {boolean} + */ + get showCurrentGame() { return this._showCurrentGame, Modules.UserSettingsStore.showCurrentGame } + + set showCurrentGame(showCurrentGame) { + this.updateSettings({showCurrentGame: !!showCurrentGame}); + } + set localShowCurrentGame(showCurrentGame) { + this.updateSettings({showCurrentGame: !!showCurrentGame}, false); + } + + _update_showCurrentGame() { + this.emit('restricted-guilds', this.showCurrentGame, this._showCurrentGame); + this._showCurrentGame = this.showCurrentGame; + } + + /** + * Whether to show images uploaded directly to Discord. + * Configurable in the text and images panel. + * @type {boolean} + */ + get inlineAttachmentMedia() { return this._inlineAttachmentMedia, Modules.UserSettingsStore.inlineAttachmentMedia } + + set inlineAttachmentMedia(inlineAttachmentMedia) { + this.updateSettings({inlineAttachmentMedia: !!inlineAttachmentMedia}); + } + set localInlineAttachmentMedia(inlineAttachmentMedia) { + this.updateSettings({inlineAttachmentMedia: !!inlineAttachmentMedia}, false); + } + + _update_inlineAttachmentMedia() { + this.emit('inline-attachment-media', this.inlineAttachmentMedia, this._inlineAttachmentMedia); + this._inlineAttachmentMedia = this.inlineAttachmentMedia; + } + + /** + * Whether to show images linked in Discord. + * Configurable in the text and images panel. + * @type {boolean} + */ + get inlineEmbedMedia() { return this._inlineEmbedMedia, Modules.UserSettingsStore.inlineEmbedMedia } + + set inlineEmbedMedia(inlineEmbedMedia) { + this.updateSettings({inlineEmbedMedia: !!inlineEmbedMedia}); + } + set localInlineEmbedMedia(inlineEmbedMedia) { + this.updateSettings({inlineEmbedMedia: !!inlineEmbedMedia}, false); + } + + _update_inlineEmbedMedia() { + this.emit('inline-embed-media', this.inlineEmbedMedia, this._inlineEmbedMedia); + this._inlineEmbedMedia = this.inlineEmbedMedia; + } + + /** + * Whether to automatically play GIFs when the Discord window is active without having to hover the mouse over the image. + * Configurable in the text and images panel. + * @type {boolean} + */ + get autoplayGifs() { return this._gifAutoPlay, Modules.UserSettingsStore.gifAutoPlay } + + set autoplayGifs(gifAutoPlay) { + this.updateSettings({gifAutoPlay: !!gifAutoPlay}); + } + set localAutoplayGifs(gifAutoPlay) { + this.updateSettings({gifAutoPlay: !!gifAutoPlay}, false); + } + + _update_gifAutoPlay() { + this.emit('autoplay-gifs', this.autoplayGifs, this._gifAutoPlay); + this._gifAutoPlay = this.autoplayGifs; + } + + /** + * Whether to show content from HTTP[s] links as embeds. + * Configurable in the text and images panel. + * @type {boolean} + */ + get showEmbeds() { return this._renderEmbeds, Modules.UserSettingsStore.renderEmbeds } + + set showEmbeds(renderEmbeds) { + this.updateSettings({renderEmbeds: !!renderEmbeds}); + } + set localShowEmbeds(renderEmbeds) { + this.updateSettings({renderEmbeds: !!renderEmbeds}, false); + } + + _update_renderEmbeds() { + this.emit('show-embeds', this.showEmbeds, this._renderEmbeds); + this._renderEmbeds = this.showEmbeds; + } + + /** + * Whether to show a message's reactions. + * Configurable in the text and images panel. + * @type {boolean} + */ + get showReactions() { return this._renderReactions, Modules.UserSettingsStore.renderReactions } + + set showReactions(renderReactions) { + this.updateSettings({renderReactions: !!renderReactions}); + } + set localShowReactions(renderReactions) { + this.updateSettings({renderReactions: !!renderReactions}, false); + } + + _update_showReactions() { + this.emit('show-reactions', this.showReactions, this._showReactions); + this._showReactions = this.showReactions; + } + + /** + * When to show spoilers. + * Configurable in the text and images panel. + * @type {boolean} + */ + get showSpoilers() { return this._renderSpoilers, Modules.UserSettingsStore.renderSpoilers } + + set showSpoilers(renderSpoilers) { + if (!['ON_CLICK', 'IF_MODERATOR', 'ALWAYS'].includes(renderSpoilers)) throw new Error('Invalid show spoilers value.'); + this.updateSettings({renderSpoilers: !!renderSpoilers}, false); + } + + _update_renderSpoilers() { + this.emit('show-spoilers', this.showSpoilers, this._renderSpoilers); + this._renderSpoilers = this.showSpoilers; + } + + get ShowSpoilersOnClick() { return 'ON_CLICK' } + get ShowSpoilersIfModerator() { return 'IF_MODERATOR' } + get ShowSpoilersAlways() { return 'ALWAYS' } + + /** + * Whether to play animated emoji. + * Configurable in the text and images panel. + * @type {boolean} + */ + get animateEmoji() { return this._animateEmoji, Modules.UserSettingsStore.animateEmoji } + + set animateEmoji(animateEmoji) { + this.updateSettings({animateEmoji: !!animateEmoji}); + } + set localAnimateEmoji(animateEmoji) { + this.updateSettings({animateEmoji: !!animateEmoji}, false); + } + + _update_animateEmoji() { + this.emit('animate-emoji', this.animateEmoji, this._animateEmoji); + this._animateEmoji = this.animateEmoji; + } + + /** + * Whether to convert ASCII emoticons to emoji. + * Configurable in the text and images panel. + * @type {boolean} + */ + get convertEmoticons() { return this._convertEmoticons, Modules.UserSettingsStore.convertEmoticons } + + set convertEmoticons(convertEmoticons) { + this.updateSettings({convertEmoticons: !!convertEmoticons}); + } + set localConvertEmoticons(convertEmoticons) { + this.updateSettings({convertEmoticons: !!convertEmoticons}, false); + } + + _update_convertEmoticons() { + this.emit('convert-emoticons', this.convertEmoticons, this._convertEmoticons); + this._convertEmoticons = this.convertEmoticons; + } + + /** + * Whether to allow playing text-to-speech messages. + * Configurable in the text and images panel. + * @type {boolean} + */ + get allowTts() { return this._enableTTSCommand, Modules.UserSettingsStore.enableTTSCommand } + + set allowTts(enableTTSCommand) { + this.updateSettings({enableTTSCommand: !!enableTTSCommand}); + } + set localAllowTts(enableTTSCommand) { + this.updateSettings({enableTTSCommand: !!enableTTSCommand}, false); + } + + _update_enableTTSCommand() { + this.emit('allow-tts', this.allowTts, this._enableTTSCommand); + this._enableTTSCommand = this.allowTts; + } + + /** + * The user's selected theme. Either "dark" or "light". + * Configurable in the appearance panel. + * @type {string} + */ + get theme() { return this._theme, Modules.UserSettingsStore.theme } + + set theme(theme) { + if (!['dark', 'light'].includes(theme)) throw new Error('Invalid theme.'); + this.updateSettings({theme}); + } + set localTheme(theme) { + if (!['dark', 'light'].includes(theme)) throw new Error('Invalid theme.'); + this.updateSettings({theme}, false); + } + + _update_theme() { + this.emit('theme', this.theme, this._theme); + this._theme = this.theme; + } + + get ThemeDark() { return 'dark' } + get ThemeLight() { return 'light' } + + /** + * Whether the user has enabled compact mode. + * `true` if compact mode is enabled, `false` if cozy mode is enabled. + * Configurable in the appearance panel. + * @type {boolean} + */ + get displayCompact() { return this._messageDisplayCompact, Modules.UserSettingsStore.messageDisplayCompact } + + set displayCompact(messageDisplayCompact) { + this.updateSettings({messageDisplayCompact: !!messageDisplayCompact}); + } + set localDisplayCompact(messageDisplayCompact) { + this.updateSettings({messageDisplayCompact: !!messageDisplayCompact}, false); + } + + _update_messageDisplayCompact() { + this.emit('inline-embed-media', this.displayCompact, this._messageDisplayCompact); + this._messageDisplayCompact = this.displayCompact; + } + + /** + * Whether the user has enabled colourblind mode. + * Configurable in the appearance panel. + * @type {boolean} + */ + get colourblindMode() { return this._colourblindMode, Modules.AccessibilityStore.colorblindMode } + + set colourblindMode(colourblindMode) { + if (!!colourblindMode === this.colourblindMode) return; + Modules.AccessibilitySettingsUpdater.toggleColorblindMode(); + } + + /** + * Whether the user has enabled the activity tab. + * Configurable in the appearance panel. + * @type {boolean} + */ + get showActivityTab() { return this._disableGamesTab, !Modules.UserSettingsStore.disableGamesTab } + + set showActivityTab(disableGamesTab) { + this.updateSettings({disableGamesTab: !disableGamesTab}); + } + set localShowActivityTab(disableGamesTab) { + this.updateSettings({disableGamesTab: !disableGamesTab}, false); + } + + _update_disableGamesTab() { + this.emit('show-activity-tab', this.showActivityTab, !this._disableGamesTab); + this._disableGamesTab = !this.showActivityTab; + } + + /** + * Whether the user has enabled developer mode. + * Currently only adds a "Copy ID" option to the context menu on users, guilds and channels. + * Configurable in the appearance panel. + * @type {boolean} + */ + get developerMode() { return this._developerMode, Modules.UserSettingsStore.developerMode } + + set developerMode(developerMode) { + this.updateSettings({developerMode: !!developerMode}); + } + set localDeveloperMode(developerMode) { + this.updateSettings({developerMode: !!developerMode}, false); + } + + _update_developerMode() { + this.emit('developer-mode', this.developerMode, this._developerMode); + this._developerMode = this.developerMode; + } + + /** + * The user's selected language code. + * Configurable in the language panel. + * @type {string} + */ + get locale() { return this._locale, Modules.UserSettingsStore.locale } + + set locale(locale) { + this.updateSettings({locale: !!locale}); + } + set localLocale(locale) { + this.updateSettings({locale: !!locale}, false); + } + + _update_locale() { + this.emit('locale', this.locale, this._locale); + this._locale = this.locale; + } + + /** + * The user's timezone offset in hours. + * This is not configurable. + * @type {number} + */ + get timezoneOffset() { return this._timezoneOffset, Modules.UserSettingsStore.timezoneOffset } + + _update_timezoneOffset() { + this.emit('timezone-offset', this.timezoneOffset, this._timezoneOffset); + this._timezoneOffset = this.timezoneOffset; + } + +} diff --git a/client/src/modules/dispatchhook.js b/client/src/modules/dispatchhook.js new file mode 100644 index 00000000..309159b8 --- /dev/null +++ b/client/src/modules/dispatchhook.js @@ -0,0 +1,56 @@ +/** + * BetterDiscord Dispatch Hook + * Copyright (c) 2015-present Jiiks/JsSucks - https://github.com/Jiiks / https://github.com/JsSucks + * All rights reserved. + * https://betterdiscord.net + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. +*/ + +import Reflection from './reflection'; +import { MonkeyPatch } from './patcher'; +import Events from './events'; +import EventListener from './eventlistener'; + +// Discord seems to like to dispatch some things multiple times +const dispatched = new WeakSet(); + +/** + * Discord event hook. + * @extends {EventListener} + */ +export default class extends EventListener { + + init() { + this.hook(); + } + + bindings() { + this.hook = this.hook.bind(this); + this.dispatch = this.dispatch.bind(this); + } + + get eventBindings() { + return [ + { id: 'discord-ready', callback: this.hook } + ]; + } + + hook() { + const { Dispatcher } = Reflection.modules; + MonkeyPatch('BD:EVENTS', Dispatcher).after('dispatch', this.dispatch); + } + + /** + * Emit callback. + */ + dispatch(Dispatcher, [event], retVal) { + if (dispatched.has(event)) return; + dispatched.add(event); + + Events.emit('discord-dispatch', event); + Events.emit(`discord-dispatch:${event.type}`, event); + } + +} diff --git a/client/src/modules/modulemanager.js b/client/src/modules/modulemanager.js index 13b94871..fd5899b0 100644 --- a/client/src/modules/modulemanager.js +++ b/client/src/modules/modulemanager.js @@ -9,8 +9,9 @@ */ import { ClientLogger as Logger } from 'common'; -import { SocketProxy, EventHook } from 'modules'; +import { SocketProxy, EventHook, DispatchHook } from 'modules'; import { ProfileBadges, ClassNormaliser } from 'ui'; +import { UserSettings } from './discordapi'; import Updater from './updater'; /** @@ -27,6 +28,8 @@ export default class { new ClassNormaliser(), new SocketProxy(), new EventHook(), + new DispatchHook(), + UserSettings, Updater ]); } diff --git a/client/src/modules/modules.js b/client/src/modules/modules.js index 437f54fc..355a665a 100644 --- a/client/src/modules/modules.js +++ b/client/src/modules/modules.js @@ -22,6 +22,7 @@ export { default as Module } from './module'; export { default as EventListener } from './eventlistener'; export { default as SocketProxy } from './socketproxy'; export { default as EventHook } from './eventhook'; +export { default as DispatchHook } from './dispatchhook'; export { default as DiscordApi, Modules as DiscordApiModules } from './discordapi'; export { default as BdWebApi } from './bdwebapi'; export { default as Connectivity } from './connectivity'; diff --git a/client/src/modules/reflection/knownmodules.js b/client/src/modules/reflection/knownmodules.js new file mode 100644 index 00000000..0a795a8c --- /dev/null +++ b/client/src/modules/reflection/knownmodules.js @@ -0,0 +1,189 @@ +/** + * BetterDiscord Reflection Modules + * Copyright (c) 2015-present Jiiks/JsSucks - https://github.com/Jiiks / https://github.com/JsSucks + * All rights reserved. + * https://betterdiscord.net + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. +*/ + +import { Filters } from 'common'; + +export default { + + React: Filters.byProperties(['createElement', 'cloneElement']), + ReactDOM: Filters.byProperties(['render', 'findDOMNode']), + + Events: Filters.byPrototypeFields(['setMaxListeners', 'emit']), + + /* Guild Info, Stores, and Utilities */ + GuildStore: Filters.byProperties(['getGuild']), + SortedGuildStore: Filters.byProperties(['getSortedGuilds']), + SelectedGuildStore: Filters.byProperties(['getLastSelectedGuildId']), + GuildSync: Filters.byProperties(['getSyncedGuilds']), + GuildInfo: Filters.byProperties(['getAcronym']), + GuildChannelsStore: Filters.byProperties(['getChannels', 'getDefaultChannel']), + GuildMemberStore: Filters.byProperties(['getMember']), + MemberCountStore: Filters.byProperties(['getMemberCounts']), + GuildEmojiStore: Filters.byProperties(['getEmojis']), + GuildActions: Filters.byProperties(['markGuildAsRead']), + GuildPermissions: Filters.byProperties(['getGuildPermissions']), + + /* Channel Store & Actions */ + ChannelStore: Filters.byProperties(['getChannels', 'getDMFromUserId']), + SelectedChannelStore: Filters.byProperties(['getLastSelectedChannelId']), + ChannelActions: Filters.byProperties(['selectChannel']), + PrivateChannelActions: Filters.byProperties(['openPrivateChannel']), + ChannelSelector: Filters.byProperties(['selectGuild', 'selectChannel']), + VoiceChannelActions: Filters.byProperties(['selectVoiceChannel']), + + /* Current User Info, State and Settings */ + UserInfoStore: Filters.byProperties(['getToken']), + UserSettingsStore: Filters.byProperties(['guildPositions']), + AccessibilityStore: Filters.byProperties(['colorblindMode']), + AccountManager: Filters.byProperties(['register', 'login']), + UserSettingsUpdater: Filters.byProperties(['updateRemoteSettings']), + AccessibilitySettingsUpdater: Filters.byProperties(['toggleColorblindMode']), + OnlineWatcher: Filters.byProperties(['isOnline']), + CurrentUserIdle: Filters.byProperties(['getIdleTime']), + RelationshipStore: Filters.byProperties(['isBlocked', 'isFriend']), + RelationshipManager: Filters.byProperties(['addRelationship']), + MentionStore: Filters.byProperties(['getMentions']), + + /* User Stores and Utils */ + UserStore: Filters.byProperties(['getCurrentUser']), + UserStatusStore: Filters.byProperties(['getStatuses']), + UserTypingStore: Filters.byProperties(['isTyping']), + UserActivityStore: Filters.byProperties(['getActivity']), + UserNameResolver: Filters.byProperties(['getName']), + UserNoteStore: Filters.byProperties(['getNote']), + UserNoteActions: Filters.byProperties(['updateNote']), + DraftActions: Filters.byProperties(['changeDraft']), + + /* Emoji Store and Utils */ + EmojiInfo: Filters.byProperties(['isEmojiDisabled']), + EmojiUtils: Filters.byProperties(['getGuildEmoji']), + EmojiStore: Filters.byProperties(['getByCategory', 'EMOJI_NAME_RE']), + + /* Invite Store and Utils */ + InviteStore: Filters.byProperties(['getInvites']), + InviteResolver: Filters.byProperties(['findInvite']), + InviteActions: Filters.byProperties(['acceptInvite']), + + /* Discord Objects & Utils */ + DiscordConstants: Filters.byProperties(['Permissions', 'ActivityTypes', 'StatusTypes']), + Permissions: Filters.byProperties(['getHighestRole']), + ColorConverter: Filters.byProperties(['hex2int']), + ColorShader: Filters.byProperties(['darken']), + TinyColor: Filters.byPrototypeFields(['toRgb']), + ClassResolver: Filters.byProperties(['getClass']), + ButtonData: Filters.byProperties(['ButtonSizes']), + IconNames: Filters.byProperties(['IconNames']), + NavigationUtils: Filters.byProperties(['transitionTo', 'replaceWith', 'getHistory']), + + /* Discord Messages */ + MessageStore: Filters.byProperties(['getMessages']), + MessageActions: Filters.byProperties(['jumpToMessage', '_sendMessage']), + MessageQueue: Filters.byProperties(['enqueue']), + MessageParser: Filters.byProperties(['createMessage', 'parse', 'unparse']), + + /* In-Game Overlay */ + OverlayUserPopoutSettings: Filters.byProperties(['openUserPopout']), + OverlayUserPopoutInfo: Filters.byProperties(['getOpenedUserPopout']), + + /* Experiments */ + ExperimentStore: Filters.byProperties(['getExperimentOverrides']), + ExperimentsManager: Filters.byProperties(['isDeveloper']), + CurrentExperiment: Filters.byProperties(['getExperimentId']), + + /* Images, Avatars and Utils */ + ImageResolver: Filters.byProperties(['getUserAvatarURL']), + ImageUtils: Filters.byProperties(['getSizedImageSrc']), + AvatarDefaults: Filters.byProperties(['getUserAvatarURL', 'DEFAULT_AVATARS']), + + /* Drag & Drop */ + DNDActions: Filters.byProperties(['beginDrag']), + DNDSources: Filters.byProperties(['addTarget']), + DNDObjects: Filters.byProperties(['DragSource']), + + /* Electron & Other Internals with Utils */ + ElectronModule: Filters.byProperties(['_getMainWindow']), + Dispatcher: Filters.byProperties(['dirtyDispatch']), + PathUtils: Filters.byProperties(['hasBasename']), + NotificationModule: Filters.byProperties(['showNotification']), + RouterModule: Filters.byProperties(['Router']), + APIModule: Filters.byProperties(['getAPIBaseURL']), + AnalyticEvents: Filters.byProperties(['AnalyticEventConfigs']), + KeyGenerator: Filters.byCode(/"binary"/), + Buffers: Filters.byProperties(['Buffer', 'kMaxLength']), + DeviceStore: Filters.byProperties(['getDevices']), + SoftwareInfo: Filters.byProperties(['os']), + CurrentContext: Filters.byProperties(['setTagsContext']), + + /* Media Stuff (Audio/Video) */ + MediaDeviceInfo: Filters.byProperties(['Codecs', 'SUPPORTED_BROWSERS']), + MediaInfo: Filters.byProperties(['getOutputVolume']), + MediaEngineInfo: Filters.byProperties(['MediaEngineFeatures']), + VoiceInfo: Filters.byProperties(['EchoCancellation']), + VideoStream: Filters.byProperties(['getVideoStream']), + SoundModule: Filters.byProperties(['playSound']), + + /* Window, DOM, HTML */ + WindowInfo: Filters.byProperties(['isFocused', 'windowSize']), + TagInfo: Filters.byProperties(['VALID_TAG_NAMES']), + DOMInfo: Filters.byProperties(['canUseDOM']), + + /* Locale/Location and Time */ + LocaleManager: Filters.byProperties(['setLocale']), + Moment: Filters.byProperties(['parseZone']), + LocationManager: Filters.byProperties(['createLocation']), + Timestamps: Filters.byProperties(['fromTimestamp']), + TimeFormatter: Filters.byProperties(['dateFormat']), + + /* Strings and Utils */ + Strings: Filters.byProperties(['TEXT', 'TEXTAREA_PLACEHOLDER']), + StringFormats: Filters.byProperties(['a', 'z']), + StringUtils: Filters.byProperties(['toASCII']), + + /* URLs and Utils */ + URLParser: Filters.byProperties(['Url', 'parse']), + ExtraURLs: Filters.byProperties(['getArticleURL']), + + /* Text Processing */ + hljs: Filters.byProperties(['highlight', 'highlightBlock']), + SimpleMarkdown: Filters.byProperties(['parseBlock', 'parseInline', 'defaultOutput']), + + /* DOM/React Components */ + /* ==================== */ + LayerManager: Filters.byProperties(['popLayer', 'pushLayer']), + UserSettingsWindow: Filters.byProperties(['open', 'updateAccount']), + ChannelSettingsWindow: Filters.byProperties(['open', 'updateChannel']), + GuildSettingsWindow: Filters.byProperties(['open', 'updateGuild']), + KeyboardCombosModal: Filters.byProperties(['show', 'activateRagingDemon']), + + /* Modals */ + ModalStack: Filters.byProperties(['push', 'update', 'pop', 'popWithKey']), + ConfirmModal: Filters.byPrototypeFields(['handleCancel', 'handleSubmit', 'handleMinorConfirm']), + UserProfileModal: Filters.byProperties(['fetchMutualFriends', 'setSection']), + ChangeNicknameModal: Filters.byProperties(['open', 'changeNickname']), + CreateChannelModal: Filters.byProperties(['open', 'createChannel']), + PruneMembersModal: Filters.byProperties(['open', 'prune']), + NotificationSettingsModal: Filters.byProperties(['open', 'updateNotificationSettings']), + PrivacySettingsModal: Filters.byCode(/PRIVACY_SETTINGS_MODAL_OPEN/, m => m.open), + CreateInviteModal: Filters.byProperties(['open', 'createInvite']), + + /* Popouts */ + PopoutStack: Filters.byProperties(['open', 'close', 'closeAll']), + PopoutOpener: Filters.byProperties(['openPopout']), + EmojiPicker: Filters.byPrototypeFields(['onHoverEmoji', 'selectEmoji']), + + /* Context Menus */ + ContextMenuActions: Filters.byCode(/CONTEXT_MENU_CLOSE/, c => c.close), + ContextMenuItemsGroup: Filters.byCode(/itemGroup/), + ContextMenuItem: Filters.byCode(/\.label\b.*\.hint\b.*\.action\b/), + + /* In-Message Links */ + ExternalLink: Filters.byCode(/\.trusted\b/) + +} diff --git a/client/src/modules/reflection/modules.js b/client/src/modules/reflection/modules.js index 2ba57675..77f2b6ba 100644 --- a/client/src/modules/reflection/modules.js +++ b/client/src/modules/reflection/modules.js @@ -10,179 +10,7 @@ import { Utils, Filters } from 'common'; import Events from '../events'; - -const KnownModules = { - React: Filters.byProperties(['createElement', 'cloneElement']), - ReactDOM: Filters.byProperties(['render', 'findDOMNode']), - - Events: Filters.byPrototypeFields(['setMaxListeners', 'emit']), - - /* Guild Info, Stores, and Utilities */ - GuildStore: Filters.byProperties(['getGuild']), - SortedGuildStore: Filters.byProperties(['getSortedGuilds']), - SelectedGuildStore: Filters.byProperties(['getLastSelectedGuildId']), - GuildSync: Filters.byProperties(['getSyncedGuilds']), - GuildInfo: Filters.byProperties(['getAcronym']), - GuildChannelsStore: Filters.byProperties(['getChannels', 'getDefaultChannel']), - GuildMemberStore: Filters.byProperties(['getMember']), - MemberCountStore: Filters.byProperties(['getMemberCounts']), - GuildEmojiStore: Filters.byProperties(['getEmojis']), - GuildActions: Filters.byProperties(['markGuildAsRead']), - GuildPermissions: Filters.byProperties(['getGuildPermissions']), - - /* Channel Store & Actions */ - ChannelStore: Filters.byProperties(['getChannels', 'getDMFromUserId']), - SelectedChannelStore: Filters.byProperties(['getLastSelectedChannelId']), - ChannelActions: Filters.byProperties(['selectChannel']), - PrivateChannelActions: Filters.byProperties(['openPrivateChannel']), - ChannelSelector: Filters.byProperties(['selectGuild', 'selectChannel']), - VoiceChannelActions: Filters.byProperties(['selectVoiceChannel']), - - /* Current User Info, State and Settings */ - UserInfoStore: Filters.byProperties(['getToken']), - UserSettingsStore: Filters.byProperties(['guildPositions']), - AccountManager: Filters.byProperties(['register', 'login']), - UserSettingsUpdater: Filters.byProperties(['updateRemoteSettings']), - OnlineWatcher: Filters.byProperties(['isOnline']), - CurrentUserIdle: Filters.byProperties(['getIdleTime']), - RelationshipStore: Filters.byProperties(['isBlocked', 'isFriend']), - RelationshipManager: Filters.byProperties(['addRelationship']), - MentionStore: Filters.byProperties(['getMentions']), - - /* User Stores and Utils */ - UserStore: Filters.byProperties(['getCurrentUser']), - UserStatusStore: Filters.byProperties(['getStatuses']), - UserTypingStore: Filters.byProperties(['isTyping']), - UserActivityStore: Filters.byProperties(['getActivity']), - UserNameResolver: Filters.byProperties(['getName']), - UserNoteStore: Filters.byProperties(['getNote']), - UserNoteActions: Filters.byProperties(['updateNote']), - DraftActions: Filters.byProperties(['changeDraft']), - - /* Emoji Store and Utils */ - EmojiInfo: Filters.byProperties(['isEmojiDisabled']), - EmojiUtils: Filters.byProperties(['getGuildEmoji']), - EmojiStore: Filters.byProperties(['getByCategory', 'EMOJI_NAME_RE']), - - /* Invite Store and Utils */ - InviteStore: Filters.byProperties(['getInvites']), - InviteResolver: Filters.byProperties(['findInvite']), - InviteActions: Filters.byProperties(['acceptInvite']), - - /* Discord Objects & Utils */ - DiscordConstants: Filters.byProperties(['Permissions', 'ActivityTypes', 'StatusTypes']), - Permissions: Filters.byProperties(['getHighestRole']), - ColorConverter: Filters.byProperties(['hex2int']), - ColorShader: Filters.byProperties(['darken']), - TinyColor: Filters.byPrototypeFields(['toRgb']), - ClassResolver: Filters.byProperties(['getClass']), - ButtonData: Filters.byProperties(['ButtonSizes']), - IconNames: Filters.byProperties(['IconNames']), - NavigationUtils: Filters.byProperties(['transitionTo', 'replaceWith', 'getHistory']), - - /* Discord Messages */ - MessageStore: Filters.byProperties(['getMessages']), - MessageActions: Filters.byProperties(['jumpToMessage', '_sendMessage']), - MessageQueue: Filters.byProperties(['enqueue']), - MessageParser: Filters.byProperties(['createMessage', 'parse', 'unparse']), - - /* In-Game Overlay */ - OverlayUserPopoutSettings: Filters.byProperties(['openUserPopout']), - OverlayUserPopoutInfo: Filters.byProperties(['getOpenedUserPopout']), - - /* Experiments */ - ExperimentStore: Filters.byProperties(['getExperimentOverrides']), - ExperimentsManager: Filters.byProperties(['isDeveloper']), - CurrentExperiment: Filters.byProperties(['getExperimentId']), - - /* Images, Avatars and Utils */ - ImageResolver: Filters.byProperties(['getUserAvatarURL']), - ImageUtils: Filters.byProperties(['getSizedImageSrc']), - AvatarDefaults: Filters.byProperties(['getUserAvatarURL', 'DEFAULT_AVATARS']), - - /* Drag & Drop */ - DNDActions: Filters.byProperties(['beginDrag']), - DNDSources: Filters.byProperties(['addTarget']), - DNDObjects: Filters.byProperties(['DragSource']), - - /* Electron & Other Internals with Utils */ - ElectronModule: Filters.byProperties(['_getMainWindow']), - Dispatcher: Filters.byProperties(['dirtyDispatch']), - PathUtils: Filters.byProperties(['hasBasename']), - NotificationModule: Filters.byProperties(['showNotification']), - RouterModule: Filters.byProperties(['Router']), - APIModule: Filters.byProperties(['getAPIBaseURL']), - AnalyticEvents: Filters.byProperties(['AnalyticEventConfigs']), - KeyGenerator: Filters.byCode(/"binary"/), - Buffers: Filters.byProperties(['Buffer', 'kMaxLength']), - DeviceStore: Filters.byProperties(['getDevices']), - SoftwareInfo: Filters.byProperties(['os']), - CurrentContext: Filters.byProperties(['setTagsContext']), - - /* Media Stuff (Audio/Video) */ - MediaDeviceInfo: Filters.byProperties(['Codecs', 'SUPPORTED_BROWSERS']), - MediaInfo: Filters.byProperties(['getOutputVolume']), - MediaEngineInfo: Filters.byProperties(['MediaEngineFeatures']), - VoiceInfo: Filters.byProperties(['EchoCancellation']), - VideoStream: Filters.byProperties(['getVideoStream']), - SoundModule: Filters.byProperties(['playSound']), - - /* Window, DOM, HTML */ - WindowInfo: Filters.byProperties(['isFocused', 'windowSize']), - TagInfo: Filters.byProperties(['VALID_TAG_NAMES']), - DOMInfo: Filters.byProperties(['canUseDOM']), - - /* Locale/Location and Time */ - LocaleManager: Filters.byProperties(['setLocale']), - Moment: Filters.byProperties(['parseZone']), - LocationManager: Filters.byProperties(['createLocation']), - Timestamps: Filters.byProperties(['fromTimestamp']), - TimeFormatter: Filters.byProperties(['dateFormat']), - - /* Strings and Utils */ - Strings: Filters.byProperties(['TEXT', 'TEXTAREA_PLACEHOLDER']), - StringFormats: Filters.byProperties(['a', 'z']), - StringUtils: Filters.byProperties(['toASCII']), - - /* URLs and Utils */ - URLParser: Filters.byProperties(['Url', 'parse']), - ExtraURLs: Filters.byProperties(['getArticleURL']), - - /* Text Processing */ - hljs: Filters.byProperties(['highlight', 'highlightBlock']), - SimpleMarkdown: Filters.byProperties(['parseBlock', 'parseInline', 'defaultOutput']), - - /* DOM/React Components */ - /* ==================== */ - LayerManager: Filters.byProperties(['popLayer', 'pushLayer']), - UserSettingsWindow: Filters.byProperties(['open', 'updateAccount']), - ChannelSettingsWindow: Filters.byProperties(['open', 'updateChannel']), - GuildSettingsWindow: Filters.byProperties(['open', 'updateGuild']), - - /* Modals */ - ModalStack: Filters.byProperties(['push', 'update', 'pop', 'popWithKey']), - ConfirmModal: Filters.byPrototypeFields(['handleCancel', 'handleSubmit', 'handleMinorConfirm']), - UserProfileModal: Filters.byProperties(['fetchMutualFriends', 'setSection']), - ChangeNicknameModal: Filters.byProperties(['open', 'changeNickname']), - CreateChannelModal: Filters.byProperties(['open', 'createChannel']), - PruneMembersModal: Filters.byProperties(['open', 'prune']), - NotificationSettingsModal: Filters.byProperties(['open', 'updateNotificationSettings']), - PrivacySettingsModal: Filters.byCode(/PRIVACY_SETTINGS_MODAL_OPEN/, m => m.open), - CreateInviteModal: Filters.byProperties(['open', 'createInvite']), - - /* Popouts */ - PopoutStack: Filters.byProperties(['open', 'close', 'closeAll']), - PopoutOpener: Filters.byProperties(['openPopout']), - EmojiPicker: Filters.byPrototypeFields(['onHoverEmoji', 'selectEmoji']), - - /* Context Menus */ - ContextMenuActions: Filters.byCode(/CONTEXT_MENU_CLOSE/, c => c.close), - ContextMenuItemsGroup: Filters.byCode(/itemGroup/), - ContextMenuItem: Filters.byCode(/\.label\b.*\.hint\b.*\.action\b/), - - /* In-Message Links */ - ExternalLink: Filters.byCode(/\.trusted\b/) -}; +import KnownModules from './knownmodules'; class Module { diff --git a/client/src/structs/discord/channel.js b/client/src/structs/discord/channel.js index d7833092..e87056f4 100644 --- a/client/src/structs/discord/channel.js +++ b/client/src/structs/discord/channel.js @@ -36,6 +36,8 @@ export class Channel { case 2: return new GuildVoiceChannel(channel); case 3: return new GroupChannel(channel); case 4: return new ChannelCategory(channel); + case 5: return new GuildNewsChannel(channel); + case 6: return new GuildStoreChannel(channel); } } @@ -48,6 +50,8 @@ export class Channel { static get GuildTextChannel() { return GuildTextChannel } static get GuildVoiceChannel() { return GuildVoiceChannel } static get ChannelCategory() { return ChannelCategory } + static get GuildNewsChannel() { return GuildNewsChannel } + static get GuildStoreChannel() { return GuildStoreChannel } static get PrivateChannel() { return PrivateChannel } static get DirectMessageChannel() { return DirectMessageChannel } static get GroupChannel() { return GroupChannel } @@ -59,9 +63,9 @@ export class Channel { /** * Send a message in this channel. - * @param {String} content The new message's content - * @param {Boolean} parse Whether to parse the message or send it as it is - * @return {Promise => Message} + * @param {string} content The new message's content + * @param {boolean} [parse=false] Whether to parse the message or send it as it is + * @return {Promise} */ async sendMessage(content, parse = false) { if (this.assertPermissions) this.assertPermissions('SEND_MESSAGES', Modules.DiscordPermissions.VIEW_CHANNEL | Modules.DiscordPermissions.SEND_MESSAGES); @@ -77,7 +81,7 @@ export class Channel { /** * Send a bot message in this channel that only the current user can see. - * @param {String} content The new message's content + * @param {string} content The new message's content * @return {Message} */ sendBotMessage(content) { @@ -111,8 +115,8 @@ export class Channel { /** * Sends an invite in this channel. - * @param {String} code The invite code - * @return {Promise => Messaage} + * @param {string} code The invite code + * @return {Promise} */ async sendInvite(code) { if (this.assertPermissions) this.assertPermissions('SEND_MESSAGES', Modules.DiscordPermissions.VIEW_CHANNEL | Modules.DiscordPermissions.SEND_MESSAGES); @@ -130,7 +134,7 @@ export class Channel { /** * Whether this channel is currently selected. - * @type {Boolean} + * @type {boolean} */ get isSelected() { return DiscordApi.currentChannel === this; @@ -138,6 +142,7 @@ export class Channel { /** * Updates this channel. + * @param {Object} body Data to send in the API request * @return {Promise} */ async updateChannel(body) { @@ -153,12 +158,25 @@ export class Channel { } +Channel.GUILD_TEXT = 0; +Channel.DM = 1; +Channel.GUILD_VOICE = 2; +Channel.GROUP_DM = 3; +Channel.GUILD_CATEGORY = 4; +Channel.GUILD_NEWS = 5; +Channel.GUILD_STORE = 6; + export class PermissionOverwrite { constructor(data, channel_id) { this.discordObject = data; this.channelId = channel_id; } + /** + * @param {Object} data + * @param {number} channel_id + * @return {PermissionOverwrite} + */ static from(data, channel_id) { switch (data.type) { default: return new PermissionOverwrite(data, channel_id); @@ -174,10 +192,16 @@ export class PermissionOverwrite { get allow() { return this.discordObject.allow } get deny() { return this.discordObject.deny } + /** + * @type {?Channel} + */ get channel() { return Channel.fromId(this.channelId); } + /** + * @type {?Guild} + */ get guild() { return this.channel ? this.channel.guild : null; } @@ -186,6 +210,9 @@ export class PermissionOverwrite { export class RolePermissionOverwrite extends PermissionOverwrite { get roleId() { return this.discordObject.id } + /** + * @type {?Role} + */ get role() { return this.guild ? this.guild.roles.find(r => r.id === this.roleId) : null; } @@ -194,6 +221,9 @@ export class RolePermissionOverwrite extends PermissionOverwrite { export class MemberPermissionOverwrite extends PermissionOverwrite { get memberId() { return this.discordObject.id } + /** + * @type {GuildMember} + */ get member() { return GuildMember.fromId(this.memberId); } @@ -208,34 +238,45 @@ export class GuildChannel extends Channel { get nicks() { return this.discordObject.nicks } checkPermissions(perms) { - return Modules.PermissionUtils.can(perms, DiscordApi.currentUser, this.discordObject); + return Modules.PermissionUtils.can(perms, DiscordApi.currentUser.discordObject, this.discordObject); } assertPermissions(name, perms) { if (!this.checkPermissions(perms)) throw new InsufficientPermissions(name); } + /** + * @type {?ChannelCategory} + */ get category() { return Channel.fromId(this.parentId); } /** * The current user's permissions on this channel. + * @type {number} */ get permissions() { return Modules.GuildPermissions.getChannelPermissions(this.id); } + /** + * @type {List} + */ get permissionOverwrites() { - return List.from(Object.entries(this.discordObject.permissionOverwrites), ([i, p]) => PermissionOverwrite.from(p, this.id)); + return List.from(Object.values(this.discordObject.permissionOverwrites), p => PermissionOverwrite.from(p, this.id)); } + /** + * @type {Guild} + */ get guild() { return Guild.fromId(this.guildId); } /** * Whether this channel is the guild's default channel. + * @type {boolean} */ get isDefaultChannel() { return Modules.GuildChannelsStore.getDefaultChannel(this.guildId).id === this.id; @@ -243,16 +284,16 @@ export class GuildChannel extends Channel { /** * Opens this channel's settings window. - * @param {String} section The section to open (see DiscordConstants.ChannelSettingsSections) + * @param {string} section The section to open (see DiscordConstants.ChannelSettingsSections) */ openSettings(section = 'OVERVIEW') { - Modules.ChannelSettingsWindow.setSection(section); Modules.ChannelSettingsWindow.open(this.id); + Modules.ChannelSettingsWindow.setSection(section); } /** * Updates this channel's name. - * @param {String} name The channel's new name + * @param {string} name The channel's new name * @return {Promise} */ updateName(name) { @@ -261,7 +302,7 @@ export class GuildChannel extends Channel { /** * Changes the channel's position. - * @param {Number} position The channel's new position + * @param {(GuildChannel|number)} [position=0] The channel's new position * @return {Promise} */ changeSortLocation(position = 0) { @@ -271,7 +312,7 @@ export class GuildChannel extends Channel { /** * Updates this channel's permission overwrites. - * @param {Array} permissionOverwrites An array of permission overwrites + * @param {Object[]} permission_overwrites An array of permission overwrites * @return {Promise} */ updatePermissionOverwrites(permission_overwrites) { @@ -280,7 +321,7 @@ export class GuildChannel extends Channel { /** * Updates this channel's category. - * @param {ChannelCategory} category The new channel category + * @param {?(ChannelCategory|number)} category The new channel category * @return {Promise} */ updateCategory(category) { @@ -296,7 +337,7 @@ export class GuildTextChannel extends GuildChannel { /** * Updates this channel's topic. - * @param {String} topc The new channel topic + * @param {string} topic The new channel topic * @return {Promise} */ updateTopic(topic) { @@ -305,13 +346,17 @@ export class GuildTextChannel extends GuildChannel { /** * Updates this channel's not-safe-for-work flag. - * @param {Boolean} nsfw Whether the channel should be marked as NSFW + * @param {boolean} [nsfw=true] Whether the channel should be marked as NSFW * @return {Promise} */ setNsfw(nsfw = true) { return this.updateChannel({ nsfw }); } + /** + * Updates this channel's not-safe-for-work flag. + * @return {Promise} + */ setNotNsfw() { return this.setNsfw(false); } @@ -328,11 +373,13 @@ export class GuildVoiceChannel extends GuildChannel { jumpToPresent() { throw new Error('Cannot select a voice channel.'); } get hasMoreAfter() { return false; } sendInvite() { throw new Error('Cannot invite someone to a voice channel.'); } + + // TODO: can select a voice/video channel when guild video is enabled select() { throw new Error('Cannot select a voice channel.'); } /** * Updates this channel's bitrate. - * @param {Number} bitrate The new bitrate + * @param {number} bitrate The new bitrate * @return {Promise} */ updateBitrate(bitrate) { @@ -341,7 +388,7 @@ export class GuildVoiceChannel extends GuildChannel { /** * Updates this channel's user limit. - * @param {Number} userLimit The new user limit + * @param {number} user_limit The new user limit * @return {Promise} */ updateUserLimit(user_limit) { @@ -365,6 +412,7 @@ export class ChannelCategory extends GuildChannel { /** * A list of channels in this category. + * @type {List} */ get channels() { return List.from(this.guild.channels.filter(c => c.parentId === this.id)); @@ -372,23 +420,105 @@ export class ChannelCategory extends GuildChannel { /** * Opens the create channel modal for this guild. - * @param {Number} type The type of channel to create - either 0 (text), 2 (voice) or 4 (category) + * @param {number} type The type of channel to create - either 0 (text), 2 (voice/video), 5 (news) or 6 (store) * @param {GuildChannel} clone A channel to clone permissions of */ - openCreateChannelModal(type, category, clone) { + openCreateChannelModal(type, clone) { this.guild.openCreateChannelModal(type, this.id, this, clone); } + /** + * Opens the create channel modal for this guild with type 0 (text). + * @param {GuildChannel} clone A channel to clone permissions of + */ + openCreateTextChannelModal(clone) { + this.guild.openCreateChannelModal(Channel.GUILD_TEXT, this.id, this, clone); + } + + /** + * Opens the create channel modal for this guild with type 2 (voice). + * @param {GuildChannel} clone A channel to clone permissions of + */ + openCreateVoiceChannelModal(clone) { + this.guild.openCreateChannelModal(Channel.GUILD_VOICE, this.id, this, clone); + } + + /** + * Opens the create channel modal for this guild with type 5 (news). + * @param {GuildChannel} clone A channel to clone permissions of + */ + openCreateNewsChannelModal(clone) { + this.guild.openCreateChannelModal(Channel.GUILD_NEWS, this.id, this, clone); + } + + /** + * Opens the create channel modal for this guild with type 6 (store). + * @param {GuildChannel} clone A channel to clone permissions of + */ + openCreateStoreChannelModal(clone) { + this.guild.openCreateChannelModal(Channel.GUILD_STORE, this.id, this, clone); + } + /** * Creates a channel in this category. - * @param {Number} type The type of channel to create - either 0 (text) or 2 (voice) - * @param {String} name A name for the new channel - * @param {Array} permission_overwrites An array of PermissionOverwrite-like objects - leave to use the permissions of the category - * @return {Promise => GuildChannel} + * @param {number} type The type of channel to create - either 0 (text) or 2 (voice) + * @param {string} name A name for the new channel + * @param {Object[]} permission_overwrites An array of PermissionOverwrite-like objects - leave to use the permissions of the category + * @return {Promise} */ createChannel(type, name, permission_overwrites) { return this.guild.createChannel(type, name, this, permission_overwrites); } + + /** + * Creates a channel in this category. + * @param {string} name A name for the new channel + * @param {Object[]} permission_overwrites An array of PermissionOverwrite-like objects - leave to use the permissions of the category + * @return {Promise} + */ + createTextChannel(name, permission_overwrites) { + return this.guild.createChannel(Channel.GUILD_TEXT, name, this, permission_overwrites); + } + + /** + * Creates a channel in this category. + * @param {string} name A name for the new channel + * @param {Object[]} permission_overwrites An array of PermissionOverwrite-like objects - leave to use the permissions of the category + * @return {Promise} + */ + createVoiceChannel(name, permission_overwrites) { + return this.guild.createChannel(Channel.GUILD_VOICE, name, this, permission_overwrites); + } + + /** + * Creates a channel in this category. + * @param {string} name A name for the new channel + * @param {Object[]} permission_overwrites An array of PermissionOverwrite-like objects - leave to use the permissions of the category + * @return {Promise} + */ + createNewsChannel(name, permission_overwrites) { + return this.guild.createChannel(Channel.GUILD_NEWS, name, this, permission_overwrites); + } + + /** + * Creates a channel in this category. + * @param {string} name A name for the new channel + * @param {Object[]} permission_overwrites An array of PermissionOverwrite-like objects - leave to use the permissions of the category + * @return {Promise} + */ + createStoreChannel(name, permission_overwrites) { + return this.guild.createChannel(Channel.GUILD_STORE, name, this, permission_overwrites); + } +} + +// Type 5 - GUILD_NEWS +export class GuildNewsChannel extends GuildTextChannel { + get type() { return 'GUILD_NEWS' } +} + +// Type 6 - GUILD_STORE +export class GuildStoreChannel extends GuildChannel { + get type() { return 'GUILD_STORE' } } export class PrivateChannel extends Channel { @@ -403,6 +533,7 @@ export class DirectMessageChannel extends PrivateChannel { /** * The other user of this direct message channel. + * @type {User} */ get recipient() { return User.fromId(this.recipientId); @@ -418,6 +549,7 @@ export class GroupChannel extends PrivateChannel { /** * A list of the other members of this group direct message channel. + * @type {List} */ get members() { return List.from(this.discordObject.recipients, id => User.fromId(id)); @@ -425,6 +557,7 @@ export class GroupChannel extends PrivateChannel { /** * The owner of this group direct message channel. This is usually the person who created it. + * @type {User} */ get owner() { return User.fromId(this.ownerId); @@ -432,7 +565,7 @@ export class GroupChannel extends PrivateChannel { /** * Updates this channel's name. - * @param {String} name The channel's new name + * @param {string} name The channel's new name * @return {Promise} */ updateName(name) { diff --git a/client/src/structs/discord/guild.js b/client/src/structs/discord/guild.js index 16109938..174dcea7 100644 --- a/client/src/structs/discord/guild.js +++ b/client/src/structs/discord/guild.js @@ -39,6 +39,9 @@ export class Role { get colour() { return this.discordObject.color } get colourString() { return this.discordObject.colorString } + /** + * @type {Guild} + */ get guild() { return Guild.fromId(this.guildId); } @@ -71,6 +74,9 @@ export class Emoji { get url() { return this.discordObject.url } get roles() { return this.discordObject.roles } + /** + * @type {Guild} + */ get guild() { return Guild.fromId(this.guildId); } @@ -122,15 +128,24 @@ export class Guild { get splash() { return this.discordObject.splash } get features() { return this.discordObject.features } + /** + * @type {GuildMember} + */ get owner() { return this.members.find(m => m.userId === this.ownerId); } + /** + * @type {List} + */ get roles() { - return List.from(Object.entries(this.discordObject.roles), ([i, r]) => new Role(r, this.id)) + return List.from(Object.values(this.discordObject.roles), r => new Role(r, this.id)) .sort((r1, r2) => r1.position === r2.position ? 0 : r1.position > r2.position ? 1 : -1); } + /** + * @type {List} + */ get channels() { const channels = Modules.GuildChannelsStore.getChannels(this.id); const returnChannels = new List(); @@ -150,6 +165,7 @@ export class Guild { /** * Channels that don't have a parent. (Channel categories and any text/voice channel not in one.) + * @type {List} */ get mainChannels() { return this.channels.filter(c => !c.parentId); @@ -157,6 +173,7 @@ export class Guild { /** * The guild's default channel. (Usually the first in the list.) + * @type {?GuildTextChannel} */ get defaultChannel() { return Channel.from(Modules.GuildChannelsStore.getDefaultChannel(this.id)); @@ -164,6 +181,7 @@ export class Guild { /** * The guild's AFK channel. + * @type {?GuildVoiceChannel} */ get afkChannel() { return this.afkChannelId ? Channel.fromId(this.afkChannelId) : null; @@ -171,6 +189,7 @@ export class Guild { /** * The channel system messages are sent to. + * @type {?GuildTextChannel} */ get systemChannel() { return this.systemChannelId ? Channel.fromId(this.systemChannelId) : null; @@ -178,6 +197,7 @@ export class Guild { /** * A list of GuildMember objects. + * @type {List} */ get members() { const members = Modules.GuildMemberStore.getMembers(this.id); @@ -186,6 +206,7 @@ export class Guild { /** * The current user as a GuildMember of this guild. + * @type {GuildMember} */ get currentUser() { return this.members.find(m => m.user === DiscordApi.currentUser); @@ -193,6 +214,7 @@ export class Guild { /** * The total number of members in the guild. + * @type {number} */ get memberCount() { return Modules.MemberCountStore.getMemberCount(this.id); @@ -200,6 +222,7 @@ export class Guild { /** * An array of the guild's custom emojis. + * @type {List} */ get emojis() { return List.from(Modules.EmojiUtils.getGuildEmoji(this.id), e => new Emoji(e, this.id)); @@ -215,6 +238,7 @@ export class Guild { /** * The current user's permissions on this guild. + * @type {number} */ get permissions() { return Modules.GuildPermissions.getGuildPermissions(this.id); @@ -222,8 +246,8 @@ export class Guild { /** * Returns the GuildMember object for a user. - * @param {User|GuildMember|Number} user A User or GuildMember object or a user ID - * @return {GuildMember} + * @param {(User|GuildMember|number)} user A User or GuildMember object or a user ID + * @return {?GuildMember} */ getMember(user) { const member = Modules.GuildMemberStore.getMember(this.id, user.userId || user.id || user); @@ -232,8 +256,8 @@ export class Guild { /** * Checks if a user is a member of this guild. - * @param {User|GuildMember|Number} user A User or GuildMember object or a user ID - * @return {Boolean} + * @param {(User|GuildMember|number)} user A User or GuildMember object or a user ID + * @return {boolean} */ isMember(user) { return Modules.GuildMemberStore.isMember(this.id, user.userId || user.id || user); @@ -241,6 +265,7 @@ export class Guild { /** * Whether the user has not restricted direct messages from members of this guild. + * @type {boolean} */ get allowPrivateMessages() { return !DiscordApi.UserSettings.restrictedGuildIds.includes(this.id); @@ -262,6 +287,7 @@ export class Guild { /** * Whether this guild is currently selected. + * @type {boolean} */ get isSelected() { return DiscordApi.currentGuild === this; @@ -269,16 +295,16 @@ export class Guild { /** * Opens this guild's settings window. - * @param {String} section The section to open (see DiscordConstants.GuildSettingsSections) + * @param {string} section The section to open (see DiscordConstants.GuildSettingsSections) */ openSettings(section = 'OVERVIEW') { - Modules.GuildSettingsWindow.setSection(section); Modules.GuildSettingsWindow.open(this.id); + Modules.GuildSettingsWindow.setSection(section); } /** * Kicks members who don't have any roles and haven't been seen in the number of days passed. - * @param {Number} days + * @param {number} days */ pruneMembers(days) { this.assertPermissions('KICK_MEMBERS', Modules.DiscordPermissions.KICK_MEMBERS); @@ -302,12 +328,56 @@ export class Guild { } /** - * Creates a channel in this guild. - * @param {Number} type The type of channel to create - either 0 (text), 2 (voice) or 4 (category) - * @param {String} name A name for the new channel + * Opens the create channel modal for this guild. * @param {ChannelCategory} category The category to create the channel in - * @param {Array} permission_overwrites An array of PermissionOverwrite-like objects - leave to use the permissions of the category - * @return {Promise => GuildChannel} + * @param {GuildChannel} clone A channel to clone permissions, topic, bitrate and user limit of + */ + openCreateTextChannelModal(category, clone) { + return this.openCreateChannelModal(Channel.GUILD_TEXT, category, clone); + } + + /** + * Opens the create channel modal for this guild. + * @param {ChannelCategory} category The category to create the channel in + * @param {GuildChannel} clone A channel to clone permissions, topic, bitrate and user limit of + */ + openCreateVoiceChannelModal(category, clone) { + return this.openCreateChannelModal(Channel.GUILD_VOICE, category, clone); + } + + /** + * Opens the create channel modal for this guild. + * @param {GuildChannel} clone A channel to clone permissions, topic, bitrate and user limit of + */ + openCreateChannelCategoryModal(clone) { + return this.openCreateChannelModal(Channel.GUILD_CATEGORY, undefined, clone); + } + + /** + * Opens the create channel modal for this guild. + * @param {ChannelCategory} category The category to create the channel in + * @param {GuildChannel} clone A channel to clone permissions, topic, bitrate and user limit of + */ + openCreateNewsChannelModal(category, clone) { + return this.openCreateChannelModal(Channel.GUILD_NEWS, category, clone); + } + + /** + * Opens the create channel modal for this guild. + * @param {ChannelCategory} category The category to create the channel in + * @param {GuildChannel} clone A channel to clone permissions, topic, bitrate and user limit of + */ + openCreateStoreChannelModal(category, clone) { + return this.openCreateChannelModal(Channel.GUILD_STORE, category, clone); + } + + /** + * Creates a channel in this guild. + * @param {number} type The type of channel to create - either 0 (text), 2 (voice), 4 (category), 5 (news) or 6 (store) + * @param {string} name A name for the new channel + * @param {ChannelCategory} category The category to create the channel in + * @param {Object[]} permission_overwrites An array of PermissionOverwrite-like objects - leave to use the permissions of the category + * @return {Promise} */ async createChannel(type, name, category, permission_overwrites) { this.assertPermissions('MANAGE_CHANNELS', Modules.DiscordPermissions.MANAGE_CHANNELS); @@ -328,6 +398,60 @@ export class Guild { return Channel.fromId(response.body.id); } + /** + * Creates a channel in this guild. + * @param {string} name A name for the new channel + * @param {ChannelCategory} category The category to create the channel in + * @param {GuildChannel} clone A channel to clone permissions, topic, bitrate and user limit of + * @return {Promise} + */ + createTextChannel(name, category, clone) { + return this.createChannel(Channel.GUILD_TEXT, name, category, clone); + } + + /** + * Creates a channel in this guild. + * @param {string} name A name for the new channel + * @param {ChannelCategory} category The category to create the channel in + * @param {GuildChannel} clone A channel to clone permissions, topic, bitrate and user limit of + * @return {Promise} + */ + createVoiceChannel(name, category, clone) { + return this.createChannel(Channel.GUILD_VOICE, name, category, clone); + } + + /** + * Creates a channel in this guild. + * @param {string} name A name for the new channel + * @param {GuildChannel} clone A channel to clone permissions, topic, bitrate and user limit of + * @return {Promise} + */ + createChannelCategory(name, clone) { + return this.createChannel(Channel.GUILD_CATEGORY, name, clone); + } + + /** + * Creates a channel in this guild. + * @param {string} name A name for the new channel + * @param {ChannelCategory} category The category to create the channel in + * @param {GuildChannel} clone A channel to clone permissions, topic, bitrate and user limit of + * @return {Promise} + */ + createNewsChannel(name, category, clone) { + return this.createChannel(Channel.GUILD_NEWS, name, category, clone); + } + + /** + * Creates a channel in this guild. + * @param {string} name A name for the new channel + * @param {ChannelCategory} category The category to create the channel in + * @param {GuildChannel} clone A channel to clone permissions, topic, bitrate and user limit of + * @return {Promise} + */ + createStoreChannel(name, category, clone) { + return this.createChannel(Channel.GUILD_STORE, name, category, clone); + } + openNotificationSettingsModal() { Modules.NotificationSettingsModal.open(this.id); } diff --git a/tests/ext/plugins/User Settings/config.json b/tests/ext/plugins/User Settings/config.json new file mode 100644 index 00000000..7fafa65b --- /dev/null +++ b/tests/ext/plugins/User Settings/config.json @@ -0,0 +1,18 @@ +{ + "info": { + "id": "user-settings-example", + "name": "User Settings Example", + "authors": [ + { + "name": "Samuel Elliott", + "url": "https://samuelelliott.ml", + "discord_id": "284056145272766465", + "github_username": "samuelthomas2774", + "twitter_username": "_samuelelliott" + } + ], + "version": "1.0" + }, + "main": "index.js", + "mainExport": "default" +} diff --git a/tests/ext/plugins/User Settings/index.js b/tests/ext/plugins/User Settings/index.js new file mode 100644 index 00000000..516239b2 --- /dev/null +++ b/tests/ext/plugins/User Settings/index.js @@ -0,0 +1,136 @@ +exports.default = (Plugin, {Logger, DiscordApi, BdMenuItems, CommonComponents, Api}) => class UserSettingsTest extends Plugin { + onstart() { + DiscordApi.UserSettings.theme = DiscordApi.UserSettings.ThemeDark; // === 'dark' + + this.menu_item = BdMenuItems.addVueComponent('Test', 'User Settings', { + template: ` +

Status

+ + +

Explicit content filter

+ + +

Default guilds restricted

+ + +

Friend source flags

+

{{ JSON.stringify(UserSettings.friendSourceFlags)}}

+ +

Friend source everyone

+ +

Friend source mutual friends

+ +

Friend source mutual guilds

+ +

Friend source anyone

+ + +

Detect platform accounts

+ + +

AFK timeout

+ + +

Show current game

+ + +

Inline attachment media

+ + +

Inline embed media

+ + +

Autoplay GIFs

+ + +

Show embeds

+ + +

Show reactions

+ + +

Show spoilers

+ + +

Animate emoji

+ + +

Convert emoticons

+ + +

Allow TTS messages

+ + +

Theme

+ + +

Compact mode

+ + +

Colourblind mode

+ + +

Show activity tab

+ + +

Developer mode

+ + +

Locale

+

{{ JSON.stringify(UserSettings.locale)}}

+ +

Timezone offset

+

{{ JSON.stringify(UserSettings.timezoneOffset)}}

+
`, + components: { + SettingsWrapper: CommonComponents.SettingsWrapper, + SettingSwitch: CommonComponents.SettingSwitch, + RadioGroup: CommonComponents.RadioGroup + }, + data() { return { + Api, plugin: Api.plugin, UserSettings: DiscordApi.UserSettings, + + statusOptions: [ + {value: DiscordApi.UserSettings.StatusOnline, text: 'Online'}, + {value: DiscordApi.UserSettings.StatusIdle, text: 'Idle'}, + {value: DiscordApi.UserSettings.StatusDND, text: 'Do not disturb'}, + {value: DiscordApi.UserSettings.StatusInvisible, text: 'Invisible'} + ], + + explicitContentFilterOptions: [ + {value: DiscordApi.UserSettings.ExplicitContentFilterDisabled, text: 'Disabled'}, + {value: DiscordApi.UserSettings.ExplicitContentFilterExceptFriends, text: 'Except friends'}, + {value: DiscordApi.UserSettings.ExplicitContentFilterEnabled, text: 'Enabled'} + ], + + afkTimeoutOptions: [ + {value: DiscordApi.UserSettings.AfkTimeout1Minute, text: '1 minute'}, + {value: DiscordApi.UserSettings.AfkTimeout2Minutes, text: '2 minutes'}, + {value: DiscordApi.UserSettings.AfkTimeout3Minutes, text: '3 minutes'}, + {value: DiscordApi.UserSettings.AfkTimeout4Minutes, text: '4 minutes'}, + {value: DiscordApi.UserSettings.AfkTimeout5Minutes, text: '5 minutes'}, + {value: DiscordApi.UserSettings.AfkTimeout6Minutes, text: '6 minutes'}, + {value: DiscordApi.UserSettings.AfkTimeout7Minutes, text: '7 minutes'}, + {value: DiscordApi.UserSettings.AfkTimeout8Minutes, text: '8 minutes'}, + {value: DiscordApi.UserSettings.AfkTimeout9Minutes, text: '9 minutes'}, + {value: DiscordApi.UserSettings.AfkTimeout10Minutes, text: '10 minutes'} + ], + + showSpoilersOptions: [ + {value: DiscordApi.UserSettings.ShowSpoilersOnClick, text: 'On click'}, + {value: DiscordApi.UserSettings.ShowSpoilersIfModerator, text: 'If moderator'}, + {value: DiscordApi.UserSettings.ShowSpoilersAlways, text: 'Always'} + ], + + themeOptions: [ + {value: DiscordApi.UserSettings.ThemeDark, text: 'Dark'}, + {value: DiscordApi.UserSettings.ThemeLight, text: 'Light'} + ] + }; } + }); + } + + onstop() { + BdMenuItems.removeAll(); + } +}