diff --git a/client/src/index.js b/client/src/index.js index 9a4e9205..b7bb9f3d 100644 --- a/client/src/index.js +++ b/client/src/index.js @@ -10,7 +10,7 @@ import { DOM, BdUI, Modals } from 'ui'; import BdCss from './styles/index.scss'; -import { Events, CssEditor, Globals, ExtModuleManager, PluginManager, ThemeManager, ModuleManager, WebpackModules, Settings, Database } from 'modules'; +import { Events, CssEditor, Globals, ExtModuleManager, PluginManager, ThemeManager, ModuleManager, WebpackModules, Settings, Database, DiscordApi } from 'modules'; import { ClientLogger as Logger, ClientIPC } from 'common'; import { EmoteModule } from 'builtin'; const ignoreExternal = false; @@ -18,6 +18,7 @@ const ignoreExternal = false; class BetterDiscord { constructor() { + window.discordApi = DiscordApi; window.bddb = Database; window.bdglobals = Globals; window.ClientIPC = ClientIPC; diff --git a/client/src/modules/discordapi.js b/client/src/modules/discordapi.js new file mode 100644 index 00000000..645d04ef --- /dev/null +++ b/client/src/modules/discordapi.js @@ -0,0 +1,405 @@ +import WebpackModules from './webpackmodules'; +import { $ } from 'vendor'; + +class List extends Array { + + constructor() { + super(...arguments); + } + + get(...filters) { + return this.find(item => { + for (let filter of filters) { + for (let key in filter) { + if (item[key] !== filter[key]) return false; + } + } + return true; + }); + } + +} + +class PermissionsError extends Error { + constructor(message) { + super(message) + this.name = "PermissionsError"; + } +} + +class InsufficientPermissions extends PermissionsError { + constructor(message) { + super(`Missing Permission — ${message}`) + this.name = "InsufficientPermissions"; + } +} + +const Modules = { + _getModule(name) { + let foundModule = WebpackModules.getModuleByName(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 DiscordPermissions() { return this.DiscordConstants.Permissions; } + +}; + +class User { + constructor(data) { + for (let key in data) this[key] = data[key]; + this.discordObject = data; + } + + static fromId(id) { + return new User(Modules.UserStore.getUser(id)); + } + + async sendMessage(content, parse = true) { + let id = await Modules.PrivateChannelActions.ensurePrivateChannel(DiscordApi.currentUser.id, this.id); + let channel = new PrivateChannel(Modules.ChannelStore.getChannel(id)); + channel.sendMessage(content, parse); + } + + get isFriend() { + return Modules.RelationshipStore.isFriend(this.id); + } + + get isBlocked() { + return Modules.RelationshipStore.isBlocked(this.id); + } + + addFriend() { + Modules.RelationshipManager.addRelationship(this.id, {location: "Context Menu"}); + } + + removeFriend() { + Modules.RelationshipManager.removeRelationship(this.id, {location: "Context Menu"}); + } + + block() { + Modules.RelationshipManager.addRelationship(this.id, {location: "Context Menu"}, Modules.DiscordConstants.RelationshipTypes.BLOCKED); + } + + unblock() { + Modules.RelationshipManager.removeRelationship(this.id, {location: "Context Menu"}); + } +} + +class Member extends User { + constructor(data, guild) { + super(data); + const userData = Modules.UserStore.getUser(data.userId); + for (let key in userData) this[key] = userData[key]; + this.guild_id = guild; + } + + checkPermissions(perms) { + return Modules.PermissionUtils.can(perms, DiscordApi.currentUser, Modules.GuildStore.getGuild(this.guild_id)); + } + + kick(reason = "") { + if (!this.checkPermissions(Modules.DiscordPermissions.KICK_MEMBERS)) throw new InsufficientPermissions("KICK_MEMBERS"); + Modules.GuildActions.kickUser(this.guild_id, this.id, reason); + } + + ban(daysToDelete = "1", reason = "") { + if (!this.checkPermissions(Modules.DiscordPermissions.BAN_MEMBERS)) throw new InsufficientPermissions("BAN_MEMBERS"); + Modules.GuildActions.banUser(this.guild_id, this.id, daysToDelete, reason); + } + + unban() { + if (!this.checkPermissions(Modules.DiscordPermissions.BAN_MEMBERS)) throw new InsufficientPermissions("BAN_MEMBERS"); + Modules.GuildActions.unbanUser(this.guild_id, this.id); + } + + move(channel_id) { + if (!this.checkPermissions(Modules.DiscordPermissions.MOVE_MEMBERS)) throw new InsufficientPermissions("MOVE_MEMBERS"); + Modules.GuildActions.setChannel(this.guild_id, this.id, channel_id); + } + + mute(active = true) { + if (!this.checkPermissions(Modules.DiscordPermissions.MUTE_MEMBERS)) throw new InsufficientPermissions("MUTE_MEMBERS"); + Modules.GuildActions.setServerMute(this.guild_id, this.id, active); + } + + unmute(active = true) { + this.mute(false); + } + + deafen(active = true) { + if (!this.checkPermissions(Modules.DiscordPermissions.DEAFEN_MEMBERS)) throw new InsufficientPermissions("DEAFEN_MEMBERS"); + Modules.GuildActions.setServerDeaf(this.guild_id, this.id, active); + } + + undeafen(active = true) { + this.deafen(false); + } +} + + +class Guild { + constructor(data) { + for (let key in data) this[key] = data[key]; + this.discordObject = data; + } + + get channels() { + const channels = Modules.GuildChannelsStore.getChannels(this.id); + const returnChannels = new List(); + for (const category in channels) { + if (!Array.isArray(channels[category])) continue; + let channelList = channels[category]; + for (const channel of channelList) { + returnChannels.push(new GuildChannel(channel.channel)); + } + } + return returnChannels; + } + + get defaultChannel() { + return new GuildChannel(Modules.GuildChannelsStore.getDefaultChannel(this.id)); + } + + get members() { + const members = Modules.GuildMemberStore.getMembers(this.id); + const returnMembers = new List(); + for (const member of members) returnMembers.push(new Member(member, this.id)); + return returnMembers; + } + + get memberCount() { + return Modules.MemberCountStore.getMemberCount(this.id); + } + + get emojis() { + return Modules.EmojiUtils.getGuildEmoji(this.id); + } + + get permissions() { + return Modules.GuildPermissions.getGuildPermissions(this.id); + } + + getMember(userId) { + return Modules.GuildMemberStore.getMember(this.id, userId); + } + + isMember(userId) { + return Modules.GuildMemberStore.isMember(this.id, userId); + } + + markAsRead() { + Modules.GuildActions.markGuildAsRead(this.id); + } + + select() { + Modules.GuildActions.selectGuild(this.id); + } + + nsfwAgree() { + Modules.GuildActions.nsfwAgree(this.id); + } + + nsfwDisagree() { + Modules.GuildActions.nsfwDisagree(this.id); + } + + changeSortLocation(index) { + Modules.GuildActions.move(DiscordApi.guildPositions.indexOf(this.id), index); + } +} + +class Channel { + constructor(data) { + for (let key in data) this[key] = data[key]; + this.discordObject = data; + } + + checkPermissions(perms) { + return Modules.PermissionUtils.can(perms, DiscordApi.currentUser, this.discordObject) || this.isPrivate(); + } + + async sendMessage(content, parse = true) { + if (!this.checkPermissions(Modules.DiscordPermissions.VIEW_CHANNEL | Modules.DiscordPermissions.SEND_MESSAGES)) throw new InsufficientPermissions("SEND_MESSAGES"); + let response = {}; + if (parse) response = await Modules.MessageActions._sendMessage(this.id, Modules.MessageParser.parse(this.discordObject, content)); + else response = await Modules.MessageActions._sendMessage(this.id, {content}); + return new Message(Modules.MessageStore.getMessage(this.id, response.body.id)); + } + + get messages() { + let messages = Modules.MessageStore.getMessages(this.id).toArray(); + for (let i in messages) messages[i] = new Message(messages[i]); + return new List(...messages); + } + + jumpToPresent() { + if (!this.checkPermissions(Modules.DiscordPermissions.VIEW_CHANNEL)) throw new InsufficientPermissions("VIEW_CHANNEL"); + if (this.hasMoreAfter) Modules.MessageActions.jumpToPresent(this.id, Modules.DiscordConstants.MAX_MESSAGES_PER_CHANNEL); + else this.messages[this.messages.length - 1].jumpTo(false); + } + + get hasMoreAfter() { + return Modules.MessageStore.getMessages(this.id).hasMoreAfter; + } + + sendInvite(inviteId) { + if (!this.checkPermissions(Modules.DiscordPermissions.VIEW_CHANNEL | Modules.DiscordPermissions.SEND_MESSAGES)) throw new InsufficientPermissions("SEND_MESSAGES"); + Modules.MessageActions.sendInvite(this.id, inviteId); + } + + select() { + if (!this.checkPermissions(Modules.DiscordPermissions.VIEW_CHANNEL)) throw new InsufficientPermissions("VIEW_CHANNEL"); + Modules.NavigationUtils.transitionToGuild(this.guild_id ? this.guild_id : Modules.DiscordConstants.ME, this.id); + } +} + +class GuildChannel extends Channel { + + constructor(data) { + super(data); + } + + get permissions() { + return Modules.GuildPermissions.getChannelPermissions(this.id); + } + + get guild() { + return new Guild(Modules.GuildStore.getGuild(this.guild_id)); + } + + isDefaultChannel() { + return Modules.GuildChannelsStore.getDefaultChannel(this.guild_id).id === this.id; + } + +} + +class PrivateChannel extends Channel { + constructor(data) { + super(data); + } +} + +class Message { + constructor(data) { + for (let key in data) this[key] = data[key]; + this.discordObject = data; + } + + delete() { + Modules.MessageActions.deleteMessage(this.channel_id, this.id); + } + + // programmatically update the content + edit(content, parse = false) { + if (this.author.id !== DiscordApi.currentUser.id) return; + if (parse) Modules.MessageActions.editMessage(this.channel_id, this.id, Modules.MessageParser.parse(this.discordObject, content)); + else Modules.MessageActions.editMessage(this.channel_id, this.id, {content}); + } + + // start the editing mode of GUI + startEdit() { + if (this.author.id !== DiscordApi.currentUser.id) return; + Modules.MessageActions.startEditMessage(this.channel_id, this.id, this.content); + } + + // end editing mode of GUI + endEdit() { + Modules.MessageActions.endEditMessage(); + } + + jumpTo(flash = true) { + Modules.MessageActions.jumpToMessage(this.channel_id, this.id, flash); + } +} + +export default class DiscordApi { + + static get channels() { + const channels = Modules.ChannelStore.getChannels(); + const returnChannels = new List(); + for (const [key, value] of Object.entries(channels)) { + returnChannels.push(value.isPrivate() ? new PrivateChannel(value) : new GuildChannel(value)); + } + return returnChannels; + } + + static get guilds() { + const guilds = Modules.GuildStore.getGuilds(); + const returnGuilds = new List(); + for (const [key, value] of Object.entries(guilds)) { + returnGuilds.push(new Guild(value)); + } + return returnGuilds; + } + + static get users() { + const users = Modules.UserStore.getUsers(); + const returnUsers = new List(); + for (const [key, value] of Object.entries(users)) { + returnUsers.push(new User(value)); + } + return returnUsers; + } + + static get memberCounts() { + return Modules.MemberCountStore.getMemberCounts(); + } + + static get sortedGuilds() { + const guilds = Modules.SortedGuildStore.getSortedGuilds(); + const returnGuilds = new List(); + for (const guild of guilds) { + returnGuilds.push(new Guild(guild)); + } + return returnGuilds; + } + + static get guildPositions() { + return Modules.SortedGuildStore.guildPositions; + } + + static get currentGuild() { + return new Guild(Modules.GuildStore.getGuild(Modules.SelectedGuildStore.getGuildId())); + } + + static get currentChannel() { + let channel = Modules.ChannelStore.getChannel(Modules.SelectedChannelStore.getChannelId()); + return channel.isPrivate ? new PrivateChannel(channel) : new GuildChannel(channel); + } + + static get currentUser() { + return Modules.UserStore.getCurrentUser(); + } + + static get friends() { + const friends = Modules.RelationshipStore.getFriendIDs(); + const returnUsers = new List(); + for (const id of friends) returnUsers.push(User.fromId(id)); + return returnUsers; + } + +} diff --git a/client/src/modules/modules.js b/client/src/modules/modules.js index 71956f05..f13d3bb3 100644 --- a/client/src/modules/modules.js +++ b/client/src/modules/modules.js @@ -14,3 +14,4 @@ export { default as EventHook } from './eventhook'; export { default as Permissions } from './permissionmanager'; export { default as Database } from './database'; export { default as EventsWrapper } from './eventswrapper'; +export { default as DiscordApi } from './discordapi'; diff --git a/client/src/modules/webpackmodules.js b/client/src/modules/webpackmodules.js index 1f173c4d..0faaa844 100644 --- a/client/src/modules/webpackmodules.js +++ b/client/src/modules/webpackmodules.js @@ -68,6 +68,8 @@ const KnownModules = { ChannelStore: Filters.byProperties(['getChannels', 'getDMFromUserId']), SelectedChannelStore: Filters.byProperties(['getLastSelectedChannelId']), ChannelActions: Filters.byProperties(["selectChannel"]), + PrivateChannelActions: Filters.byProperties(["openPrivateChannel"]), + ChannelSelector: Filters.byProperties(["selectGuild", "selectChannel"]), /* Current User Info, State and Settings */ UserInfoStore: Filters.byProperties(["getToken"]), @@ -77,6 +79,7 @@ const KnownModules = { OnlineWatcher: Filters.byProperties(['isOnline']), CurrentUserIdle: Filters.byProperties(['getIdleTime']), RelationshipStore: Filters.byProperties(['isBlocked']), + RelationshipManager: Filters.byProperties(['addRelationship']), MentionStore: Filters.byProperties(["getMentions"]), /* User Stores and Utils */ @@ -86,9 +89,10 @@ const KnownModules = { UserActivityStore: Filters.byProperties(['getActivity']), UserNameResolver: Filters.byProperties(['getName']), + /* Emoji Store and Utils */ EmojiInfo: Filters.byProperties(['isEmojiDisabled']), - EmojiUtils: Filters.byProperties(['diversitySurrogate']), + EmojiUtils: Filters.byProperties(['getGuildEmoji']), EmojiStore: Filters.byProperties(['getByCategory', 'EMOJI_NAME_RE']), /* Invite Store and Utils */ @@ -96,6 +100,7 @@ const KnownModules = { InviteResolver: Filters.byProperties(['findInvite']), InviteActions: Filters.byProperties(['acceptInvite']), + /* Discord Objects & Utils */ DiscordConstants: Filters.byProperties(["Permissions", "ActivityTypes", "StatusTypes"]), Permissions: Filters.byProperties(['getHighestRole']), @@ -104,9 +109,10 @@ const KnownModules = { ClassResolver: Filters.byProperties(["getClass"]), ButtonData: Filters.byProperties(["ButtonSizes"]), IconNames: Filters.byProperties(["IconNames"]), + NavigationUtils: Filters.byProperties(['transitionTo', 'replaceWith', 'getHistory']), /* Discord Messages */ - HistoryUtils: Filters.byProperties(['transitionTo', 'replaceWith', 'getHistory']), + MessageStore: Filters.byProperties(['getMessages']), MessageActions: Filters.byProperties(['jumpToMessage', '_sendMessage']), MessageQueue: Filters.byProperties(['enqueue']), MessageParser: Filters.byProperties(['createMessage', 'parse', 'unparse']), @@ -120,6 +126,7 @@ const KnownModules = { ExperimentsManager: Filters.byProperties(['isDeveloper']), CurrentExperiment: Filters.byProperties(['getExperimentId']), + /* Images, Avatars and Utils */ ImageResolver: Filters.byProperties(["getUserAvatarURL"]), ImageUtils: Filters.byProperties(['getSizedImageSrc']), @@ -173,6 +180,7 @@ const KnownModules = { URLParser: Filters.byProperties(['Url', 'parse']), ExtraURLs: Filters.byProperties(['getArticleURL']), + /* DOM/React Components */ /* ==================== */ UserSettingsWindow: Filters.byProperties(['open', 'updateAccount']),