diff --git a/src/client/Client.js b/src/client/Client.js index 861becb2c..c896d00ee 100644 --- a/src/client/Client.js +++ b/src/client/Client.js @@ -12,6 +12,11 @@ const WebSocketManager = require('./websocket/WebSocketManager'); const ActionsManager = require('./actions/ActionsManager'); const Collection = require('../util/Collection'); const Presence = require('../structures/Presence').Presence; +const VoiceRegion = require('../structures/VoiceRegion'); +const Webhook = require('../structures/Webhook'); +const User = require('../structures/User'); +const Invite = require('../structures/Invite'); +const OAuth2Application = require('../structures/OAuth2Application'); const ShardClientUtil = require('../sharding/ShardClientUtil'); const VoiceBroadcast = require('./voice/VoiceBroadcast'); @@ -44,6 +49,13 @@ class Client extends EventEmitter { */ this.rest = new RESTManager(this); + /** + * API shortcut + * @type {Object} + * @private + */ + this.api = this.rest.api; + /** * The data manager of the client * @type {ClientDataManager} @@ -274,7 +286,11 @@ class Client extends EventEmitter { * client.login('my token'); */ login(token) { - return this.rest.methods.login(token); + return new Promise((resolve, reject) => { + if (typeof token !== 'string') throw new Error(Constants.Errors.INVALID_TOKEN); + token = token.replace(/^Bot\s*/i, ''); + this.manager.connectToWebSocket(token, resolve, reject); + }); } /** @@ -312,7 +328,9 @@ class Client extends EventEmitter { */ fetchUser(id, cache = true) { if (this.users.has(id)) return Promise.resolve(this.users.get(id)); - return this.rest.methods.getUser(id, cache); + return this.api.users(id).get().then(data => + cache ? this.dataManager.newUser(data) : new User(this, data) + ); } /** @@ -322,7 +340,8 @@ class Client extends EventEmitter { */ fetchInvite(invite) { const code = this.resolver.resolveInviteCode(invite); - return this.rest.methods.getInvite(code); + return this.api.invites(code).get({ query: { with_counts: true } }) + .then(data => new Invite(this, data)); } /** @@ -332,7 +351,7 @@ class Client extends EventEmitter { * @returns {Promise} */ fetchWebhook(id, token) { - return this.rest.methods.getWebhook(id, token); + return this.api.webhooks(id, token).get().then(data => new Webhook(this.client, data)); } /** @@ -340,7 +359,11 @@ class Client extends EventEmitter { * @returns {Collection} */ fetchVoiceRegions() { - return this.rest.methods.fetchVoiceRegions(); + return this.rest.api.voice.regions.get().then(res => { + const regions = new Collection(); + for (const region of res) regions.set(region.id, new VoiceRegion(region)); + return regions; + }); } /** @@ -385,7 +408,8 @@ class Client extends EventEmitter { * @returns {Promise} */ fetchApplication(id = '@me') { - return this.rest.methods.getApplication(id); + return this.rest.api.oauth2.applications(id).get() + .then(app => new OAuth2Application(this.client, app)); } /** diff --git a/src/client/ClientManager.js b/src/client/ClientManager.js index 538411079..4239e2e4a 100644 --- a/src/client/ClientManager.js +++ b/src/client/ClientManager.js @@ -38,7 +38,7 @@ class ClientManager { this.client.emit(Constants.Events.DEBUG, `Authenticated using token ${token}`); this.client.token = token; const timeout = this.client.setTimeout(() => reject(new Error(Constants.Errors.TOOK_TOO_LONG)), 1000 * 300); - this.client.rest.methods.getGateway().then(res => { + this.client.api.gateway.get().then(res => { const protocolVersion = Constants.DefaultOptions.ws.version; const gateway = `${res.url}/?v=${protocolVersion}&encoding=${WebSocketConnection.ENCODING}`; this.client.emit(Constants.Events.DEBUG, `Using gateway ${gateway}`); @@ -63,7 +63,7 @@ class ClientManager { this.client.token = null; return Promise.resolve(); } else { - return this.client.rest.methods.logout().then(() => { + return this.client.api.logout.post().then(() => { this.client.token = null; }); } diff --git a/src/client/WebhookClient.js b/src/client/WebhookClient.js index f89c8f973..36c945e01 100644 --- a/src/client/WebhookClient.js +++ b/src/client/WebhookClient.js @@ -34,6 +34,13 @@ class WebhookClient extends Webhook { */ this.rest = new RESTManager(this); + /** + * API shortcut + * @type {Object} + * @private + */ + this.api = this.rest.api; + /** * The data resolver of the client * @type {ClientDataResolver} diff --git a/src/client/rest/APIRequest.js b/src/client/rest/APIRequest.js index 518017ae3..7271e64b1 100644 --- a/src/client/rest/APIRequest.js +++ b/src/client/rest/APIRequest.js @@ -1,16 +1,15 @@ +const querystring = require('querystring'); const snekfetch = require('snekfetch'); const Constants = require('../../util/Constants'); class APIRequest { - constructor(rest, method, path, auth, data, files) { + constructor(rest, method, path, options) { this.rest = rest; this.client = rest.client; this.method = method; this.path = path.toString(); - this.auth = auth; - this.data = data; - this.files = files; this.route = this.getRoute(this.path); + this.options = options; } getRoute(url) { @@ -34,14 +33,23 @@ class APIRequest { gen() { const API = `${this.client.options.http.host}/api/v${this.client.options.http.version}`; + + if (this.options.query) { + const queryString = (querystring.stringify(this.options.query).match(/[^=&?]+=[^=&?]+/g) || []).join('&'); + this.path += `?${queryString}`; + } + const request = snekfetch[this.method](`${API}${this.path}`); - if (this.auth) request.set('Authorization', this.getAuth()); + + if (this.options.auth !== false) request.set('Authorization', this.getAuth()); + if (this.options.reason) request.set('X-Audit-Log-Reason', this.options.reason); if (!this.rest.client.browser) request.set('User-Agent', this.rest.userAgentManager.userAgent); - if (this.files) { - for (const file of this.files) if (file && file.file) request.attach(file.name, file.file, file.name); - if (typeof this.data !== 'undefined') request.attach('payload_json', JSON.stringify(this.data)); - } else if (this.data) { - request.send(this.data); + + if (this.options.files) { + for (const file of this.options.files) if (file && file.file) request.attach(file.name, file.file, file.name); + if (typeof this.options.data !== 'undefined') request.attach('payload_json', JSON.stringify(this.options.data)); + } else if (typeof this.options.data !== 'undefined') { + request.send(this.options.data); } return request; } diff --git a/src/client/rest/APIRouter.js b/src/client/rest/APIRouter.js new file mode 100644 index 000000000..20133822f --- /dev/null +++ b/src/client/rest/APIRouter.js @@ -0,0 +1,39 @@ +const util = require('util'); + +const methods = ['get', 'post', 'delete', 'patch', 'put']; +// Paramable exists so we don't return a function unless we actually need one #savingmemory +const paramable = [ + 'channels', 'users', 'guilds', 'members', + 'bans', 'emojis', 'pins', 'permissions', + 'reactions', 'webhooks', 'messages', + 'notes', 'roles', 'applications', + 'invites', +]; +const reflectors = ['toString', 'valueOf', 'inspect', Symbol.toPrimitive, util.inspect.custom]; + +module.exports = restManager => { + const handler = { + get(list, name) { + if (reflectors.includes(name)) return () => list.join('/'); + if (paramable.includes(name)) { + function toReturn(...args) { // eslint-disable-line no-inner-declarations + list = list.concat(name); + for (const arg of args) { + if (arg !== null && typeof arg !== 'undefined') list = list.concat(arg); + } + return new Proxy(list, handler); + } + const directJoin = () => `${list.join('/')}/${name}`; + for (const r of reflectors) toReturn[r] = directJoin; + for (const method of methods) { + toReturn[method] = options => restManager.request(method, `${list.join('/')}/${name}`, options); + } + return toReturn; + } + if (methods.includes(name)) return options => restManager.request(name, list.join('/'), options); + return new Proxy(list.concat(name), handler); + }, + }; + + return new Proxy([''], handler); +}; diff --git a/src/client/rest/DiscordAPIError.js b/src/client/rest/DiscordAPIError.js index 7d3ff7ef9..c5bd31b15 100644 --- a/src/client/rest/DiscordAPIError.js +++ b/src/client/rest/DiscordAPIError.js @@ -29,8 +29,8 @@ class DiscordAPIError extends Error { if (obj[k]._errors) { messages.push(`${newKey}: ${obj[k]._errors.map(e => e.message).join(' ')}`); - } else if (obj[k].code && obj[k].message) { - messages.push(`${obj[k].code}: ${obj[k].message}`); + } else if (obj[k].code || obj[k].message) { + messages.push(`${obj[k].code ? `${obj[k].code}: ` : ''}${obj[k].message}`.trim()); } else { messages = messages.concat(this.flattenErrors(obj[k], newKey)); } diff --git a/src/client/rest/RESTManager.js b/src/client/rest/RESTManager.js index 512b3063e..5a7e9f0ed 100644 --- a/src/client/rest/RESTManager.js +++ b/src/client/rest/RESTManager.js @@ -1,8 +1,8 @@ const UserAgentManager = require('./UserAgentManager'); -const RESTMethods = require('./RESTMethods'); const SequentialRequestHandler = require('./RequestHandlers/Sequential'); const BurstRequestHandler = require('./RequestHandlers/Burst'); const APIRequest = require('./APIRequest'); +const mountApi = require('./APIRouter'); const Constants = require('../../util/Constants'); class RESTManager { @@ -10,9 +10,10 @@ class RESTManager { this.client = client; this.handlers = {}; this.userAgentManager = new UserAgentManager(this); - this.methods = new RESTMethods(this); this.rateLimitedEndpoints = {}; this.globallyRateLimited = false; + + this.api = mountApi(this); } destroy() { @@ -42,8 +43,8 @@ class RESTManager { } } - makeRequest(method, url, auth, data, file) { - const apiRequest = new APIRequest(this, method, url, auth, data, file); + request(method, url, options = {}) { + const apiRequest = new APIRequest(this, method, url, options); if (!this.handlers[apiRequest.route]) { const RequestHandlerType = this.getRequestHandler(); this.handlers[apiRequest.route] = new RequestHandlerType(this, apiRequest.route); diff --git a/src/client/rest/RESTMethods.js b/src/client/rest/RESTMethods.js deleted file mode 100644 index 30d5f4ff9..000000000 --- a/src/client/rest/RESTMethods.js +++ /dev/null @@ -1,903 +0,0 @@ -const querystring = require('querystring'); -const long = require('long'); -const Permissions = require('../../util/Permissions'); -const Constants = require('../../util/Constants'); -const Endpoints = Constants.Endpoints; -const Collection = require('../../util/Collection'); -const Snowflake = require('../../util/Snowflake'); -const Util = require('../../util/Util'); - -const User = require('../../structures/User'); -const GuildMember = require('../../structures/GuildMember'); -const Message = require('../../structures/Message'); -const Role = require('../../structures/Role'); -const Invite = require('../../structures/Invite'); -const Webhook = require('../../structures/Webhook'); -const UserProfile = require('../../structures/UserProfile'); -const OAuth2Application = require('../../structures/OAuth2Application'); -const Channel = require('../../structures/Channel'); -const GroupDMChannel = require('../../structures/GroupDMChannel'); -const Guild = require('../../structures/Guild'); -const VoiceRegion = require('../../structures/VoiceRegion'); -const GuildAuditLogs = require('../../structures/GuildAuditLogs'); - -class RESTMethods { - constructor(restManager) { - this.rest = restManager; - this.client = restManager.client; - this._ackToken = null; - } - - login(token = this.client.token) { - return new Promise((resolve, reject) => { - if (typeof token !== 'string') throw new Error(Constants.Errors.INVALID_TOKEN); - token = token.replace(/^Bot\s*/i, ''); - this.client.manager.connectToWebSocket(token, resolve, reject); - }); - } - - logout() { - return this.rest.makeRequest('post', Endpoints.logout, true, {}); - } - - getGateway(bot = false) { - return this.rest.makeRequest('get', bot ? Endpoints.gateway.bot : Endpoints.gateway, true); - } - - fetchVoiceRegions(guildID) { - let endpoint; - if (guildID) endpoint = Endpoints.Guild(guildID).voiceRegions; - else endpoint = Endpoints.voiceRegions; - return this.rest.makeRequest('get', endpoint, true).then(res => { - const regions = new Collection(); - for (const region of res) regions.set(region.id, new VoiceRegion(region)); - return regions; - }); - } - - sendMessage(channel, content, { tts, nonce, embed, disableEveryone, split, code, reply } = {}, files = null) { - return new Promise((resolve, reject) => { // eslint-disable-line complexity - if (typeof content !== 'undefined') content = this.client.resolver.resolveString(content); - - // The nonce has to be a uint64 :< - if (typeof nonce !== 'undefined') { - nonce = parseInt(nonce); - if (isNaN(nonce) || nonce < 0) throw new RangeError('Message nonce must fit in an unsigned 64-bit integer.'); - } - - if (content) { - if (split && typeof split !== 'object') split = {}; - - // Wrap everything in a code block - if (typeof code !== 'undefined' && (typeof code !== 'boolean' || code === true)) { - content = Util.escapeMarkdown(this.client.resolver.resolveString(content), true); - content = `\`\`\`${typeof code !== 'boolean' ? code || '' : ''}\n${content}\n\`\`\``; - if (split) { - split.prepend = `\`\`\`${typeof code !== 'boolean' ? code || '' : ''}\n`; - split.append = '\n```'; - } - } - - // Add zero-width spaces to @everyone/@here - if (disableEveryone || (typeof disableEveryone === 'undefined' && this.client.options.disableEveryone)) { - content = content.replace(/@(everyone|here)/g, '@\u200b$1'); - } - - // Add the reply prefix - if (reply && !(channel instanceof User || channel instanceof GuildMember) && channel.type !== 'dm') { - const id = this.client.resolver.resolveUserID(reply); - const mention = `<@${reply instanceof GuildMember && reply.nickname ? '!' : ''}${id}>`; - content = `${mention}${content ? `, ${content}` : ''}`; - if (split) split.prepend = `${mention}, ${split.prepend || ''}`; - } - - // Split the content - if (split) content = Util.splitMessage(content, split); - } else if (reply && !(channel instanceof User || channel instanceof GuildMember) && channel.type !== 'dm') { - const id = this.client.resolver.resolveUserID(reply); - content = `<@${reply instanceof GuildMember && reply.nickname ? '!' : ''}${id}>`; - } - - const send = chan => { - if (content instanceof Array) { - const messages = []; - (function sendChunk(list, index) { - const options = index === list.length ? { tts, embed } : { tts }; - chan.send(list[index], options, index === list.length ? files : null).then(message => { - messages.push(message); - if (index >= list.length - 1) return resolve(messages); - return sendChunk(list, ++index); - }); - }(content, 0)); - } else { - this.rest.makeRequest('post', Endpoints.Channel(chan).messages, true, { - content, tts, nonce, embed, - }, files).then(data => resolve(this.client.actions.MessageCreate.handle(data).message), reject); - } - }; - - if (channel instanceof User || channel instanceof GuildMember) this.createDM(channel).then(send, reject); - else send(channel); - }); - } - - updateMessage(message, content, { embed, code, reply } = {}) { - if (typeof content !== 'undefined') content = this.client.resolver.resolveString(content); - - // Wrap everything in a code block - if (typeof code !== 'undefined' && (typeof code !== 'boolean' || code === true)) { - content = Util.escapeMarkdown(this.client.resolver.resolveString(content), true); - content = `\`\`\`${typeof code !== 'boolean' ? code || '' : ''}\n${content}\n\`\`\``; - } - - // Add the reply prefix - if (reply && message.channel.type !== 'dm') { - const id = this.client.resolver.resolveUserID(reply); - const mention = `<@${reply instanceof GuildMember && reply.nickname ? '!' : ''}${id}>`; - content = `${mention}${content ? `, ${content}` : ''}`; - } - - return this.rest.makeRequest('patch', Endpoints.Message(message), true, { - content, embed, - }).then(data => this.client.actions.MessageUpdate.handle(data).updated); - } - - deleteMessage(message) { - return this.rest.makeRequest('delete', Endpoints.Message(message), true) - .then(() => - this.client.actions.MessageDelete.handle({ - id: message.id, - channel_id: message.channel.id, - }).message - ); - } - - ackMessage(message) { - return this.rest.makeRequest('post', Endpoints.Message(message).ack, true, { token: this._ackToken }).then(res => { - if (res.token) this._ackToken = res.token; - return message; - }); - } - - ackTextChannel(channel) { - return this.rest.makeRequest('post', Endpoints.Channel(channel).Message(channel.lastMessageID).ack, true, { - token: this._ackToken, - }).then(res => { - if (res.token) this._ackToken = res.token; - return channel; - }); - } - - ackGuild(guild) { - return this.rest.makeRequest('post', Endpoints.Guild(guild).ack, true).then(() => guild); - } - - bulkDeleteMessages(channel, messages, filterOld) { - if (filterOld) { - messages = messages.filter(id => - Date.now() - Snowflake.deconstruct(id).date.getTime() < 1209600000 - ); - } - return this.rest.makeRequest('post', Endpoints.Channel(channel).messages.bulkDelete, true, { - messages, - }).then(() => - this.client.actions.MessageDeleteBulk.handle({ - channel_id: channel.id, - ids: messages, - }).messages - ); - } - - search(target, options) { - if (typeof options === 'string') options = { content: options }; - if (options.before) { - if (!(options.before instanceof Date)) options.before = new Date(options.before); - options.maxID = long.fromNumber(options.before.getTime() - 14200704e5).shiftLeft(22).toString(); - } - if (options.after) { - if (!(options.after instanceof Date)) options.after = new Date(options.after); - options.minID = long.fromNumber(options.after.getTime() - 14200704e5).shiftLeft(22).toString(); - } - if (options.during) { - if (!(options.during instanceof Date)) options.during = new Date(options.during); - const t = options.during.getTime() - 14200704e5; - options.minID = long.fromNumber(t).shiftLeft(22).toString(); - options.maxID = long.fromNumber(t + 86400000).shiftLeft(22).toString(); - } - if (options.channel) options.channel = this.client.resolver.resolveChannelID(options.channel); - if (options.author) options.author = this.client.resolver.resolveUserID(options.author); - if (options.mentions) options.mentions = this.client.resolver.resolveUserID(options.options.mentions); - options = { - content: options.content, - max_id: options.maxID, - min_id: options.minID, - has: options.has, - channel_id: options.channel, - author_id: options.author, - author_type: options.authorType, - context_size: options.contextSize, - sort_by: options.sortBy, - sort_order: options.sortOrder, - limit: options.limit, - offset: options.offset, - mentions: options.mentions, - mentions_everyone: options.mentionsEveryone, - link_hostname: options.linkHostname, - embed_provider: options.embedProvider, - embed_type: options.embedType, - attachment_filename: options.attachmentFilename, - attachment_extension: options.attachmentExtension, - }; - - for (const key in options) if (options[key] === undefined) delete options[key]; - const queryString = (querystring.stringify(options).match(/[^=&?]+=[^=&?]+/g) || []).join('&'); - - let endpoint; - if (target instanceof Channel) { - endpoint = Endpoints.Channel(target).search; - } else if (target instanceof Guild) { - endpoint = Endpoints.Guild(target).search; - } else { - throw new TypeError('Target must be a TextChannel, DMChannel, GroupDMChannel, or Guild.'); - } - return this.rest.makeRequest('get', `${endpoint}?${queryString}`, true).then(body => { - const messages = body.messages.map(x => - x.map(m => new Message(this.client.channels.get(m.channel_id), m, this.client)) - ); - return { - totalResults: body.total_results, - messages, - }; - }); - } - - createChannel(guild, channelName, channelType, overwrites) { - if (overwrites instanceof Collection) overwrites = overwrites.array(); - return this.rest.makeRequest('post', Endpoints.Guild(guild).channels, true, { - name: channelName, - type: channelType, - permission_overwrites: overwrites, - }).then(data => this.client.actions.ChannelCreate.handle(data).channel); - } - - createDM(recipient) { - const dmChannel = this.getExistingDM(recipient); - if (dmChannel) return Promise.resolve(dmChannel); - return this.rest.makeRequest('post', Endpoints.User(this.client.user).channels, true, { - recipient_id: recipient.id, - }).then(data => this.client.actions.ChannelCreate.handle(data).channel); - } - - createGroupDM(options) { - const data = this.client.user.bot ? - { access_tokens: options.accessTokens, nicks: options.nicks } : - { recipients: options.recipients }; - return this.rest.makeRequest('post', Endpoints.User('@me').channels, true, data) - .then(res => new GroupDMChannel(this.client, res)); - } - - addUserToGroupDM(channel, options) { - const data = this.client.user.bot ? - { nick: options.nick, access_token: options.accessToken } : - { recipient: options.id }; - return this.rest.makeRequest('put', Endpoints.Channel(channel).Recipient(options.id), true, data) - .then(() => channel); - } - - getExistingDM(recipient) { - return this.client.channels.find(channel => - channel.recipient && channel.recipient.id === recipient.id - ); - } - - deleteChannel(channel) { - if (channel instanceof User || channel instanceof GuildMember) channel = this.getExistingDM(channel); - if (!channel) return Promise.reject(new Error('No channel to delete.')); - return this.rest.makeRequest('delete', Endpoints.Channel(channel), true).then(data => { - data.id = channel.id; - return this.client.actions.ChannelDelete.handle(data).channel; - }); - } - - updateChannel(channel, _data) { - const data = {}; - data.name = (_data.name || channel.name).trim(); - data.topic = _data.topic || channel.topic; - data.position = _data.position || channel.position; - data.bitrate = _data.bitrate || channel.bitrate; - data.user_limit = _data.userLimit || channel.userLimit; - return this.rest.makeRequest('patch', Endpoints.Channel(channel), true, data).then(newData => - this.client.actions.ChannelUpdate.handle(newData).updated - ); - } - - leaveGuild(guild) { - if (guild.ownerID === this.client.user.id) return Promise.reject(new Error('Guild is owned by the client.')); - return this.rest.makeRequest('delete', Endpoints.User('@me').Guild(guild.id), true).then(() => - this.client.actions.GuildDelete.handle({ id: guild.id }).guild - ); - } - - createGuild(options) { - options.icon = this.client.resolver.resolveBase64(options.icon) || null; - options.region = options.region || 'us-central'; - return new Promise((resolve, reject) => { - this.rest.makeRequest('post', Endpoints.guilds, true, options).then(data => { - if (this.client.guilds.has(data.id)) return resolve(this.client.guilds.get(data.id)); - - const handleGuild = guild => { - if (guild.id === data.id) { - this.client.removeListener(Constants.Events.GUILD_CREATE, handleGuild); - this.client.clearTimeout(timeout); - resolve(guild); - } - }; - this.client.on(Constants.Events.GUILD_CREATE, handleGuild); - - const timeout = this.client.setTimeout(() => { - this.client.removeListener(Constants.Events.GUILD_CREATE, handleGuild); - reject(new Error('Took too long to receive guild data.')); - }, 10000); - return undefined; - }, reject); - }); - } - - // Untested but probably will work - deleteGuild(guild) { - return this.rest.makeRequest('delete', Endpoints.Guild(guild), true).then(() => - this.client.actions.GuildDelete.handle({ id: guild.id }).guild - ); - } - - getUser(userID, cache) { - return this.rest.makeRequest('get', Endpoints.User(userID), true).then(data => { - if (cache) return this.client.actions.UserGet.handle(data).user; - else return new User(this.client, data); - }); - } - - updateCurrentUser(_data, password) { - const user = this.client.user; - const data = {}; - data.username = _data.username || user.username; - data.avatar = this.client.resolver.resolveBase64(_data.avatar) || user.avatar; - if (!user.bot) { - data.email = _data.email || user.email; - data.password = password; - if (_data.new_password) data.new_password = _data.newPassword; - } - return this.rest.makeRequest('patch', Endpoints.User('@me'), true, data).then(newData => - this.client.actions.UserUpdate.handle(newData).updated - ); - } - - updateGuild(guild, _data) { - const data = {}; - if (_data.name) data.name = _data.name; - if (_data.region) data.region = _data.region; - if (_data.verificationLevel) data.verification_level = Number(_data.verificationLevel); - if (_data.afkChannel) data.afk_channel_id = this.client.resolver.resolveChannel(_data.afkChannel).id; - if (_data.afkTimeout) data.afk_timeout = Number(_data.afkTimeout); - if (_data.icon) data.icon = this.client.resolver.resolveBase64(_data.icon); - if (_data.owner) data.owner_id = this.client.resolver.resolveUser(_data.owner).id; - if (_data.splash) data.splash = this.client.resolver.resolveBase64(_data.splash); - return this.rest.makeRequest('patch', Endpoints.Guild(guild), true, data).then(newData => - this.client.actions.GuildUpdate.handle(newData).updated - ); - } - - kickGuildMember(guild, member, reason) { - const url = `${Endpoints.Guild(guild).Member(member)}?reason=${reason}`; - return this.rest.makeRequest('delete', url, true).then(() => - this.client.actions.GuildMemberRemove.handle({ - guild_id: guild.id, - user: member.user, - }).member - ); - } - - createGuildRole(guild, data) { - if (data.color) data.color = this.client.resolver.resolveColor(data.color); - if (data.permissions) data.permissions = Permissions.resolve(data.permissions); - return this.rest.makeRequest('post', Endpoints.Guild(guild).roles, true, data).then(role => - this.client.actions.GuildRoleCreate.handle({ - guild_id: guild.id, - role, - }).role - ); - } - - deleteGuildRole(role) { - return this.rest.makeRequest('delete', Endpoints.Guild(role.guild).Role(role.id), true).then(() => - this.client.actions.GuildRoleDelete.handle({ - guild_id: role.guild.id, - role_id: role.id, - }).role - ); - } - - setChannelOverwrite(channel, payload) { - return this.rest.makeRequest('put', `${Endpoints.Channel(channel).permissions}/${payload.id}`, true, payload); - } - - deletePermissionOverwrites(overwrite) { - return this.rest.makeRequest( - 'delete', `${Endpoints.Channel(overwrite.channel).permissions}/${overwrite.id}`, true - ).then(() => overwrite); - } - - getChannelMessages(channel, payload = {}) { - const params = []; - if (payload.limit) params.push(`limit=${payload.limit}`); - if (payload.around) params.push(`around=${payload.around}`); - else if (payload.before) params.push(`before=${payload.before}`); - else if (payload.after) params.push(`after=${payload.after}`); - - let endpoint = Endpoints.Channel(channel).messages; - if (params.length > 0) endpoint += `?${params.join('&')}`; - return this.rest.makeRequest('get', endpoint, true); - } - - getChannelMessage(channel, messageID) { - const msg = channel.messages.get(messageID); - if (msg) return Promise.resolve(msg); - return this.rest.makeRequest('get', Endpoints.Channel(channel).Message(messageID), true); - } - - putGuildMember(guild, user, options) { - options.access_token = options.accessToken; - if (options.roles) { - const roles = options.roles; - if (roles instanceof Collection || (roles instanceof Array && roles[0] instanceof Role)) { - options.roles = roles.map(role => role.id); - } - } - return this.rest.makeRequest('put', Endpoints.Guild(guild).Member(user.id), true, options) - .then(data => this.client.actions.GuildMemberGet.handle(guild, data).member); - } - - getGuildMember(guild, user, cache) { - return this.rest.makeRequest('get', Endpoints.Guild(guild).Member(user.id), true).then(data => { - if (cache) return this.client.actions.GuildMemberGet.handle(guild, data).member; - else return new GuildMember(guild, data); - }); - } - - updateGuildMember(member, data) { - if (data.channel) { - data.channel_id = this.client.resolver.resolveChannel(data.channel).id; - data.channel = null; - } - if (data.roles) data.roles = data.roles.map(role => role instanceof Role ? role.id : role); - - let endpoint = Endpoints.Member(member); - // Fix your endpoints, discord ;-; - if (member.id === this.client.user.id) { - const keys = Object.keys(data); - if (keys.length === 1 && keys[0] === 'nick') { - endpoint = Endpoints.Member(member).nickname; - } - } - - return this.rest.makeRequest('patch', endpoint, true, data).then(newData => - member.guild._updateMember(member, newData).mem - ); - } - - addMemberRole(member, role) { - return new Promise((resolve, reject) => { - if (member._roles.includes(role.id)) return resolve(member); - - const listener = (oldMember, newMember) => { - if (!oldMember._roles.includes(role.id) && newMember._roles.includes(role.id)) { - this.client.removeListener(Constants.Events.GUILD_MEMBER_UPDATE, listener); - resolve(newMember); - } - }; - - this.client.on(Constants.Events.GUILD_MEMBER_UPDATE, listener); - const timeout = this.client.setTimeout(() => - this.client.removeListener(Constants.Events.GUILD_MEMBER_UPDATE, listener), 10e3); - - return this.rest.makeRequest('put', Endpoints.Member(member).Role(role.id), true).catch(err => { - this.client.removeListener(Constants.Events.GUILD_BAN_REMOVE, listener); - this.client.clearTimeout(timeout); - reject(err); - }); - }); - } - - removeMemberRole(member, role) { - return new Promise((resolve, reject) => { - if (!member._roles.includes(role.id)) return resolve(member); - - const listener = (oldMember, newMember) => { - if (oldMember._roles.includes(role.id) && !newMember._roles.includes(role.id)) { - this.client.removeListener(Constants.Events.GUILD_MEMBER_UPDATE, listener); - resolve(newMember); - } - }; - - this.client.on(Constants.Events.GUILD_MEMBER_UPDATE, listener); - const timeout = this.client.setTimeout(() => - this.client.removeListener(Constants.Events.GUILD_MEMBER_UPDATE, listener), 10e3); - - return this.rest.makeRequest('delete', Endpoints.Member(member).Role(role.id), true).catch(err => { - this.client.removeListener(Constants.Events.GUILD_BAN_REMOVE, listener); - this.client.clearTimeout(timeout); - reject(err); - }); - }); - } - - sendTyping(channelID) { - return this.rest.makeRequest('post', Endpoints.Channel(channelID).typing, true); - } - - banGuildMember(guild, member, options) { - const id = this.client.resolver.resolveUserID(member); - if (!id) return Promise.reject(new Error('Couldn\'t resolve the user ID to ban.')); - - const url = `${Endpoints.Guild(guild).bans}/${id}?${querystring.stringify(options)}`; - return this.rest.makeRequest('put', url, true).then(() => { - if (member instanceof GuildMember) return member; - const user = this.client.resolver.resolveUser(id); - if (user) { - member = this.client.resolver.resolveGuildMember(guild, user); - return member || user; - } - return id; - }); - } - - unbanGuildMember(guild, member) { - return new Promise((resolve, reject) => { - const id = this.client.resolver.resolveUserID(member); - if (!id) throw new Error('Couldn\'t resolve the user ID to unban.'); - - const listener = (eGuild, eUser) => { - if (eGuild.id === guild.id && eUser.id === id) { - this.client.removeListener(Constants.Events.GUILD_BAN_REMOVE, listener); - this.client.clearTimeout(timeout); - resolve(eUser); - } - }; - this.client.on(Constants.Events.GUILD_BAN_REMOVE, listener); - - const timeout = this.client.setTimeout(() => { - this.client.removeListener(Constants.Events.GUILD_BAN_REMOVE, listener); - reject(new Error('Took too long to receive the ban remove event.')); - }, 10000); - - this.rest.makeRequest('delete', `${Endpoints.Guild(guild).bans}/${id}`, true).catch(err => { - this.client.removeListener(Constants.Events.GUILD_BAN_REMOVE, listener); - this.client.clearTimeout(timeout); - reject(err); - }); - }); - } - - getGuildBans(guild) { - return this.rest.makeRequest('get', Endpoints.Guild(guild).bans, true).then(bans => - bans.reduce((collection, ban) => { - collection.set(ban.user.id, { - reason: ban.reason, - user: this.client.dataManager.newUser(ban.user), - }); - return collection; - }, new Collection()) - ); - } - - updateGuildRole(role, _data) { - const data = {}; - data.name = _data.name || role.name; - data.position = typeof _data.position !== 'undefined' ? _data.position : role.position; - data.color = this.client.resolver.resolveColor(_data.color || role.color); - data.hoist = typeof _data.hoist !== 'undefined' ? _data.hoist : role.hoist; - data.mentionable = typeof _data.mentionable !== 'undefined' ? _data.mentionable : role.mentionable; - - if (_data.permissions) data.permissions = Permissions.resolve(_data.permissions); - else data.permissions = role.permissions; - - return this.rest.makeRequest('patch', Endpoints.Guild(role.guild).Role(role.id), true, data).then(_role => - this.client.actions.GuildRoleUpdate.handle({ - role: _role, - guild_id: role.guild.id, - }).updated - ); - } - - pinMessage(message) { - return this.rest.makeRequest('put', Endpoints.Channel(message.channel).Pin(message.id), true) - .then(() => message); - } - - unpinMessage(message) { - return this.rest.makeRequest('delete', Endpoints.Channel(message.channel).Pin(message.id), true) - .then(() => message); - } - - getChannelPinnedMessages(channel) { - return this.rest.makeRequest('get', Endpoints.Channel(channel).pins, true); - } - - createChannelInvite(channel, options) { - const payload = {}; - payload.temporary = options.temporary; - payload.max_age = options.maxAge; - payload.max_uses = options.maxUses; - return this.rest.makeRequest('post', Endpoints.Channel(channel).invites, true, payload) - .then(invite => new Invite(this.client, invite)); - } - - deleteInvite(invite) { - return this.rest.makeRequest('delete', Endpoints.Invite(invite.code), true).then(() => invite); - } - - getInvite(code) { - return this.rest.makeRequest('get', Endpoints.Invite(code), true).then(invite => - new Invite(this.client, invite) - ); - } - - getGuildInvites(guild) { - return this.rest.makeRequest('get', Endpoints.Guild(guild).invites, true).then(inviteItems => { - const invites = new Collection(); - for (const inviteItem of inviteItems) { - const invite = new Invite(this.client, inviteItem); - invites.set(invite.code, invite); - } - return invites; - }); - } - - pruneGuildMembers(guild, days, dry) { - return this.rest.makeRequest(dry ? 'get' : 'post', `${Endpoints.Guild(guild).prune}?days=${days}`, true) - .then(data => data.pruned); - } - - createEmoji(guild, image, name, roles) { - const data = { image, name }; - if (roles) data.roles = roles.map(r => r.id ? r.id : r); - return this.rest.makeRequest('post', Endpoints.Guild(guild).emojis, true, data) - .then(emoji => this.client.actions.GuildEmojiCreate.handle(guild, emoji).emoji); - } - - updateEmoji(emoji, _data) { - const data = {}; - if (_data.name) data.name = _data.name; - if (_data.roles) data.roles = _data.roles.map(r => r.id ? r.id : r); - return this.rest.makeRequest('patch', Endpoints.Guild(emoji.guild).Emoji(emoji.id), true, data) - .then(newEmoji => this.client.actions.GuildEmojiUpdate.handle(emoji, newEmoji).emoji); - } - - deleteEmoji(emoji) { - return this.rest.makeRequest('delete', Endpoints.Guild(emoji.guild).Emoji(emoji.id), true) - .then(() => this.client.actions.GuildEmojiDelete.handle(emoji).data); - } - - getGuildAuditLogs(guild, options = {}) { - if (options.before && options.before instanceof GuildAuditLogs.Entry) options.before = options.before.id; - if (options.after && options.after instanceof GuildAuditLogs.Entry) options.after = options.after.id; - if (typeof options.type === 'string') options.type = GuildAuditLogs.Actions[options.type]; - - const queryString = (querystring.stringify({ - before: options.before, - after: options.after, - limit: options.limit, - user_id: this.client.resolver.resolveUserID(options.user), - action_type: options.type, - }).match(/[^=&?]+=[^=&?]+/g) || []).join('&'); - - return this.rest.makeRequest('get', `${Endpoints.Guild(guild).auditLogs}?${queryString}`, true) - .then(data => GuildAuditLogs.build(guild, data)); - } - - getWebhook(id, token) { - return this.rest.makeRequest('get', Endpoints.Webhook(id, token), !token).then(data => - new Webhook(this.client, data) - ); - } - - getGuildWebhooks(guild) { - return this.rest.makeRequest('get', Endpoints.Guild(guild).webhooks, true).then(data => { - const hooks = new Collection(); - for (const hook of data) hooks.set(hook.id, new Webhook(this.client, hook)); - return hooks; - }); - } - - getChannelWebhooks(channel) { - return this.rest.makeRequest('get', Endpoints.Channel(channel).webhooks, true).then(data => { - const hooks = new Collection(); - for (const hook of data) hooks.set(hook.id, new Webhook(this.client, hook)); - return hooks; - }); - } - - createWebhook(channel, name, avatar) { - return this.rest.makeRequest('post', Endpoints.Channel(channel).webhooks, true, { name, avatar }) - .then(data => new Webhook(this.client, data)); - } - - editWebhook(webhook, name, avatar) { - return this.rest.makeRequest('patch', Endpoints.Webhook(webhook.id, webhook.token), false, { - name, - avatar, - }).then(data => { - webhook.name = data.name; - webhook.avatar = data.avatar; - return webhook; - }); - } - - deleteWebhook(webhook) { - return this.rest.makeRequest('delete', Endpoints.Webhook(webhook.id, webhook.token), false); - } - - sendWebhookMessage(webhook, content, { avatarURL, tts, disableEveryone, embeds, username } = {}, file = null) { - username = username || webhook.name; - if (typeof content !== 'undefined') content = this.client.resolver.resolveString(content); - if (content) { - if (disableEveryone || (typeof disableEveryone === 'undefined' && this.client.options.disableEveryone)) { - content = content.replace(/@(everyone|here)/g, '@\u200b$1'); - } - } - return this.rest.makeRequest('post', `${Endpoints.Webhook(webhook.id, webhook.token)}?wait=true`, false, { - username, - avatar_url: avatarURL, - content, - tts, - embeds, - }, file); - } - - sendSlackWebhookMessage(webhook, body) { - return this.rest.makeRequest( - 'post', `${Endpoints.Webhook(webhook.id, webhook.token)}/slack?wait=true`, false, body - ); - } - - fetchUserProfile(user) { - return this.rest.makeRequest('get', Endpoints.User(user).profile, true).then(data => - new UserProfile(user, data) - ); - } - - fetchMentions(options) { - if (options.guild instanceof Guild) options.guild = options.guild.id; - Util.mergeDefault({ limit: 25, roles: true, everyone: true, guild: null }, options); - - return this.rest.makeRequest( - 'get', Endpoints.User('@me').Mentions(options.limit, options.roles, options.everyone, options.guild), true - ).then(data => data.map(m => new Message(this.client.channels.get(m.channel_id), m, this.client))); - } - - addFriend(user) { - return this.rest.makeRequest('post', Endpoints.User('@me'), true, { - username: user.username, - discriminator: user.discriminator, - }).then(() => user); - } - - removeFriend(user) { - return this.rest.makeRequest('delete', Endpoints.User('@me').Relationship(user.id), true) - .then(() => user); - } - - blockUser(user) { - return this.rest.makeRequest('put', Endpoints.User('@me').Relationship(user.id), true, { type: 2 }) - .then(() => user); - } - - unblockUser(user) { - return this.rest.makeRequest('delete', Endpoints.User('@me').Relationship(user.id), true) - .then(() => user); - } - - updateChannelPositions(guildID, channels) { - const data = new Array(channels.length); - for (let i = 0; i < channels.length; i++) { - data[i] = { - id: this.client.resolver.resolveChannelID(channels[i].channel), - position: channels[i].position, - }; - } - - return this.rest.makeRequest('patch', Endpoints.Guild(guildID).channels, true, data).then(() => - this.client.actions.GuildChannelsPositionUpdate.handle({ - guild_id: guildID, - channels, - }).guild - ); - } - - setRolePositions(guildID, roles) { - return this.rest.makeRequest('patch', Endpoints.Guild(guildID).roles, true, roles).then(() => - this.client.actions.GuildRolesPositionUpdate.handle({ - guild_id: guildID, - roles, - }).guild - ); - } - - setChannelPositions(guildID, channels) { - return this.rest.makeRequest('patch', Endpoints.Guild(guildID).channels, true, channels).then(() => - this.client.actions.GuildChannelsPositionUpdate.handle({ - guild_id: guildID, - channels, - }).guild - ); - } - - addMessageReaction(message, emoji) { - return this.rest.makeRequest( - 'put', Endpoints.Message(message).Reaction(emoji).User('@me'), true - ).then(() => - message._addReaction(Util.parseEmoji(emoji), message.client.user) - ); - } - - removeMessageReaction(message, emoji, userID) { - const endpoint = Endpoints.Message(message).Reaction(emoji).User(userID === this.client.user.id ? '@me' : userID); - return this.rest.makeRequest('delete', endpoint, true).then(() => - this.client.actions.MessageReactionRemove.handle({ - user_id: userID, - message_id: message.id, - emoji: Util.parseEmoji(emoji), - channel_id: message.channel.id, - }).reaction - ); - } - - removeMessageReactions(message) { - return this.rest.makeRequest('delete', Endpoints.Message(message).reactions, true) - .then(() => message); - } - - getMessageReactionUsers(message, emoji, limit = 100) { - return this.rest.makeRequest('get', Endpoints.Message(message).Reaction(emoji, limit), true); - } - - getApplication(id) { - return this.rest.makeRequest('get', Endpoints.OAUTH2.Application(id), true).then(app => - new OAuth2Application(this.client, app) - ); - } - - resetApplication(id) { - return this.rest.makeRequest('post', Endpoints.OAUTH2.Application(id).reset, true) - .then(app => new OAuth2Application(this.client, app)); - } - - setNote(user, note) { - return this.rest.makeRequest('put', Endpoints.User(user).note, true, { note }).then(() => user); - } - - acceptInvite(code) { - if (code.id) code = code.id; - return new Promise((resolve, reject) => - this.rest.makeRequest('post', Endpoints.Invite(code), true).then(res => { - const handler = guild => { - if (guild.id === res.id) { - resolve(guild); - this.client.removeListener(Constants.Events.GUILD_CREATE, handler); - } - }; - this.client.on(Constants.Events.GUILD_CREATE, handler); - this.client.setTimeout(() => { - this.client.removeListener(Constants.Events.GUILD_CREATE, handler); - reject(new Error('Accepting invite timed out')); - }, 120e3); - }) - ); - } - - patchUserSettings(data) { - return this.rest.makeRequest('patch', Constants.Endpoints.User('@me').settings, true, data); - } -} - -module.exports = RESTMethods; diff --git a/src/structures/Channel.js b/src/structures/Channel.js index 3d492e451..d8f877c9a 100644 --- a/src/structures/Channel.js +++ b/src/structures/Channel.js @@ -53,7 +53,7 @@ class Channel { } /** - * Deletes the channel. + * Deletes this channel. * @returns {Promise} * @example * // Delete the channel @@ -62,7 +62,7 @@ class Channel { * .catch(console.error); // Log error */ delete() { - return this.client.rest.methods.deleteChannel(this); + return this.client.api.channels(this.id).delete().then(() => this); } } diff --git a/src/structures/ClientUser.js b/src/structures/ClientUser.js index dcd16423c..95d254054 100644 --- a/src/structures/ClientUser.js +++ b/src/structures/ClientUser.js @@ -2,6 +2,10 @@ const User = require('./User'); const Collection = require('../util/Collection'); const ClientUserSettings = require('./ClientUserSettings'); const Constants = require('../util/Constants'); +const Util = require('../util/Util'); +const Guild = require('./Guild'); +const Message = require('./Message'); +const GroupDMChannel = require('./GroupDMChannel'); /** * Represents the logged in client's Discord user. @@ -75,8 +79,18 @@ class ClientUser extends User { if (data.user_settings) this.settings = new ClientUserSettings(this, data.user_settings); } - edit(data) { - return this.client.rest.methods.updateCurrentUser(data); + edit(data, password) { + const _data = {}; + _data.username = data.username || this.username; + _data.avatar = this.client.resolver.resolveBase64(data.avatar) || this.avatar; + if (!this.bot) { + _data.email = data.email || this.email; + _data.password = password; + if (data.new_password) _data.new_password = data.newPassword; + } + + return this.client.api.users('@me').patch({ data }) + .then(newData => this.client.actions.UserUpdate.handle(newData).updated); } /** @@ -93,7 +107,7 @@ class ClientUser extends User { * .catch(console.error); */ setUsername(username, password) { - return this.client.rest.methods.updateCurrentUser({ username }, password); + return this.edit({ username }, password); } /** @@ -109,7 +123,7 @@ class ClientUser extends User { * .catch(console.error); */ setEmail(email, password) { - return this.client.rest.methods.updateCurrentUser({ email }, password); + return this.edit({ email }, password); } /** @@ -125,7 +139,7 @@ class ClientUser extends User { * .catch(console.error); */ setPassword(newPassword, oldPassword) { - return this.client.rest.methods.updateCurrentUser({ password: newPassword }, oldPassword); + return this.edit({ password: newPassword }, oldPassword); } /** @@ -140,11 +154,10 @@ class ClientUser extends User { */ setAvatar(avatar) { if (typeof avatar === 'string' && avatar.startsWith('data:')) { - return this.client.rest.methods.updateCurrentUser({ avatar }); + return this.edit({ avatar }); } else { - return this.client.resolver.resolveBuffer(avatar).then(data => - this.client.rest.methods.updateCurrentUser({ avatar: data }) - ); + return this.client.resolver.resolveBuffer(avatar) + .then(data => this.edit({ avatar: this.client.resolver.resolveBase64(data) || null })); } } @@ -266,58 +279,42 @@ class ClientUser extends User { * @returns {Promise} */ fetchMentions(options = {}) { - return this.client.rest.methods.fetchMentions(options); - } + if (options.guild instanceof Guild) options.guild = options.guild.id; + Util.mergeDefault({ limit: 25, roles: true, everyone: true, guild: null }, options); - /** - * Send a friend request. - * This is only available when using a user account. - * @param {UserResolvable} user The user to send the friend request to - * @returns {Promise} The user the friend request was sent to - */ - addFriend(user) { - user = this.client.resolver.resolveUser(user); - return this.client.rest.methods.addFriend(user); - } - - /** - * Remove a friend. - * This is only available when using a user account. - * @param {UserResolvable} user The user to remove from your friends - * @returns {Promise} The user that was removed - */ - removeFriend(user) { - user = this.client.resolver.resolveUser(user); - return this.client.rest.methods.removeFriend(user); + return this.client.api.users('@me').mentions.get({ query: options }) + .then(data => data.map(m => new Message(this.client.channels.get(m.channel_id), m, this.client))); } /** * Creates a guild. * This is only available when using a user account. * @param {string} name The name of the guild - * @param {string} region The region for the server - * @param {BufferResolvable|Base64Resolvable} [icon=null] The icon for the guild + * @param {Object} [options] Options for the creating + * @param {string} [options.region] The region for the server, defaults to the closest one available + * @param {BufferResolvable|Base64Resolvable} [options.icon=null] The icon for the guild * @returns {Promise} The guild that was created */ - createGuild(name, region, icon = null) { - if (!icon) return this.client.rest.methods.createGuild({ name, icon, region }); - if (typeof icon === 'string' && icon.startsWith('data:')) { - return this.client.rest.methods.createGuild({ name, icon, region }); + createGuild(name, { region, icon = null } = {}) { + if (!icon || (typeof icon === 'string' && icon.startsWith('data:'))) { + return this.client.api.guilds.post({ data: { name, region, icon } }) + .then(data => this.client.dataManager.newGuild(data)); } else { - return this.client.resolver.resolveBuffer(icon).then(data => - this.client.rest.methods.createGuild({ name, icon: data, region }) - ); + return this.client.resolver.resolveBuffer(icon) + .then(data => this.createGuild(name, region, this.client.resolver.resolveBase64(data) || null)); } } /** * An object containing either a user or access token, and an optional nickname. * @typedef {Object} GroupDMRecipientOptions - * @property {UserResolvable|Snowflake} [user] User to add to the Group DM + * @property {UserResolvable} [user] User to add to the Group DM * (only available if a user is creating the DM) * @property {string} [accessToken] Access token to use to add a user to the Group DM * (only available if a bot is creating the DM) * @property {string} [nick] Permanent nickname (only available if a bot is creating the DM) + * @property {string} [id] If no user resolveable is provided and you want to assign nicknames + * you must provide user ids instead */ /** @@ -326,21 +323,15 @@ class ClientUser extends User { * @returns {Promise} */ createGroupDM(recipients) { - return this.client.rest.methods.createGroupDM({ - recipients: recipients.map(u => this.client.resolver.resolveUserID(u.user)), - accessTokens: recipients.map(u => u.accessToken), - nicks: recipients.map(u => u.nick), - }); - } - - /** - * Accepts an invite to join a guild. - * This is only available when using a user account. - * @param {Invite|string} invite Invite or code to accept - * @returns {Promise} Joined guild - */ - acceptInvite(invite) { - return this.client.rest.methods.acceptInvite(invite); + const data = this.bot ? { + access_tokens: recipients.map(u => u.accessToken), + nicks: recipients.reduce((o, r) => { + if (r.nick) o[r.user ? r.user.id : r.id] = r.nick; + return o; + }, {}), + } : { recipients: recipients.map(u => this.client.resolver.resolveUserID(u)) }; + return this.client.api.users('@me').channels.post({ data }) + .then(res => new GroupDMChannel(this.client, res)); } } diff --git a/src/structures/ClientUserSettings.js b/src/structures/ClientUserSettings.js index 798f348c5..7ba29f71a 100644 --- a/src/structures/ClientUserSettings.js +++ b/src/structures/ClientUserSettings.js @@ -33,7 +33,7 @@ class ClientUserSettings { * @returns {Promise} */ update(name, value) { - return this.user.client.rest.methods.patchUserSettings({ [name]: value }); + return this.user.client.api.users('@me').settings.patch({ data: { [name]: value } }); } /** diff --git a/src/structures/Emoji.js b/src/structures/Emoji.js index 452dd6654..3010d578b 100644 --- a/src/structures/Emoji.js +++ b/src/structures/Emoji.js @@ -112,6 +112,7 @@ class Emoji { /** * Edits the emoji. * @param {EmojiEditData} data The new data for the emoji + * @param {string} [reason] Reason for editing this emoji * @returns {Promise} * @example * // Edit a emoji @@ -119,8 +120,13 @@ class Emoji { * .then(e => console.log(`Edited emoji ${e}`)) * .catch(console.error); */ - edit(data) { - return this.client.rest.methods.updateEmoji(this, data); + edit(data, reason) { + return this.client.api.guilds(this.guild.id).emojis(this.id) + .patch({ data: { + name: data.name, + roles: data.roles ? data.roles.map(r => r.id ? r.id : r) : [], + }, reason }) + .then(() => this); } /** diff --git a/src/structures/GroupDMChannel.js b/src/structures/GroupDMChannel.js index 30a11fc5c..f197b6728 100644 --- a/src/structures/GroupDMChannel.js +++ b/src/structures/GroupDMChannel.js @@ -126,16 +126,17 @@ class GroupDMChannel extends Channel { /** * Add a user to the DM - * @param {UserResolvable|string} accessTokenOrID Access token or user resolvable + * @param {UserResolvable|string} accessTokenOrUser Access token or user resolvable * @param {string} [nick] Permanent nickname to give the user (only available if a bot is creating the DM) + * @returns {Promise} */ - - addUser(accessTokenOrID, nick) { - return this.client.rest.methods.addUserToGroupDM(this, { - nick, - id: this.client.resolver.resolveUserID(accessTokenOrID), - accessToken: accessTokenOrID, - }); + addUser(accessTokenOrUser, nick) { + const id = this.client.resolver.resolveUserID(accessTokenOrUser); + const data = this.client.user.bot ? + { nick, access_token: accessTokenOrUser } : + { recipient: id }; + return this.client.api.channels(this.id).recipients(id).put({ data }) + .then(() => this); } /** diff --git a/src/structures/Guild.js b/src/structures/Guild.js index df2ee6297..5f1108292 100644 --- a/src/structures/Guild.js +++ b/src/structures/Guild.js @@ -2,12 +2,18 @@ const Long = require('long'); const User = require('./User'); const Role = require('./Role'); const Emoji = require('./Emoji'); +const Invite = require('./Invite'); +const GuildAuditLogs = require('./GuildAuditLogs'); +const Webhook = require('./Webhook'); const Presence = require('./Presence').Presence; const GuildMember = require('./GuildMember'); +const VoiceRegion = require('./VoiceRegion'); const Constants = require('../util/Constants'); const Collection = require('../util/Collection'); const Util = require('../util/Util'); const Snowflake = require('../util/Snowflake'); +const Permissions = require('../util/Permissions'); +const Shared = require('./shared'); /** * Represents a guild (or a server) on Discord. @@ -264,7 +270,7 @@ class Guild { size = format; format = 'default'; } - return Constants.Endpoints.Guild(this).Icon(this.client.options.http.cdn, this.icon, format, size); + return Constants.Endpoints.CDN(this.client.options.http.cdn).Icon(this.id, this.icon, format, size); } /** @@ -283,7 +289,7 @@ class Guild { */ get splashURL() { if (!this.splash) return null; - return Constants.Endpoints.Guild(this).Splash(this.client.options.http.cdn, this.splash); + return Constants.Endpoints.CDN(this.client.options.http.cdn).Splash(this.id, this.splash); } /** @@ -370,13 +376,15 @@ class Guild { * @returns {Promise>} */ fetchBans() { - return this.client.rest.methods.getGuildBans(this) - // This entire re-mapping can be removed in the next major release - .then(bans => { - const users = new Collection(); - for (const ban of bans.values()) users.set(ban.user.id, ban.user); - return users; - }); + return this.client.api.guilds(this.id).bans.get().then(bans => + bans.reduce((collection, ban) => { + collection.set(ban.user.id, { + reason: ban.reason, + user: this.client.dataManager.newUser(ban.user), + }); + return collection; + }, new Collection()) + ); } /** @@ -384,7 +392,15 @@ class Guild { * @returns {Promise>} */ fetchInvites() { - return this.client.rest.methods.getGuildInvites(this); + return this.client.api.guilds(this.id).invites.get() + .then(inviteItems => { + const invites = new Collection(); + for (const inviteItem of inviteItems) { + const invite = new Invite(this.client, inviteItem); + invites.set(invite.code, invite); + } + return invites; + }); } /** @@ -392,7 +408,11 @@ class Guild { * @returns {Collection} */ fetchWebhooks() { - return this.client.rest.methods.getGuildWebhooks(this); + return this.client.api.guilds(this.id).webhooks.get().then(data => { + const hooks = new Collection(); + for (const hook of data) hooks.set(hook.id, new Webhook(this.client, hook)); + return hooks; + }); } /** @@ -400,7 +420,11 @@ class Guild { * @returns {Collection} */ fetchVoiceRegions() { - return this.client.rest.methods.fetchVoiceRegions(this.id); + return this.client.api.guilds(this.id).regions.get().then(res => { + const regions = new Collection(); + for (const region of res) regions.set(region.id, new VoiceRegion(region)); + return regions; + }); } /** @@ -413,8 +437,19 @@ class Guild { * @param {string|number} [options.type] Only show entries involving this action type * @returns {Promise} */ - fetchAuditLogs(options) { - return this.client.rest.methods.getGuildAuditLogs(this, options); + fetchAuditLogs(options = {}) { + if (options.before && options.before instanceof GuildAuditLogs.Entry) options.before = options.before.id; + if (options.after && options.after instanceof GuildAuditLogs.Entry) options.after = options.after.id; + if (typeof options.type === 'string') options.type = GuildAuditLogs.Actions[options.type]; + + return this.client.api.guilds(this.id)['audit-logs'].get({ query: { + before: options.before, + after: options.after, + limit: options.limit, + user_id: this.client.resolver.resolveUserID(options.user), + action_type: options.type, + } }) + .then(data => GuildAuditLogs.build(this, data)); } /** @@ -432,7 +467,15 @@ class Guild { */ addMember(user, options) { if (this.members.has(user.id)) return Promise.resolve(this.members.get(user.id)); - return this.client.rest.methods.putGuildMember(this, user, options); + options.access_token = options.accessToken; + if (options.roles) { + const roles = options.roles; + if (roles instanceof Collection || (roles instanceof Array && roles[0] instanceof Role)) { + options.roles = roles.map(role => role.id); + } + } + return this.client.api.guilds(this.id).members(user.id).put({ data: options }) + .then(data => this.client.actions.GuildMemberGet.handle(this, data).member); } /** @@ -445,7 +488,11 @@ class Guild { user = this.client.resolver.resolveUser(user); if (!user) return Promise.reject(new Error('User is not cached. Use Client.fetchUser first.')); if (this.members.has(user.id)) return Promise.resolve(this.members.get(user.id)); - return this.client.rest.methods.getGuildMember(this, user, cache); + return this.client.api.guilds(this.id).members(user.id).get() + .then(data => { + if (cache) return this.client.actions.GuildMemberGet.handle(this, data).member; + else return new GuildMember(this, data); + }); } /** @@ -502,7 +549,7 @@ class Guild { * }).catch(console.error); */ search(options = {}) { - return this.client.rest.methods.search(this, options); + return Shared.search(this, options); } /** @@ -521,6 +568,7 @@ class Guild { /** * Updates the guild with new information - e.g. a new name. * @param {GuildEditData} data The data to update the guild with + * @param {string} [reason] Reason for editing this guild * @returns {Promise} * @example * // Set the guild name and region @@ -531,8 +579,18 @@ class Guild { * .then(updated => console.log(`New guild name ${updated.name} in region ${updated.region}`)) * .catch(console.error); */ - edit(data) { - return this.client.rest.methods.updateGuild(this, data); + edit(data, reason) { + const _data = {}; + if (data.name) _data.name = data.name; + if (data.region) _data.region = data.region; + if (data.verificationLevel) _data.verification_level = Number(data.verificationLevel); + if (data.afkChannel) _data.afk_channel_id = this.client.resolver.resolveChannel(data.afkChannel).id; + if (data.afkTimeout) _data.afk_timeout = Number(data.afkTimeout); + if (data.icon) _data.icon = this.client.resolver.resolveBase64(data.icon); + if (data.owner) _data.owner_id = this.client.resolver.resolveUser(data.owner).id; + if (data.splash) _data.splash = this.client.resolver.resolveBase64(data.splash); + return this.client.api.guilds(this.id).patch({ data: _data, reason }) + .then(newData => this.client.actions.GuildUpdate.handle(newData).updated); } /** @@ -667,7 +725,12 @@ class Guild { * @returns {Promise} */ acknowledge() { - return this.client.rest.methods.ackGuild(this); + return this.client.api.guilds(this.id).ack + .post({ data: { token: this.client.rest._ackToken } }) + .then(res => { + if (res.token) this.client.rest._ackToken = res.token; + return this; + }); } /** @@ -697,19 +760,26 @@ class Guild { * .then(user => console.log(`Banned ${user.username || user.id || user} from ${guild.name}`)) * .catch(console.error); */ - ban(user, options = {}) { - if (typeof options === 'number') { - options = { reason: null, 'delete-message-days': options }; - } else if (typeof options === 'string') { - options = { reason: options, 'delete-message-days': 0 }; - } + ban(user, options = { days: 0 }) { if (options.days) options['delete-message-days'] = options.days; - return this.client.rest.methods.banGuildMember(this, user, options); + const id = this.client.resolver.resolveUserID(user); + if (!id) return Promise.reject(new Error('Couldn\'t resolve the user ID to ban.')); + return this.client.api.guilds(this.id).bans(id).put({ query: options }) + .then(() => { + if (user instanceof GuildMember) return user; + const _user = this.client.resolver.resolveUser(id); + if (_user) { + const member = this.client.resolver.resolveGuildMember(this, _user); + return member || _user; + } + return id; + }); } /** * Unbans a user from the guild. * @param {UserResolvable} user The user to unban + * @param {string} [reason] Reason for unbanning user * @returns {Promise} * @example * // Unban a user by ID (or with a user/guild member object) @@ -717,29 +787,35 @@ class Guild { * .then(user => console.log(`Unbanned ${user.username} from ${guild.name}`)) * .catch(console.error); */ - unban(user) { - return this.client.rest.methods.unbanGuildMember(this, user); + unban(user, reason) { + const id = this.client.resolver.resolveUserID(user); + if (!id) throw new Error('Couldn\'t resolve the user ID to unban.'); + + return this.client.api.guilds(this.id).bans(id).delete({ reason }) + .then(() => user); } /** * Prunes members from the guild based on how long they have been inactive. - * @param {number} days Number of days of inactivity required to kick - * @param {boolean} [dry=false] If true, will return number of users that will be kicked, without actually doing it + * @param {number} [options.days=7] Number of days of inactivity required to kick + * @param {boolean} [options.dry=false] Get number of users that will be kicked, without actually kicking them + * @param {string} [options.reason] Reason for this prune * @returns {Promise} The number of members that were/will be kicked * @example * // See how many members will be pruned - * guild.pruneMembers(12, true) + * guild.pruneMembers({ dry: true }) * .then(pruned => console.log(`This will prune ${pruned} people!`)) * .catch(console.error); * @example * // Actually prune the members - * guild.pruneMembers(12) + * guild.pruneMembers({ days: 1, reason: 'too many people!' }) * .then(pruned => console.log(`I just pruned ${pruned} people!`)) * .catch(console.error); */ - pruneMembers(days, dry = false) { + pruneMembers({ days = 7, dry = false, reason } = {}) { if (typeof days !== 'number') throw new TypeError('Days must be a number.'); - return this.client.rest.methods.pruneGuildMembers(this, days, dry); + return this.client.api.guilds(this.id).prune[dry ? 'get' : 'post']({ query: { days }, reason }) + .then(data => data.pruned); } /** @@ -754,7 +830,9 @@ class Guild { * Creates a new channel in the guild. * @param {string} name The name of the new channel * @param {string} type The type of the new channel, either `text` or `voice` - * @param {Array} overwrites Permission overwrites to apply to the new channel + * @param {Object} options Options + * @param {Array} [options.overwrites] Permission overwrites to apply to the new channel + * @param {string} [options.reason] Reason for creating this channel * @returns {Promise} * @example * // Create a new text channel @@ -762,8 +840,14 @@ class Guild { * .then(channel => console.log(`Created new channel ${channel}`)) * .catch(console.error); */ - createChannel(name, type, overwrites) { - return this.client.rest.methods.createChannel(this, name, type, overwrites); + createChannel(name, type, { overwrites, reason } = {}) { + if (overwrites instanceof Collection) overwrites = overwrites.array(); + return this.client.api.guilds(this.id).channels.post({ + data: { + name, type, permission_overwrites: overwrites, + }, + reason, + }).then(data => this.client.actions.ChannelCreate.handle(data).channel); } /** @@ -783,12 +867,30 @@ class Guild { * .catch(console.error); */ setChannelPositions(channelPositions) { - return this.client.rest.methods.updateChannelPositions(this.id, channelPositions); + const data = new Array(channelPositions.length); + for (let i = 0; i < channelPositions.length; i++) { + data[i] = { + id: this.client.resolver.resolveChannelID(channelPositions[i].channel), + position: channelPositions[i].position, + }; + } + + return this.client.api.guilds(this.id).channels.patch({ data: { + guild_id: this.id, + channels: channelPositions, + } }).then(() => + this.client.actions.GuildChannelsPositionUpdate.handle({ + guild_id: this.id, + channels: channelPositions, + }).guild + ); } /** * Creates a new role in the guild with given information - * @param {RoleData} [data] The data to update the role with + * @param {Object} [options] Options + * @param {RoleData} [options.data] The data to update the role with + * @param {string} [options.reason] Reason for creating this role * @returns {Promise} * @example * // Create a new role @@ -796,16 +898,27 @@ class Guild { * .then(role => console.log(`Created role ${role}`)) * .catch(console.error); * @example - * // Create a new role with data + * // Create a new role with data and a reason * guild.createRole({ - * name: 'Super Cool People', - * color: 'BLUE', + * data: { + * name: 'Super Cool People', + * color: 'BLUE', + * }, + * reason: 'we needed a role for Super Cool People', * }) * .then(role => console.log(`Created role ${role}`)) * .catch(console.error) */ - createRole(data = {}) { - return this.client.rest.methods.createGuildRole(this, data); + createRole({ data = {}, reason } = {}) { + if (data.color) data.color = this.client.resolver.resolveColor(data.color); + if (data.permissions) data.permissions = Permissions.resolve(data.permissions); + + return this.client.api.guilds(this.id).roles.post({ data, reason }).then(role => + this.client.actions.GuildRoleCreate.handle({ + guild_id: this.id, + role, + }).role + ); } /** @@ -826,16 +939,18 @@ class Guild { * .catch(console.error); */ createEmoji(attachment, name, roles) { - return new Promise(resolve => { - if (typeof attachment === 'string' && attachment.startsWith('data:')) { - resolve(this.client.rest.methods.createEmoji(this, attachment, name, roles)); - } else { - this.client.resolver.resolveBuffer(attachment).then(data => { - const dataURI = this.client.resolver.resolveBase64(data); - resolve(this.client.rest.methods.createEmoji(this, dataURI, name, roles)); - }); - } - }); + if (typeof attahment === 'string' && attachment.startsWith('data:')) { + const data = { image: attachment, name }; + if (roles) data.roles = roles.map(r => r.id ? r.id : r); + return this.client.api.guilds(this.id).emojis.post({ data }) + .then(emoji => this.client.actions.GuildEmojiCreate.handle(this, emoji).emoji); + } else { + return this.client.resolver.resolveBuffer(attachment) + .then(data => { + const dataURI = this.client.resolver.resolveBase64(data); + return this.createEmoji(dataURI, name, roles); + }); + } } /** @@ -845,7 +960,8 @@ class Guild { */ deleteEmoji(emoji) { if (!(emoji instanceof Emoji)) emoji = this.emojis.get(emoji); - return this.client.rest.methods.deleteEmoji(emoji); + return this.client.api.guilds(this.id).emojis(this.id).delete() + .then(() => this.client.actions.GuildEmojiDelete.handle(emoji).data); } /** @@ -858,7 +974,9 @@ class Guild { * .catch(console.error); */ leave() { - return this.client.rest.methods.leaveGuild(this); + if (this.ownerID === this.client.user.id) return Promise.reject(new Error('Guild is owned by the client.')); + return this.rest.api.users('@me').guilds(this.id).delete() + .then(() => this.client.actions.GuildDelete.handle({ id: this.id }).guild); } /** @@ -871,7 +989,8 @@ class Guild { * .catch(console.error); */ delete() { - return this.client.rest.methods.deleteGuild(this); + return this.client.api.guilds(this.id).delete() + .then(() => this.client.actions.GuildDelete.handle({ id: this.id }).guild); } /** @@ -1028,7 +1147,13 @@ class Guild { Util.moveElementInArray(updatedRoles, role, position, relative); updatedRoles = updatedRoles.map((r, i) => ({ id: r.id, position: i })); - return this.client.rest.methods.setRolePositions(this.id, updatedRoles); + return this.client.api.guilds(this.id).roles.patch({ data: updatedRoles }) + .then(() => + this.client.actions.GuildRolesPositionUpdate.handle({ + guild_id: this.id, + roles: updatedRoles, + }).guild + ); } /** @@ -1052,7 +1177,13 @@ class Guild { Util.moveElementInArray(updatedChannels, channel, position, relative); updatedChannels = updatedChannels.map((r, i) => ({ id: r.id, position: i })); - return this.client.rest.methods.setChannelPositions(this.id, updatedChannels); + return this.client.api.guilds(this.id).channels.patch({ data: updatedChannels }) + .then(() => + this.client.actions.GuildChannelsPositionUpdate.handle({ + guild_id: this.id, + roles: updatedChannels, + }).guild + ); } /** diff --git a/src/structures/GuildAuditLogs.js b/src/structures/GuildAuditLogs.js index 9202ed231..163c89d01 100644 --- a/src/structures/GuildAuditLogs.js +++ b/src/structures/GuildAuditLogs.js @@ -10,6 +10,7 @@ const Targets = { WEBHOOK: 'WEBHOOK', EMOJI: 'EMOJI', MESSAGE: 'MESSAGE', + UNKNOWN: 'UNKNOWN', }; const Actions = { @@ -83,7 +84,7 @@ class GuildAuditLogs { if (target < 60) return Targets.WEBHOOK; if (target < 70) return Targets.EMOJI; if (target < 80) return Targets.MESSAGE; - return null; + return Targets.UNKNOWN; } @@ -219,11 +220,14 @@ class GuildAuditLogsEntry { } } - if ([Targets.USER, Targets.GUILD].includes(targetType)) { + + if (targetType === Targets.UNKNOWN) { /** * The target of this entry - * @type {?Guild|User|Role|Emoji|Invite|Webhook} + * @type {Snowflake|Guild|User|Role|Emoji|Invite|Webhook} */ + this.target = data.target_id; + } else if ([Targets.USER, Targets.GUILD].includes(targetType)) { this.target = guild.client[`${targetType.toLowerCase()}s`].get(data.target_id); } else if (targetType === Targets.WEBHOOK) { this.target = guild.fetchWebhooks() diff --git a/src/structures/GuildChannel.js b/src/structures/GuildChannel.js index 9fa7ad80c..1d5750a3d 100644 --- a/src/structures/GuildChannel.js +++ b/src/structures/GuildChannel.js @@ -1,5 +1,6 @@ const Channel = require('./Channel'); const Role = require('./Role'); +const Invite = require('./Invite'); const PermissionOverwrites = require('./PermissionOverwrites'); const Permissions = require('../util/Permissions'); const Collection = require('../util/Collection'); @@ -138,7 +139,8 @@ class GuildChannel extends Channel { * Overwrites the permissions for a user or role in this channel. * @param {RoleResolvable|UserResolvable} userOrRole The user or role to update * @param {PermissionOverwriteOptions} options The configuration for the update - * @returns {Promise} + * @param {string} [reason] Reason for creating/editing this overwrite + * @returns {Promise} * @example * // Overwrite permissions for a message author * message.channel.overwritePermissions(message.author, { @@ -147,7 +149,7 @@ class GuildChannel extends Channel { * .then(() => console.log('Done!')) * .catch(console.error); */ - overwritePermissions(userOrRole, options) { + overwritePermissions(userOrRole, options, reason) { const payload = { allow: 0, deny: 0, @@ -186,7 +188,9 @@ class GuildChannel extends Channel { } } - return this.client.rest.methods.setChannelOverwrite(this, payload); + return this.client.api.channels(this.id).permissions(payload.id) + .put({ data: payload, reason }) + .then(() => this); } /** @@ -202,6 +206,7 @@ class GuildChannel extends Channel { /** * Edits the channel. * @param {ChannelData} data The new data for the channel + * @param {string} [reason] Reason for editing this channel * @returns {Promise} * @example * // Edit a channel @@ -209,8 +214,17 @@ class GuildChannel extends Channel { * .then(c => console.log(`Edited channel ${c}`)) * .catch(console.error); */ - edit(data) { - return this.client.rest.methods.updateChannel(this, data); + edit(data, reason) { + return this.client.api.channels(this.id).patch({ + data: { + name: (data.name || this.name).trim(), + topic: data.topic || this.topic, + position: data.position || this.position, + bitrate: data.bitrate || this.bitrate, + user_limit: data.userLimit || this.userLimit, + }, + reason, + }).then(newData => this.client.actions.ChannelUpdate.handle(newData).updated); } /** @@ -253,7 +267,7 @@ class GuildChannel extends Channel { * .catch(console.error); */ setTopic(topic) { - return this.client.rest.methods.updateChannel(this, { topic }); + return this.edit({ topic }); } /** @@ -269,10 +283,14 @@ class GuildChannel extends Channel { * kicked after 24 hours if they have not yet received a role * @param {number} [options.maxAge=86400] How long the invite should last (in seconds, 0 for forever) * @param {number} [options.maxUses=0] Maximum number of uses + * @param {string} [reason] Reason for creating this * @returns {Promise} */ - createInvite(options = {}) { - return this.client.rest.methods.createChannelInvite(this, options); + createInvite({ temporary = false, maxAge = 86400, maxUses = 0 }, reason) { + return this.client.api.channels(this.id).invites.post({ data: { + temporary, max_age: maxAge, max_uses: maxUses, + }, reason }) + .then(invite => new Invite(this.client, invite)); } /** @@ -322,6 +340,20 @@ class GuildChannel extends Channel { this.permissionsFor(this.client.user).has(Permissions.FLAGS.MANAGE_CHANNELS); } + /** + * Deletes this channel. + * @param {string} [reason] Reason for deleting this channel + * @returns {Promise} + * @example + * // Delete the channel + * channel.delete('making room for new channels') + * .then() // Success + * .catch(console.error); // Log error + */ + delete(reason) { + return this.client.api.channels(this.id).delete({ reason }).then(() => this); + } + /** * When concatenated with a string, this automatically returns the channel's mention instead of the Channel object. * @returns {string} diff --git a/src/structures/GuildMember.js b/src/structures/GuildMember.js index a311f0302..201a2b792 100644 --- a/src/structures/GuildMember.js +++ b/src/structures/GuildMember.js @@ -332,10 +332,24 @@ class GuildMember { /** * Edit a guild member. * @param {GuildMemberEditData} data The data to edit the member with + * @param {string} [reason] Reason for editing this user * @returns {Promise} */ - edit(data) { - return this.client.rest.methods.updateGuildMember(this, data); + edit(data, reason) { + if (data.channel) { + data.channel_id = this.client.resolver.resolveChannel(data.channel).id; + data.channel = null; + } + if (data.roles) data.roles = data.roles.map(role => role instanceof Role ? role.id : role); + let endpoint = this.client.api.guilds(this.guild.id); + if (this.user.id === this.client.user.id) { + const keys = Object.keys(data); + if (keys.length === 1 && keys[0] === 'nick') endpoint = endpoint.members('@me').nick; + else endpoint = endpoint.members(this.id); + } else { + endpoint = endpoint.members(this.id); + } + return endpoint.patch({ data, reason }).then(newData => this.guild._updateMember(this, newData).mem); } /** @@ -382,7 +396,10 @@ class GuildMember { addRole(role) { if (!(role instanceof Role)) role = this.guild.roles.get(role); if (!role) return Promise.reject(new TypeError('Supplied parameter was neither a Role nor a Snowflake.')); - return this.client.rest.methods.addMemberRole(this, role); + if (this._roles.includes(role.id)) return Promise.resolve(this); + return this.client.api.guilds(this.guild.id).members(this.user.id).roles(role.id) + .put() + .then(() => this); } /** @@ -394,9 +411,9 @@ class GuildMember { let allRoles; if (roles instanceof Collection) { allRoles = this._roles.slice(); - for (const role of roles.values()) allRoles.push(role.id); + for (const role of roles.values()) allRoles.push(role.id ? role.id : role); } else { - allRoles = this._roles.concat(roles); + allRoles = this._roles.concat(roles.map(r => r.id ? r.id : r)); } return this.edit({ roles: allRoles }); } @@ -409,7 +426,9 @@ class GuildMember { removeRole(role) { if (!(role instanceof Role)) role = this.guild.roles.get(role); if (!role) return Promise.reject(new TypeError('Supplied parameter was neither a Role nor a Snowflake.')); - return this.client.rest.methods.removeMemberRole(this, role); + return this.client.api.guilds(this.guild.id).members(this.user.id).roles(role.id) + .delete() + .then(() => this); } /** @@ -464,7 +483,13 @@ class GuildMember { * @returns {Promise} */ kick(reason) { - return this.client.rest.methods.kickGuildMember(this.guild, this, reason); + return this.client.api.guilds(this.guild.id).members(this.user.id).delete({ reason }) + .then(() => + this.client.actions.GuildMemberRemove.handle({ + guild_id: this.guild.id, + user: this.user, + }).member + ); } /** diff --git a/src/structures/Invite.js b/src/structures/Invite.js index cd9324b44..db2cbf7dc 100644 --- a/src/structures/Invite.js +++ b/src/structures/Invite.js @@ -136,15 +136,16 @@ class Invite { * @readonly */ get url() { - return Constants.Endpoints.inviteLink(this.code); + return Constants.Endpoints.invite(this.code); } /** * Deletes this invite. + * @param {string} [reason] Reason for deleting this invite * @returns {Promise} */ - delete() { - return this.client.rest.methods.deleteInvite(this); + delete(reason) { + return this.client.api.invites(this.code).delete({ reason }).then(() => this); } /** diff --git a/src/structures/Message.js b/src/structures/Message.js index 8ca7b75a1..4881fe127 100644 --- a/src/structures/Message.js +++ b/src/structures/Message.js @@ -380,7 +380,27 @@ class Message { } else if (!options) { options = {}; } - return this.client.rest.methods.updateMessage(this, content, options); + + if (typeof content !== 'undefined') content = this.client.resolver.resolveString(content); + + const { embed, code, reply } = options; + + // Wrap everything in a code block + if (typeof code !== 'undefined' && (typeof code !== 'boolean' || code === true)) { + content = Util.escapeMarkdown(this.client.resolver.resolveString(content), true); + content = `\`\`\`${typeof code !== 'boolean' ? code || '' : ''}\n${content}\n\`\`\``; + } + + // Add the reply prefix + if (reply && this.channel.type !== 'dm') { + const id = this.client.resolver.resolveUserID(reply); + const mention = `<@${reply instanceof GuildMember && reply.nickname ? '!' : ''}${id}>`; + content = `${mention}${content ? `, ${content}` : ''}`; + } + + return this.client.api.channels(this.channel.id).messages(this.id) + .patch({ data: { content, embed } }) + .then(data => this.client.actions.MessageUpdate.handle(data).updated); } /** @@ -388,7 +408,8 @@ class Message { * @returns {Promise} */ pin() { - return this.client.rest.methods.pinMessage(this); + return this.client.api.channels(this.channel.id).pins(this.id).put() + .then(() => this); } /** @@ -396,7 +417,8 @@ class Message { * @returns {Promise} */ unpin() { - return this.client.rest.methods.unpinMessage(this); + return this.client.api.channels(this.channel.id).pins(this.id).delete() + .then(() => this); } /** @@ -408,7 +430,9 @@ class Message { emoji = this.client.resolver.resolveEmojiIdentifier(emoji); if (!emoji) throw new TypeError('Emoji must be a string or Emoji/ReactionEmoji'); - return this.client.rest.methods.addMessageReaction(this, emoji); + return this.client.api.channels(this.channel.id).messages(this.id).reactions(emoji)['@me'] + .put() + .then(() => this._addReaction(Util.parseEmoji(emoji), this.client.user)); } /** @@ -416,12 +440,15 @@ class Message { * @returns {Promise} */ clearReactions() { - return this.client.rest.methods.removeMessageReactions(this); + return this.client.api.channels(this.channel.id).messages(this.id).reactions.delete() + .then(() => this); } /** * Deletes the message. - * @param {number} [timeout=0] How long to wait to delete the message in milliseconds + * @param {Object} [options] Options + * @param {number} [options.timeout=0] How long to wait to delete the message in milliseconds + * @param {string} [options.reason] Reason for deleting this message, if it does not belong to the client user * @returns {Promise} * @example * // Delete a message @@ -429,13 +456,19 @@ class Message { * .then(msg => console.log(`Deleted message from ${msg.author}`)) * .catch(console.error); */ - delete(timeout = 0) { + delete({ timeout = 0, reason } = {}) { if (timeout <= 0) { - return this.client.rest.methods.deleteMessage(this); + return this.client.api.channels(this.channel.id).messages(this.id) + .delete({ reason }) + .then(() => + this.client.actions.MessageDelete.handle({ + id: this.id, + channel_id: this.channel.id, + }).message); } else { return new Promise(resolve => { this.client.setTimeout(() => { - resolve(this.delete()); + resolve(this.delete({ reason })); }, timeout); }); } @@ -468,7 +501,12 @@ class Message { * @returns {Promise} */ acknowledge() { - return this.client.rest.methods.ackMessage(this); + return this.client.api.channels(this.channel.id).messages(this.id).ack + .post({ data: { token: this.client.rest._ackToken } }) + .then(res => { + if (res.token) this.client.rest._ackToken = res.token; + return this; + }); } /** diff --git a/src/structures/MessageReaction.js b/src/structures/MessageReaction.js index 251bafcc0..87178aa20 100644 --- a/src/structures/MessageReaction.js +++ b/src/structures/MessageReaction.js @@ -61,12 +61,19 @@ class MessageReaction { * @returns {Promise} */ remove(user = this.message.client.user) { - const message = this.message; const userID = this.message.client.resolver.resolveUserID(user); if (!userID) return Promise.reject(new Error('Couldn\'t resolve the user ID to remove from the reaction.')); - return message.client.rest.methods.removeMessageReaction( - message, this.emoji.identifier, userID - ); + return this.message.client.api.channels(this.message.channel.id).messages(this.message.id) + .reactions(this.emoji.identifier)[userID === this.message.client.user.id ? '@me' : userID] + .delete() + .then(() => + this.message.client.actions.MessageReactionRemove.handle({ + user_id: userID, + message_id: this.message.id, + emoji: this.emoji, + channel_id: this.message.channel.id, + }).reaction + ); } /** @@ -76,17 +83,18 @@ class MessageReaction { */ fetchUsers(limit = 100) { const message = this.message; - return message.client.rest.methods.getMessageReactionUsers( - message, this.emoji.identifier, limit - ).then(users => { - this.users = new Collection(); - for (const rawUser of users) { - const user = this.message.client.dataManager.newUser(rawUser); - this.users.set(user.id, user); - } - this.count = this.users.size; - return this.users; - }); + return message.client.api.channels(message.channel.id).messages(message.id) + .reactions(this.emoji.identifier) + .get({ query: { limit } }) + .then(users => { + this.users = new Collection(); + for (const rawUser of users) { + const user = message.client.dataManager.newUser(rawUser); + this.users.set(user.id, user); + } + this.count = this.users.size; + return this.users; + }); } } diff --git a/src/structures/OAuth2Application.js b/src/structures/OAuth2Application.js index b6f64dd3b..52eb89a12 100644 --- a/src/structures/OAuth2Application.js +++ b/src/structures/OAuth2Application.js @@ -137,7 +137,8 @@ class OAuth2Application { * @returns {OAuth2Application} */ reset() { - return this.client.rest.methods.resetApplication(this.id); + return this.rest.api.oauth2.applications(this.id).reset.post() + .then(app => new OAuth2Application(this.client, app)); } /** diff --git a/src/structures/PermissionOverwrites.js b/src/structures/PermissionOverwrites.js index 8044be45a..3ffcaf98b 100644 --- a/src/structures/PermissionOverwrites.js +++ b/src/structures/PermissionOverwrites.js @@ -33,10 +33,13 @@ class PermissionOverwrites { /** * Delete this Permission Overwrite. + * @param {string} [reason] Reason for deleting this overwrite * @returns {Promise} */ - delete() { - return this.channel.client.rest.methods.deletePermissionOverwrites(this); + delete(reason) { + return this.channel.client.api.channels(this.channel.id).permissions(this.id) + .delete({ reason }) + .then(() => this); } } diff --git a/src/structures/Role.js b/src/structures/Role.js index 22594fd8c..40d997fe7 100644 --- a/src/structures/Role.js +++ b/src/structures/Role.js @@ -190,6 +190,7 @@ class Role { /** * Edits the role. * @param {RoleData} data The new data for the role + * @param {string} [reason] Reason for editing this role * @returns {Promise} * @example * // Edit a role @@ -197,8 +198,20 @@ class Role { * .then(r => console.log(`Edited role ${r}`)) * .catch(console.error); */ - edit(data) { - return this.client.rest.methods.updateGuildRole(this, data); + edit(data, reason) { + if (data.permissions) data.permissions = Permissions.resolve(data.permissions); + else data.permissions = this.permissions; + return this.client.api.guilds(this.guild.id).roles(this.id).patch({ + data: { + name: data.name || this.name, + position: typeof data.position !== 'undefined' ? data.position : this.position, + color: this.client.resolver.resolveColor(data.color || this.color), + hoist: typeof data.hoist !== 'undefined' ? data.hoist : this.hoist, + mentionable: typeof data.mentionable !== 'undefined' ? data.mentionable : this.mentionable, + }, + reason, + }) + .then(role => this.client.actions.GuildRoleUpdate.handle({ role, guild_id: this.guild.id }).updated); } /** @@ -288,6 +301,7 @@ class Role { /** * Deletes the role. + * @param {string} [reason] Reason for deleting this role * @returns {Promise} * @example * // Delete a role @@ -295,8 +309,11 @@ class Role { * .then(r => console.log(`Deleted role ${r}`)) * .catch(console.error); */ - delete() { - return this.client.rest.methods.deleteGuildRole(this); + delete(reason) { + return this.client.api.guilds(this.guild.id).roles(this.id).delete({ reason }) + .then(() => + this.client.actions.GuildRoleDelete.handle({ guild_id: this.guild.id, role_id: this.id }).role + ); } /** diff --git a/src/structures/TextChannel.js b/src/structures/TextChannel.js index 818b9217a..0f0d05ba9 100644 --- a/src/structures/TextChannel.js +++ b/src/structures/TextChannel.js @@ -1,4 +1,5 @@ const GuildChannel = require('./GuildChannel'); +const Webhook = require('./Webhook'); const TextBasedChannel = require('./interfaces/TextBasedChannel'); const Collection = require('../util/Collection'); @@ -56,7 +57,11 @@ class TextChannel extends GuildChannel { * @returns {Promise>} */ fetchWebhooks() { - return this.client.rest.methods.getChannelWebhooks(this); + return this.client.api.channels(this.id).webhooks.get().then(data => { + const hooks = new Collection(); + for (const hook of data) hooks.set(hook.id, new Webhook(this.client, hook)); + return hooks; + }); } /** @@ -70,15 +75,14 @@ class TextChannel extends GuildChannel { * .catch(console.error) */ createWebhook(name, avatar) { - return new Promise(resolve => { - if (typeof avatar === 'string' && avatar.startsWith('data:')) { - resolve(this.client.rest.methods.createWebhook(this, name, avatar)); - } else { - this.client.resolver.resolveBuffer(avatar).then(data => - resolve(this.client.rest.methods.createWebhook(this, name, data)) - ); - } - }); + if (typeof avatar === 'string' && avatar.startsWith('data:')) { + return this.client.api.channels(this.id).webhooks.post({ data: { + name, avatar, + } }).then(data => new Webhook(this.client, data)); + } else { + return this.client.resolver.resolveBuffer(avatar).then(data => + this.createWebhook(name, this.client.resolver.resolveBase64(data) || null)); + } } // These are here only for documentation purposes - they are implemented by TextBasedChannel diff --git a/src/structures/User.js b/src/structures/User.js index b7b6eb96d..2797f7ff9 100644 --- a/src/structures/User.js +++ b/src/structures/User.js @@ -1,6 +1,7 @@ const TextBasedChannel = require('./interfaces/TextBasedChannel'); const Constants = require('../util/Constants'); const Presence = require('./Presence').Presence; +const UserProfile = require('./UserProfile'); const Snowflake = require('../util/Snowflake'); /** @@ -115,7 +116,7 @@ class User { size = format; format = 'default'; } - return Constants.Endpoints.User(this).Avatar(this.client.options.http.cdn, this.avatar, format, size); + return Constants.Endpoints.CDN(this.client.options.http.cdn).Avatar(this.id, this.avatar, format, size); } /** @@ -199,7 +200,11 @@ class User { * @returns {Promise} */ createDM() { - return this.client.rest.methods.createDM(this); + if (this.dmChannel) return Promise.resolve(this.dmChannel); + return this.client.api.users(this.client.user.id).channels.post({ data: { + recipient_id: this.id, + } }) + .then(data => this.client.actions.ChannelCreate.handle(data).channel); } /** @@ -207,43 +212,10 @@ class User { * @returns {Promise} */ deleteDM() { - return this.client.rest.methods.deleteChannel(this); - } - - /** - * Sends a friend request to the user. - * This is only available when using a user account. - * @returns {Promise} - */ - addFriend() { - return this.client.rest.methods.addFriend(this); - } - - /** - * Removes the user from your friends. - * This is only available when using a user account. - * @returns {Promise} - */ - removeFriend() { - return this.client.rest.methods.removeFriend(this); - } - - /** - * Blocks the user. - * This is only available when using a user account. - * @returns {Promise} - */ - block() { - return this.client.rest.methods.blockUser(this); - } - - /** - * Unblocks the user. - * This is only available when using a user account. - * @returns {Promise} - */ - unblock() { - return this.client.rest.methods.unblockUser(this); + if (!this.dmChannel) return Promise.reject(new Error('No DM Channel exists!')); + return this.client.api.channels(this.dmChannel.id).delete().then(data => + this.client.actions.ChannelDelete.handle(data).channel + ); } /** @@ -252,7 +224,7 @@ class User { * @returns {Promise} */ fetchProfile() { - return this.client.rest.methods.fetchUserProfile(this); + return this.client.api.users(this.id).profile.get().then(data => new UserProfile(data)); } /** @@ -262,7 +234,8 @@ class User { * @returns {Promise} */ setNote(note) { - return this.client.rest.methods.setNote(this, note); + return this.client.api.users('@me').notes(this.id).put({ data: { note } }) + .then(() => this); } /** diff --git a/src/structures/Webhook.js b/src/structures/Webhook.js index ca20b0943..3a6bc5b6c 100644 --- a/src/structures/Webhook.js +++ b/src/structures/Webhook.js @@ -91,7 +91,7 @@ class Webhook { * Send a message with this webhook. * @param {StringResolvable} [content] The content to send * @param {WebhookMessageOptions} [options={}] The options to provide - * @returns {Promise} + * @returns {Promise} * @example * // Send a message * webhook.send('hello!') @@ -106,6 +106,23 @@ class Webhook { options = {}; } + if (!options.username) options.username = this.name; + + if (options.avatarURL) { + options.avatar_url = options.avatarURL; + options.avatarURL = null; + } + + if (typeof content !== 'undefined') content = this.client.resolver.resolveString(content); + if (content) { + if (options.disableEveryone || + (typeof options.disableEveryone === 'undefined' && this.client.options.disableEveryone) + ) { + content = content.replace(/@(everyone|here)/g, '@\u200b$1'); + } + } + options.content = content; + if (options.file) { if (options.files) options.files.push(options.file); else options.files = [options.file]; @@ -132,16 +149,29 @@ class Webhook { file.file = buffer; return file; }) - )).then(files => this.client.rest.methods.sendWebhookMessage(this, content, options, files)); + )).then(files => this.client.api.webhooks(this.id, this.token).post({ + data: options, + query: { wait: true }, + files, + auth: false, + })); } - return this.client.rest.methods.sendWebhookMessage(this, content, options); + return this.client.api.webhooks(this.id, this.token).post({ + data: options, + query: { wait: true }, + auth: false, + }).then(data => { + if (!this.client.channels) return data; + const Message = require('./Message'); + return new Message(this.client.channels.get(data.channel_id, data, this.client)); + }); } /** * Send a raw slack message with this webhook. * @param {Object} body The raw body to send - * @returns {Promise} + * @returns {Promise} * @example * // Send a slack message * webhook.sendSlackMessage({ @@ -156,34 +186,49 @@ class Webhook { * }).catch(console.error); */ sendSlackMessage(body) { - return this.client.rest.methods.sendSlackWebhookMessage(this, body); + return this.client.api.webhooks(this.id, this.token).slack.post({ + query: { wait: true }, + auth: false, + data: body, + }).then(data => { + if (!this.client.channels) return data; + const Message = require('./Message'); + return new Message(this.client.channels.get(data.channel_id, data, this.client)); + }); } /** * Edit the webhook. - * @param {string} name The new name for the webhook - * @param {BufferResolvable} avatar The new avatar for the webhook + * @param {Object} options Options + * @param {string} [options.name] New name for this webhook + * @param {BufferResolvable} [options.avatar] New avatar for this webhook + * @param {string} [reason] Reason for editing this webhook * @returns {Promise} */ - edit(name = this.name, avatar) { - if (avatar) { + edit({ name = this.name, avatar }, reason) { + if (avatar && (typeof avatar === 'string' && !avatar.startsWith('data:'))) { return this.client.resolver.resolveBuffer(avatar).then(file => { const dataURI = this.client.resolver.resolveBase64(file); - return this.client.rest.methods.editWebhook(this, name, dataURI); + return this.edit({ name, avatar: dataURI }, reason); }); } - return this.client.rest.methods.editWebhook(this, name).then(data => { - this.setup(data); + return this.client.api.webhooks(this.id, this.token).patch({ + data: { name, avatar }, + reason, + }).then(data => { + this.name = data.name; + this.avatar = data.avatar; return this; }); } /** * Delete the webhook. + * @param {string} [reason] Reason for deleting this webhook * @returns {Promise} */ - delete() { - return this.client.rest.methods.deleteWebhook(this); + delete(reason) { + return this.client.api.webhooks(this.id, this.token).delete({ reason }); } } diff --git a/src/structures/interfaces/TextBasedChannel.js b/src/structures/interfaces/TextBasedChannel.js index eb8296681..7c3b64fda 100644 --- a/src/structures/interfaces/TextBasedChannel.js +++ b/src/structures/interfaces/TextBasedChannel.js @@ -1,7 +1,8 @@ const path = require('path'); -const Message = require('../Message'); const MessageCollector = require('../MessageCollector'); +const Shared = require('../shared'); const Collection = require('../../util/Collection'); +const Snowflake = require('../../util/Snowflake'); /** * Interface for classes that have text-channel-like features. @@ -78,6 +79,8 @@ class TextBasedChannel { options = {}; } + if (!options.content) options.content = content; + if (options.embed && options.embed.file) options.file = options.embed.file; if (options.file) { @@ -106,10 +109,13 @@ class TextBasedChannel { file.file = buffer; return file; }) - )).then(files => this.client.rest.methods.sendMessage(this, content, options, files)); + )).then(files => { + options.files = files; + return Shared.sendMessage(this, options); + }); } - return this.client.rest.methods.sendMessage(this, content, options); + return Shared.sendMessage(this, options); } /** @@ -125,14 +131,17 @@ class TextBasedChannel { * .catch(console.error); */ fetchMessage(messageID) { + const Message = require('../Message'); if (!this.client.user.bot) { - return this.fetchMessages({ limit: 1, around: messageID }).then(messages => { + return this.fetchMessages({ limit: 1, around: messageID }) + .then(messages => { const msg = messages.get(messageID); if (!msg) throw new Error('Message not found.'); return msg; }); } - return this.client.rest.methods.getChannelMessage(this, messageID).then(data => { + return this.client.api.channels(this.id).messages(messageID).get() + .then(data => { const msg = data instanceof Message ? data : new Message(this, data, this.client); this._cacheMessage(msg); return msg; @@ -160,7 +169,9 @@ class TextBasedChannel { * .catch(console.error); */ fetchMessages(options = {}) { - return this.client.rest.methods.getChannelMessages(this, options).then(data => { + const Message = require('../Message'); + return this.client.api.channels(this.id).messages.get({ query: options }) + .then(data => { const messages = new Collection(); for (const message of data) { const msg = new Message(this, message, this.client); @@ -176,7 +187,8 @@ class TextBasedChannel { * @returns {Promise>} */ fetchPinnedMessages() { - return this.client.rest.methods.getChannelPinnedMessages(this).then(data => { + const Message = require('../Message'); + return this.client.api.channels(this.id).pins.get().then(data => { const messages = new Collection(); for (const message of data) { const msg = new Message(this, message, this.client); @@ -231,7 +243,7 @@ class TextBasedChannel { * }).catch(console.error); */ search(options = {}) { - return this.client.rest.methods.search(this, options); + return Shared.search(this, options); } /** @@ -244,13 +256,14 @@ class TextBasedChannel { startTyping(count) { if (typeof count !== 'undefined' && count < 1) throw new RangeError('Count must be at least 1.'); if (!this.client.user._typing.has(this.id)) { + const endpoint = this.client.api.channels(this.id).typing; this.client.user._typing.set(this.id, { count: count || 1, interval: this.client.setInterval(() => { - this.client.rest.methods.sendTyping(this.id); + endpoint.post(); }, 9000), }); - this.client.rest.methods.sendTyping(this.id); + endpoint.post(); } else { const entry = this.client.user._typing.get(this.id); entry.count = count || entry.count + 1; @@ -360,8 +373,20 @@ class TextBasedChannel { bulkDelete(messages, filterOld = false) { if (!isNaN(messages)) return this.fetchMessages({ limit: messages }).then(msgs => this.bulkDelete(msgs, filterOld)); if (messages instanceof Array || messages instanceof Collection) { - const messageIDs = messages instanceof Collection ? messages.keyArray() : messages.map(m => m.id); - return this.client.rest.methods.bulkDeleteMessages(this, messageIDs, filterOld); + let messageIDs = messages instanceof Collection ? messages.keyArray() : messages.map(m => m.id); + if (filterOld) { + messageIDs = messageIDs.filter(id => + Date.now() - Snowflake.deconstruct(id).date.getTime() < 1209600000 + ); + } + return this.rest.api.channels(this.id).messages['bulk-delete'] + .post({ data: { messages: messageIDs } }) + .then(() => + this.client.actions.MessageDeleteBulk.handle({ + channel_id: this.id, + ids: messageIDs, + }).messages + ); } throw new TypeError('The messages must be an Array, Collection, or number.'); } @@ -373,7 +398,12 @@ class TextBasedChannel { */ acknowledge() { if (!this.lastMessageID) return Promise.resolve(this); - return this.client.rest.methods.ackTextChannel(this); + return this.client.api.channels(this.id).messages(this.lastMessageID) + .post({ data: { token: this.client.rest._ackToken } }) + .then(res => { + if (res.token) this.client.rest._ackToken = res.token; + return this; + }); } _cacheMessage(message) { diff --git a/src/structures/shared/Search.js b/src/structures/shared/Search.js new file mode 100644 index 000000000..fc4b20c3c --- /dev/null +++ b/src/structures/shared/Search.js @@ -0,0 +1,63 @@ +const long = require('long'); + +module.exports = function search(target, options) { + if (typeof options === 'string') options = { content: options }; + if (options.before) { + if (!(options.before instanceof Date)) options.before = new Date(options.before); + options.maxID = long.fromNumber(options.before.getTime() - 14200704e5).shiftLeft(22).toString(); + } + if (options.after) { + if (!(options.after instanceof Date)) options.after = new Date(options.after); + options.minID = long.fromNumber(options.after.getTime() - 14200704e5).shiftLeft(22).toString(); + } + if (options.during) { + if (!(options.during instanceof Date)) options.during = new Date(options.during); + const t = options.during.getTime() - 14200704e5; + options.minID = long.fromNumber(t).shiftLeft(22).toString(); + options.maxID = long.fromNumber(t + 86400000).shiftLeft(22).toString(); + } + if (options.channel) options.channel = target.client.resolver.resolveChannelID(options.channel); + if (options.author) options.author = target.client.resolver.resolveUserID(options.author); + if (options.mentions) options.mentions = target.client.resolver.resolveUserID(options.options.mentions); + options = { + content: options.content, + max_id: options.maxID, + min_id: options.minID, + has: options.has, + channel_id: options.channel, + author_id: options.author, + author_type: options.authorType, + context_size: options.contextSize, + sort_by: options.sortBy, + sort_order: options.sortOrder, + limit: options.limit, + offset: options.offset, + mentions: options.mentions, + mentions_everyone: options.mentionsEveryone, + link_hostname: options.linkHostname, + embed_provider: options.embedProvider, + embed_type: options.embedType, + attachment_filename: options.attachmentFilename, + attachment_extension: options.attachmentExtension, + }; + + // Lazy load these because some of them use util + const Channel = require('../Channel'); + const Guild = require('../Guild'); + const Message = require('../Message'); + + if (!(target instanceof Channel || target instanceof Guild)) { + throw new TypeError('Target must be a TextChannel, DMChannel, GroupDMChannel, or Guild.'); + } + + let endpoint = target.client.api[target instanceof Channel ? 'channels' : 'guilds'](target.id).messages().search; + return endpoint.get({ query: options }).then(body => { + const messages = body.messages.map(x => + x.map(m => new Message(target.client.channels.get(m.channel_id), m, target.client)) + ); + return { + totalResults: body.total_results, + messages, + }; + }); +}; diff --git a/src/structures/shared/SendMessage.js b/src/structures/shared/SendMessage.js new file mode 100644 index 000000000..cfeef8ef9 --- /dev/null +++ b/src/structures/shared/SendMessage.js @@ -0,0 +1,60 @@ +const Util = require('../../util/Util'); + +module.exports = function sendMessage(channel, options) { + const User = require('../User'); + if (channel instanceof User) return channel.createDM().then(dm => dm.send(options)); + const GuildMember = require('../GuildMember'); + let { content, nonce, reply, code, disableEveryone, tts, embed, files, split } = options; + + if (typeof nonce !== 'undefined') { + nonce = parseInt(nonce); + if (isNaN(nonce) || nonce < 0) throw new RangeError('Message nonce must fit in an unsigned 64-bit integer.'); + } + + if (content) { + if (split && typeof split !== 'object') split = {}; + // Wrap everything in a code block + if (typeof code !== 'undefined' && (typeof code !== 'boolean' || code === true)) { + content = Util.escapeMarkdown(channel.client.resolver.resolveString(content), true); + content = `\`\`\`${typeof code !== 'boolean' ? code || '' : ''}\n${content}\n\`\`\``; + if (split) { + split.prepend = `\`\`\`${typeof code !== 'boolean' ? code || '' : ''}\n`; + split.append = '\n```'; + } + } + + // Add zero-width spaces to @everyone/@here + if (disableEveryone || (typeof disableEveryone === 'undefined' && channel.client.options.disableEveryone)) { + content = content.replace(/@(everyone|here)/g, '@\u200b$1'); + } + + if (split) content = Util.splitMessage(content, split); + } + + // Add the reply prefix + if (reply && !(channel instanceof User || channel instanceof GuildMember) && channel.type !== 'dm') { + const id = channel.client.resolver.resolveUserID(reply); + const mention = `<@${reply instanceof GuildMember && reply.nickname ? '!' : ''}${id}>`; + if (split) split.prepend = `${mention}, ${split.prepend || ''}`; + content = `${mention}${typeof content !== 'undefined' ? `, ${content}` : ''}`; + } + + if (content instanceof Array) { + return new Promise((resolve, reject) => { + const messages = []; + (function sendChunk() { + const opt = content.length ? { tts } : { tts, embed, files }; + channel.send(content.shift(), opt).then(message => { + messages.push(message); + if (content.length === 0) return resolve(messages); + return sendChunk(); + }).catch(reject); + }()); + }); + } + + return channel.client.api.channels(channel.id).messages.post({ + data: { content, tts, nonce, embed }, + files, + }).then(data => channel.client.actions.MessageCreate.handle(data).message); +}; diff --git a/src/structures/shared/index.js b/src/structures/shared/index.js new file mode 100644 index 000000000..67eed7f83 --- /dev/null +++ b/src/structures/shared/index.js @@ -0,0 +1,4 @@ +module.exports = { + search: require('./Search'), + sendMessage: require('./SendMessage'), +}; diff --git a/src/util/Constants.js b/src/util/Constants.js index 3678410ac..5e5c2c1ee 100644 --- a/src/util/Constants.js +++ b/src/util/Constants.js @@ -106,101 +106,7 @@ const AllowedImageSizes = [ 2048, ]; -const Endpoints = exports.Endpoints = { - User: userID => { - if (userID.id) userID = userID.id; - const base = `/users/${userID}`; - return { - toString: () => base, - channels: `${base}/channels`, - profile: `${base}/profile`, - relationships: `${base}/relationships`, - settings: `${base}/settings`, - Relationship: uID => `${base}/relationships/${uID}`, - Guild: guildID => `${base}/guilds/${guildID}`, - Note: id => `${base}/notes/${id}`, - Mentions: (limit, roles, everyone, guildID) => - `${base}/mentions?limit=${limit}&roles=${roles}&everyone=${everyone}${guildID ? `&guild_id=${guildID}` : ''}`, - Avatar: (root, hash, format, size) => { - if (userID === '1') return hash; - return Endpoints.CDN(root).Avatar(userID, hash, format, size); - }, - }; - }, - guilds: '/guilds', - Guild: guildID => { - if (guildID.id) guildID = guildID.id; - const base = `/guilds/${guildID}`; - return { - toString: () => base, - prune: `${base}/prune`, - embed: `${base}/embed`, - bans: `${base}/bans`, - integrations: `${base}/integrations`, - members: `${base}/members`, - channels: `${base}/channels`, - invites: `${base}/invites`, - roles: `${base}/roles`, - emojis: `${base}/emojis`, - search: `${base}/messages/search`, - voiceRegions: `${base}/regions`, - webhooks: `${base}/webhooks`, - ack: `${base}/ack`, - settings: `${base}/settings`, - auditLogs: `${base}/audit-logs`, - Emoji: emojiID => `${base}/emojis/${emojiID}`, - Icon: (root, hash, format, size) => Endpoints.CDN(root).Icon(guildID, hash, format, size), - Splash: (root, hash) => Endpoints.CDN(root).Splash(guildID, hash), - Role: roleID => `${base}/roles/${roleID}`, - Member: memberID => { - if (memberID.id) memberID = memberID.id; - const mbase = `${base}/members/${memberID}`; - return { - toString: () => mbase, - Role: roleID => `${mbase}/roles/${roleID}`, - nickname: `${base}/members/@me/nick`, - }; - }, - }; - }, - channels: '/channels', - Channel: channelID => { - if (channelID.id) channelID = channelID.id; - const base = `/channels/${channelID}`; - return { - toString: () => base, - messages: { - toString: () => `${base}/messages`, - bulkDelete: `${base}/messages/bulk-delete`, - }, - invites: `${base}/invites`, - typing: `${base}/typing`, - permissions: `${base}/permissions`, - webhooks: `${base}/webhooks`, - search: `${base}/messages/search`, - pins: `${base}/pins`, - Pin: messageID => `${base}/pins/${messageID}`, - Recipient: recipientID => `${base}/recipients/${recipientID}`, - Message: messageID => { - if (messageID.id) messageID = messageID.id; - const mbase = `${base}/messages/${messageID}`; - return { - toString: () => mbase, - reactions: `${mbase}/reactions`, - ack: `${mbase}/ack`, - Reaction: (emoji, limit) => { - const rbase = `${mbase}/reactions/${emoji}${limit ? `?limit=${limit}` : ''}`; - return { - toString: () => rbase, - User: userID => `${rbase}/${userID}`, - }; - }, - }; - }, - }; - }, - Message: m => exports.Endpoints.Channel(m.channel).Message(m), - Member: m => exports.Endpoints.Guild(m.guild).Member(m), +exports.Endpoints = { CDN(root) { return { Emoji: emojiID => `${root}/emojis/${emojiID}.png`, @@ -227,26 +133,7 @@ const Endpoints = exports.Endpoints = { Splash: (guildID, hash) => `${root}/splashes/${guildID}/${hash}.jpg`, }; }, - OAUTH2: { - Application: appID => { - const base = `/oauth2/applications/${appID}`; - return { - toString: () => base, - reset: `${base}/reset`, - }; - }, - App: appID => `/oauth2/authorize?client_id=${appID}`, - }, - login: '/auth/login', - logout: '/auth/logout', - voiceRegions: '/voice/regions', - gateway: { - toString: () => '/gateway', - bot: '/gateway/bot', - }, - Invite: inviteID => `/invite/${inviteID}?with_counts=true`, - inviteLink: id => `https://discord.gg/${id}`, - Webhook: (webhookID, token) => `/webhooks/${webhookID}${token ? `/${token}` : ''}`, + invite: code => `https://discord.gg/${code}`, }; diff --git a/src/util/Util.js b/src/util/Util.js index 9b7d536cd..2e844be01 100644 --- a/src/util/Util.js +++ b/src/util/Util.js @@ -30,7 +30,7 @@ class Util { } messages[msg] += (messages[msg].length > 0 && messages[msg] !== prepend ? char : '') + splitText[i]; } - return messages; + return messages.filter(m => m); } /** diff --git a/test/tester1000.js b/test/tester1000.js new file mode 100644 index 000000000..45ed165d9 --- /dev/null +++ b/test/tester1000.js @@ -0,0 +1,46 @@ +const Discord = require('../src'); +const { token, prefix, owner } = require('./auth.js'); + +// eslint-disable-next-line no-console +const log = (...args) => console.log(process.uptime().toFixed(3), ...args); + +const client = new Discord.Client(); + +client.on('debug', log); +client.on('ready', () => { + log('READY', client.user.tag, client.user.id); +}); + +const commands = { + eval: message => { + if (message.author.id !== owner) return; + let res; + try { + res = eval(message.content); + if (typeof res !== 'string') res = require('util').inspect(res); + } catch (err) { + // eslint-disable-next-line no-console + console.error(err.stack); + res = err.message; + } + message.channel.send(res, { code: 'js' }); + }, +}; + +client.on('message', message => { + if (!message.content.startsWith(prefix) || message.author.bot) return; + + message.content = message.content.replace(prefix, '').trim().split(' '); + const command = message.content.shift(); + message.content = message.content.join(' '); + + // eslint-disable-next-line no-console + console.log('COMMAND', command, message.content); + + if (command in commands) commands[command](message); +}); + +client.login(token); + +// eslint-disable-next-line no-console +process.on('unhandledRejection', console.error);