From 55ee566fb2fa9b121679710a103e821682d466ff Mon Sep 17 00:00:00 2001 From: Schuyler Cebulskie Date: Thu, 29 Dec 2016 23:51:56 -0500 Subject: [PATCH 01/12] Improve Travis builds --- .travis.yml | 2 +- deploy/deploy.sh | 41 ++++++++++++++--------------------------- 2 files changed, 15 insertions(+), 28 deletions(-) diff --git a/.travis.yml b/.travis.yml index 06b247f7f..bd496ce4a 100644 --- a/.travis.yml +++ b/.travis.yml @@ -6,7 +6,7 @@ cache: - node_modules install: npm install script: - - npm run test + - npm run lint - bash ./deploy/deploy.sh env: global: diff --git a/deploy/deploy.sh b/deploy/deploy.sh index dabad94b9..e26e86a7e 100644 --- a/deploy/deploy.sh +++ b/deploy/deploy.sh @@ -3,36 +3,27 @@ set -e -function build { - # Build docs - npm run docs - - # Build the webpack - VERSIONED=false npm run web-dist -} - -# Ignore Travis checking PRs +# Only run tests for PRs if [ "$TRAVIS_PULL_REQUEST" != "false" ]; then - echo "deploy.sh: Ignoring PR build" - build + echo "deploy.sh: This is a PR build - only running tests" + npm run test-docs + VERSIONED=false npm run web-dist exit 0 fi -# Ignore travis checking other branches irrelevant to users -if [ "$TRAVIS_BRANCH" == "gh-pages" -o "$TRAVIS_BRANCH" == "gh-pages-dev" -o "$TRAVIS_BRANCH" == "docs" -o "$TRAVIS_BRANCH" == "webpack" -o "$TRAVIS_BRANCH" == "v8" ]; then - echo "deploy.sh: Ignoring push to blacklisted branch" - build - exit 0 -fi - -SOURCE=$TRAVIS_BRANCH - -# Make sure tag pushes are handled +# Figure out the source of the build if [ -n "$TRAVIS_TAG" ]; then - echo "deploy.sh: This is a tag build, proceeding accordingly" + echo "deploy.sh: This is a tag build for $TRAVIS_TAG" SOURCE=$TRAVIS_TAG +else + echo "deploy.sh: This is a branch build for $TRAVIS_BRANCH" + SOURCE=$TRAVIS_BRANCH fi +# Build everything +npm run docs +VERSIONED=false npm run web-dist + # Initialise some useful variables REPO=`git config remote.origin.url` SSH_REPO=${REPO/https:\/\/github.com\//git@github.com:} @@ -48,15 +39,11 @@ chmod 600 deploy_key eval `ssh-agent -s` ssh-add deploy_key -# Build everything -build - # Checkout the repo in the target branch so we can build docs and push to it TARGET_BRANCH="docs" git clone $REPO out -b $TARGET_BRANCH -# Move the generated JSON file to the newly-checked-out repo, to be committed -# and pushed +# Move the generated JSON file to the newly-checked-out repo, to be committed and pushed mv docs/docs.json out/$SOURCE.json # Commit and push From ed08c247842a9eae65de09576097814b88ee2bf5 Mon Sep 17 00:00:00 2001 From: Schuyler Cebulskie Date: Fri, 30 Dec 2016 00:05:43 -0500 Subject: [PATCH 02/12] Add Node 7 testing and prettify output --- .travis.yml | 1 + deploy/deploy.sh | 27 +++++++++++++++++++-------- 2 files changed, 20 insertions(+), 8 deletions(-) diff --git a/.travis.yml b/.travis.yml index bd496ce4a..6bdf028d3 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,6 +1,7 @@ language: node_js node_js: - "6" + - "7" cache: directories: - node_modules diff --git a/deploy/deploy.sh b/deploy/deploy.sh index e26e86a7e..8212742a6 100644 --- a/deploy/deploy.sh +++ b/deploy/deploy.sh @@ -3,26 +3,37 @@ set -e -# Only run tests for PRs -if [ "$TRAVIS_PULL_REQUEST" != "false" ]; then - echo "deploy.sh: This is a PR build - only running tests" +function tests { npm run test-docs VERSIONED=false npm run web-dist exit 0 +} + +function build { + npm run docs + VERSIONED=false npm run web-dist +} + +# For PRs or Node 7, only run tests +if [ "$TRAVIS_PULL_REQUEST" != "false" ]; then + echo -e "\e[36m\e[4m\e[1mThis is a PR build - only running tests" + tests +fi +if [ "$TRAVIS_NODE_VERSION" != "6" ]; then + echo -e "\e[36m\e[4m\e[1mThis is a Node v$TRAVIS_NODE_VERSION build - only running tests" + tests fi # Figure out the source of the build if [ -n "$TRAVIS_TAG" ]; then - echo "deploy.sh: This is a tag build for $TRAVIS_TAG" + echo -e "\e[36m\e[4m\e[1mThis is a tag build for $TRAVIS_TAG" SOURCE=$TRAVIS_TAG else - echo "deploy.sh: This is a branch build for $TRAVIS_BRANCH" + echo -e "\e[36m\e[4m\e[1mThis is a branch build for $TRAVIS_BRANCH" SOURCE=$TRAVIS_BRANCH fi -# Build everything -npm run docs -VERSIONED=false npm run web-dist +build # Initialise some useful variables REPO=`git config remote.origin.url` From e44d57167163217aa16343f7a2073404e2682308 Mon Sep 17 00:00:00 2001 From: Schuyler Cebulskie Date: Fri, 30 Dec 2016 00:06:24 -0500 Subject: [PATCH 03/12] Add PR number output to Travis build --- deploy/deploy.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/deploy/deploy.sh b/deploy/deploy.sh index 8212742a6..9e89bb482 100644 --- a/deploy/deploy.sh +++ b/deploy/deploy.sh @@ -16,7 +16,7 @@ function build { # For PRs or Node 7, only run tests if [ "$TRAVIS_PULL_REQUEST" != "false" ]; then - echo -e "\e[36m\e[4m\e[1mThis is a PR build - only running tests" + echo -e "\e[36m\e[4m\e[1mThis is a PR build for #$TRAVIS_PULL_REQUEST - only running tests" tests fi if [ "$TRAVIS_NODE_VERSION" != "6" ]; then From bcbd1872238a614fd132fef238230c33a95728d8 Mon Sep 17 00:00:00 2001 From: Schuyler Cebulskie Date: Fri, 30 Dec 2016 00:07:24 -0500 Subject: [PATCH 04/12] Fix test-docs script --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 5c4895a66..eddbfdcd1 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,7 @@ "scripts": { "test": "eslint src && docgen --source src --custom docs/index.yml", "docs": "docgen --source src --custom docs/index.yml --output docs/docs.json", - "test-docs": "docgen --source src --custom docs", + "test-docs": "docgen --source src --custom docs/index.yml", "lint": "eslint src", "web-dist": "node ./node_modules/parallel-webpack/bin/run.js" }, From 5fd10ef63e012461cc939be38d9faf19bd1c0418 Mon Sep 17 00:00:00 2001 From: Schuyler Cebulskie Date: Fri, 30 Dec 2016 00:53:53 -0500 Subject: [PATCH 05/12] Tweak Travis output --- deploy/deploy.sh | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/deploy/deploy.sh b/deploy/deploy.sh index 9e89bb482..146a2ba17 100644 --- a/deploy/deploy.sh +++ b/deploy/deploy.sh @@ -14,25 +14,27 @@ function build { VERSIONED=false npm run web-dist } -# For PRs or Node 7, only run tests +# Only run tests for PRs if [ "$TRAVIS_PULL_REQUEST" != "false" ]; then - echo -e "\e[36m\e[4m\e[1mThis is a PR build for #$TRAVIS_PULL_REQUEST - only running tests" - tests -fi -if [ "$TRAVIS_NODE_VERSION" != "6" ]; then - echo -e "\e[36m\e[4m\e[1mThis is a Node v$TRAVIS_NODE_VERSION build - only running tests" + echo -e "\e[36m\e[1mThis is a PR build for #$TRAVIS_PULL_REQUEST - only running tests" tests fi # Figure out the source of the build if [ -n "$TRAVIS_TAG" ]; then - echo -e "\e[36m\e[4m\e[1mThis is a tag build for $TRAVIS_TAG" + echo -e "\e[36m\e[1mThis is a tag build for $TRAVIS_TAG" SOURCE=$TRAVIS_TAG else - echo -e "\e[36m\e[4m\e[1mThis is a branch build for $TRAVIS_BRANCH" + echo -e "\e[36m\e[1mThis is a branch build for $TRAVIS_BRANCH" SOURCE=$TRAVIS_BRANCH fi +# Only run tests for Node versions other than 6 +if [ "$TRAVIS_NODE_VERSION" != "6" ]; then + echo -e "\e[36m\e[1mThis is a Node v$TRAVIS_NODE_VERSION build - only running tests" + tests +fi + build # Initialise some useful variables From 1afb21c98152a2116364f5e994e00f96365daa2d Mon Sep 17 00:00:00 2001 From: Schuyler Cebulskie Date: Fri, 30 Dec 2016 01:01:16 -0500 Subject: [PATCH 06/12] Tweak output some more --- deploy/deploy.sh | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/deploy/deploy.sh b/deploy/deploy.sh index 146a2ba17..13811122b 100644 --- a/deploy/deploy.sh +++ b/deploy/deploy.sh @@ -16,22 +16,22 @@ function build { # Only run tests for PRs if [ "$TRAVIS_PULL_REQUEST" != "false" ]; then - echo -e "\e[36m\e[1mThis is a PR build for #$TRAVIS_PULL_REQUEST - only running tests" + echo -e "\e[36m\e[1mBuild triggered for PR #$TRAVIS_PULL_REQUEST to branch $TRAVIS_BRANCH - only running tests." tests fi # Figure out the source of the build if [ -n "$TRAVIS_TAG" ]; then - echo -e "\e[36m\e[1mThis is a tag build for $TRAVIS_TAG" + echo -e "\e[36m\e[1mBuild triggered for tag \"$TRAVIS_TAG\"." SOURCE=$TRAVIS_TAG else - echo -e "\e[36m\e[1mThis is a branch build for $TRAVIS_BRANCH" + echo -e "\e[36m\e[1mBuild triggered for branch \"$TRAVIS_BRANCH\"." SOURCE=$TRAVIS_BRANCH fi # Only run tests for Node versions other than 6 if [ "$TRAVIS_NODE_VERSION" != "6" ]; then - echo -e "\e[36m\e[1mThis is a Node v$TRAVIS_NODE_VERSION build - only running tests" + echo -e "\e[36m\e[1mBuild triggered with Node v$TRAVIS_NODE_VERSION - only running tests." tests fi From f0adf8f1223101b537548e5b60e70d85a7b9327f Mon Sep 17 00:00:00 2001 From: Schuyler Cebulskie Date: Fri, 30 Dec 2016 01:46:56 -0500 Subject: [PATCH 07/12] Fix User#defaultAvatarURL in Node 6 --- src/structures/User.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/structures/User.js b/src/structures/User.js index f7148289a..fd0a8d305 100644 --- a/src/structures/User.js +++ b/src/structures/User.js @@ -111,9 +111,9 @@ class User { * @readonly */ get defaultAvatarURL() { - let defaultAvatars = Object.values(Constants.DefaultAvatars); - let defaultAvatar = this.discriminator % defaultAvatars.length; - return Constants.Endpoints.assets(`${defaultAvatars[defaultAvatar]}.png`); + const avatars = Object.keys(Constants.DefaultAvatars); + const avatar = avatars[this.discriminator % avatars.length]; + return Constants.Endpoints.assets(`${Constants.DefaultAvatars[avatar]}.png`); } /** From beffb390e67a65a0f25c297b2bfe22f33ec20f30 Mon Sep 17 00:00:00 2001 From: Gus Caplan Date: Fri, 30 Dec 2016 01:44:19 -0600 Subject: [PATCH 08/12] Add search (#1043) * add search * Update ClientDataResolver.js --- package.json | 1 + src/client/ClientDataResolver.js | 13 ++ src/client/rest/RESTMethods.js | 12 ++ src/structures/Guild.js | 20 ++ src/structures/Message.js | 6 + src/structures/MessageSearch.js | 215 +++++++++++++++++++ src/structures/interface/TextBasedChannel.js | 21 ++ src/util/Constants.js | 2 + 8 files changed, 290 insertions(+) create mode 100644 src/structures/MessageSearch.js diff --git a/package.json b/package.json index eddbfdcd1..a26af5637 100644 --- a/package.json +++ b/package.json @@ -32,6 +32,7 @@ "runkitExampleFilename": "./docs/examples/ping.js", "dependencies": { "@types/node": "^6.0.0", + "long": "^3.2.0", "pako": "^1.0.0", "superagent": "^3.3.0", "tweetnacl": "^0.14.0", diff --git a/src/client/ClientDataResolver.js b/src/client/ClientDataResolver.js index d38fb7c95..c17d4944e 100644 --- a/src/client/ClientDataResolver.js +++ b/src/client/ClientDataResolver.js @@ -123,6 +123,19 @@ class ClientDataResolver { return null; } + /** + * Resolves a ChannelResolvable to a Channel object + * @param {ChannelResolvable} channel The channel resolvable to resolve + * @returns {?string} + */ + resolveChannelID(channel) { + if (channel instanceof Channel) return channel.id; + if (typeof channel === 'string') return channel; + if (channel instanceof Message) return channel.channel.id; + if (channel instanceof Guild) return channel.defaultChannel.id; + return null; + } + /** * Data that can be resolved to give an invite code. This can be: * * An invite code diff --git a/src/client/rest/RESTMethods.js b/src/client/rest/RESTMethods.js index 70062482e..0484aec1d 100644 --- a/src/client/rest/RESTMethods.js +++ b/src/client/rest/RESTMethods.js @@ -117,6 +117,18 @@ class RESTMethods { ); } + search(type, id, options) { + const queryString = Object.keys(options) + .filter(k => options[k]) + .map(k => [k, options[k]]) + .map(x => x.join('=')) + .join('&'); + const url = `${Constants.Endpoints[`${type}Search`](id)}?${queryString}`; + return this.rest.makeRequest('get', url, true).then(body => + body.messages.map(x => x.map(m => new Message(this.client.channels.get(m.channel_id), m, this.client))) + ); + } + createChannel(guild, channelName, channelType, overwrites) { if (overwrites instanceof Collection) overwrites = overwrites.array(); return this.rest.makeRequest('post', Constants.Endpoints.guildChannels(guild.id), true, { diff --git a/src/structures/Guild.js b/src/structures/Guild.js index 368269509..8a45507eb 100644 --- a/src/structures/Guild.js +++ b/src/structures/Guild.js @@ -3,6 +3,7 @@ const Role = require('./Role'); const Emoji = require('./Emoji'); const Presence = require('./Presence').Presence; const GuildMember = require('./GuildMember'); +const MessageSearch = require('./MessageSearch'); const Constants = require('../util/Constants'); const Collection = require('../util/Collection'); const cloneObject = require('../util/CloneObject'); @@ -703,6 +704,25 @@ class Guild { return this.client.rest.methods.setRolePositions(this.id, updatedRoles); } + /** + * Performs a search + * @param {MessageSearchOptions} [options={}] Options to pass to the search + * @returns {MessageSearch} + * @example + * guild.search() + * .content('discord.js') + * .before('2016-11-17') + * .execute() + * .then(res => { + * const hit = res[0].find(m => m.hit).content; + * console.log(`I found: **${hit}**`); + * }) + * .catch(console.error); + */ + search(options) { + return new MessageSearch(this, options); + } + /** * Whether this Guild equals another Guild. It compares all properties, so for most operations * it is advisable to just compare `guild.id === guild2.id` as it is much faster and is often diff --git a/src/structures/Message.js b/src/structures/Message.js index 7fcc5b4b8..85a17c549 100644 --- a/src/structures/Message.js +++ b/src/structures/Message.js @@ -172,6 +172,12 @@ class Message { * @type {?string} */ this.webhookID = data.webhook_id || null; + + /** + * Whether this message is a hit in a search + * @type {?boolean} + */ + this.hit = typeof data.hit === 'boolean' ? data.hit : null; } patch(data) { // eslint-disable-line complexity diff --git a/src/structures/MessageSearch.js b/src/structures/MessageSearch.js new file mode 100644 index 000000000..50135f7b4 --- /dev/null +++ b/src/structures/MessageSearch.js @@ -0,0 +1,215 @@ +const long = require('long'); +let TextChannel, DMChannel, GroupDMChannel, Guild; + +/** + * @typedef {Object} MessageSearchOptions + * @property {string} [content] Message content + * @property {string} [maxID] Maximum ID for the filter + * @property {string} [minID] Minimum ID for the filter + * @property {string} [has] One of `link`, `embed`, `file`, `video`, `image`, or `sound` + * @property {string} [channelID] Channel ID to limit search to (only for guild search endpoint) + * @property {string} [authorID] Author ID to limit search + * @property {string} [sortBy='recent'] `recent` or `relevant` + * @property {number} [contextSize=2] How many messages to get around the matched message (0 to 2) + * @property {number} [limit=25] Maximum number of results to get (1 to 25) + */ + +/** + * Fluent interface for running a search against a guild or channel + */ +class MessageSearch { + /** + * @param {TextChannel|DMChannel|GroupDMChannel|Guild} target Target of the search + * @param {MessageSearchOptions} [options] Options for the search + */ + constructor(target, options = {}) { + if (!TextChannel) { + TextChannel = require('./TextChannel'); + DMChannel = require('./DMChannel'); + GroupDMChannel = require('./GroupDMChannel'); + Guild = require('./Guild'); + } + + if (target instanceof TextChannel || target instanceof DMChannel || target instanceof GroupDMChannel) { + /** + * The type of search, either `channel` or `guild` + * @type {string} + */ + this.type = 'channel'; + } else if (target instanceof Guild) { + this.type = 'guild'; + } else { + throw new TypeError('Target must be a TextChannel, DMChannel, GroupDMChannel, or Guild.'); + } + + /** + * Client to use + * @type {Client} + */ + this.client = target.client; + + /** + * ID of the search target + * @type {string} + */ + this.id = target.id; + + /** + * Options for the search + * @type {MessageSearchOptions} + */ + this.options = options; + } + + /** + * Sets the content for the search + * @param {string} content Content to search for + * @returns {MessageSearch} + */ + content(content) { + this.options.content = content; + return this; + } + + /** + * Sets the minimum ID for the search + * @param {string} id Snowflake minimum ID + * @returns {MessageSearch} + */ + minID(id) { + this.options.minID = id; + return this; + } + + /** + * Sets the maximum ID for the search + * @param {string} id Snowflake maximum ID + * @returns {MessageSearch} + */ + maxID(id) { + this.options.maxID = id; + return this; + } + + /** + * Sets the before date for the search + * @param {Date} date Date to find messages before + * @returns {MessageSearch} + */ + before(date) { + if (typeof date !== Date) date = new Date(date); + return this.maxID(long.fromNumber(date.getTime() - 14200704e5).shiftLeft(22).toString()); + } + + /** + * Sets the after date for the search + * @param {Date} date Date to find messages after + * @returns {MessageSearch} + */ + after(date) { + if (typeof date !== Date) date = new Date(date); + return this.minID(long.fromNumber(date.getTime() - 14200704e5).shiftLeft(22).toString()); + } + + /** + * Sets the during date for the search + * @param {Date} date Date to find messages during (range of date to date + 24 hours) + * @returns {MessageSearch} + */ + during(date) { + if (typeof date !== Date) date = new Date(date); + const t = date.getTime() - 14200704e5; + this.minID(long.fromNumber(t).shiftLeft(22).toString()); + this.maxID(long.fromNumber(t + 86400000).shift(222).toString()); + return this; + } + + /** + * Sets the filter for the search + * @param {string} type Filter for some type of embed or attachment that can be in the message + * must be one of ['link', 'embed', 'file', 'video', 'image', 'sound'] + * @returns {MessageSearch} + */ + has(type) { + const allowed = ['link', 'embed', 'file', 'video', 'image', 'sound']; + if (!allowed.includes(type)) throw new Error(`Type must be one of [${allowed.join(', ')}]`); + this.options.has = type; + return this; + } + + /** + * Sets the author for the search + * @param {UserResolvable} user User to only find messages from + * @returns {MessageSearch} + */ + from(user) { + this.options.authorID = this.client.resolver.resolverUserID(user); + return this; + } + + /** + * Sets the channel for the search + * @param {ChannelResolvable} channel Channel to only find messages from + * This is only for use with a guild search + * @returns {MessageSearch} + */ + in(channel) { + this.options.channelID = this.client.resolver.resolveChannelID(channel); + return this; + } + + /** + * Sets the maximum results for the search + * @param {number} limit Maximum number of results (1 to 25) + * @returns {MessageSearch} + */ + limit(limit) { + if (limit < 1 || limit > 25) throw new RangeError('Limit must be within 1 to 25.'); + this.options.limit = limit; + return this; + } + + /** + * Sets the context size for the search + * @param {number} size Number of messages to get around the matched message (0 to 2) + * @returns {MessageSearch} + */ + contextSize(size) { + if (size < 0 || size > 2) throw new RangeError('Context size must be within 0 to 2'); + this.options.contextSize = size; + return this; + } + + /** + * Sets the sorting order for the search + * @param {string} [type='recent'] Sorting type (`recent` or `relevant`) + * @returns {MessageSearch} + */ + sort(type) { + if (type !== 'recent' || type !== 'relevant') throw new Error('Sort type must be `recent` or `relevant`.'); + this.options.sortBy = type; + return this; + } + + /** + * Executes the search + * @returns {Promise>} + * An array containing arrays of messages. Each inner array is a search context cluster. + * The message which has triggered the result will have the `hit` property set to `true`. + */ + execute() { + return this.client.rest.methods.search(this.type, this.id, { + content: this.options.content, + max_id: this.options.maxID, + min_id: this.options.minID, + has: this.options.has, + channel_id: this.options.channelID, + author_id: this.options.authorID, + context_size: this.options.contextSize, + sort_by: this.options.sortBy, + limit: this.options.limit, + }); + } +} + +module.exports = MessageSearch; diff --git a/src/structures/interface/TextBasedChannel.js b/src/structures/interface/TextBasedChannel.js index 353c0a9cf..08ba4f388 100644 --- a/src/structures/interface/TextBasedChannel.js +++ b/src/structures/interface/TextBasedChannel.js @@ -1,6 +1,7 @@ const path = require('path'); const Message = require('../Message'); const MessageCollector = require('../MessageCollector'); +const MessageSearch = require('../MessageSearch'); const Collection = require('../../util/Collection'); @@ -214,6 +215,25 @@ class TextBasedChannel { }); } + /** + * Performs a search + * @param {MessageSearchOptions} [options={}] Options to pass to the search + * @returns {MessageSearch} + * @example + * channel.search() + * .content('discord.js') + * .before('2016-11-17') + * .execute() + * .then(res => { + * const hit = res[0].find(m => m.hit).content; + * console.log(`I found: **${hit}**`); + * }) + * .catch(console.error); + */ + search(options) { + return new MessageSearch(this, options); + } + /** * Starts a typing indicator in the channel. * @param {number} [count] The number of times startTyping should be considered to have been called @@ -361,6 +381,7 @@ exports.applyToClass = (structure, full = false) => { '_cacheMessage', 'fetchMessages', 'fetchMessage', + 'search', 'bulkDelete', 'startTyping', 'stopTyping', diff --git a/src/util/Constants.js b/src/util/Constants.js index cee2192f1..dc80da5aa 100644 --- a/src/util/Constants.js +++ b/src/util/Constants.js @@ -122,6 +122,7 @@ const Endpoints = exports.Endpoints = { guildMemberNickname: (guildID) => `${Endpoints.guildMember(guildID, '@me')}/nick`, guildChannels: (guildID) => `${Endpoints.guild(guildID)}/channels`, guildEmojis: (guildID) => `${Endpoints.guild(guildID)}/emojis`, + guildSearch: (guildID) => `${Endpoints.guild(guildID)}/messages/search`, // channels channels: `${API}/channels`, @@ -132,6 +133,7 @@ const Endpoints = exports.Endpoints = { channelPermissions: (channelID) => `${Endpoints.channel(channelID)}/permissions`, channelMessage: (channelID, messageID) => `${Endpoints.channelMessages(channelID)}/${messageID}`, channelWebhooks: (channelID) => `${Endpoints.channel(channelID)}/webhooks`, + channelSearch: (channelID) => `${Endpoints.channelMessages(channelID)}/search`, // message reactions messageReactions: (channelID, messageID) => `${Endpoints.channelMessage(channelID, messageID)}/reactions`, From 258e4b90859dc8daff12457d25ba09892d3c37ff Mon Sep 17 00:00:00 2001 From: Gus Caplan Date: Fri, 30 Dec 2016 11:00:16 -0600 Subject: [PATCH 09/12] fix setting avatar to buffer (#1048) --- src/structures/ClientUser.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/structures/ClientUser.js b/src/structures/ClientUser.js index 35ec22bde..6a8532141 100644 --- a/src/structures/ClientUser.js +++ b/src/structures/ClientUser.js @@ -116,7 +116,7 @@ class ClientUser extends User { * .catch(console.error); */ setAvatar(avatar) { - if (avatar.startsWith('data:')) { + if (typeof avatar === 'string' && avatar.startsWith('data:')) { return this.client.rest.methods.updateCurrentUser({ avatar }); } else { return this.client.resolver.resolveBuffer(avatar).then(data => From da32c2ec3df709032b83b37e413f4a6a8e7cb0e6 Mon Sep 17 00:00:00 2001 From: Gus Caplan Date: Fri, 30 Dec 2016 11:14:31 -0600 Subject: [PATCH 10/12] add more search stuff (#1046) * add more search stuff * clean up the options * fix link hostname * use some resolvers * fix type --- src/client/rest/RESTMethods.js | 89 +++++++- src/structures/Guild.js | 25 +-- src/structures/MessageSearch.js | 215 ------------------- src/structures/interface/TextBasedChannel.js | 26 +-- 4 files changed, 113 insertions(+), 242 deletions(-) delete mode 100644 src/structures/MessageSearch.js diff --git a/src/client/rest/RESTMethods.js b/src/client/rest/RESTMethods.js index 0484aec1d..ff94719c5 100644 --- a/src/client/rest/RESTMethods.js +++ b/src/client/rest/RESTMethods.js @@ -1,3 +1,4 @@ +const long = require('long'); const Constants = require('../../util/Constants'); const Collection = require('../../util/Collection'); const splitMessage = require('../../util/SplitMessage'); @@ -12,6 +13,35 @@ const Invite = require('../../structures/Invite'); const Webhook = require('../../structures/Webhook'); const UserProfile = require('../../structures/UserProfile'); const ClientOAuth2Application = require('../../structures/ClientOAuth2Application'); +const Channel = require('../../structures/Channel'); +const Guild = require('../../structures/Guild'); + +/** + * @typedef {Object} MessageSearchOptions + * @property {string} [content] Message content + * @property {string} [maxID] Maximum ID for the filter + * @property {string} [minID] Minimum ID for the filter + * @property {string} [has] One of `link`, `embed`, `file`, `video`, `image`, or `sound`, + * or add `-` to negate (e.g. `-file`) + * @property {ChannelResolvable} [channel] Channel to limit search to (only for guild search endpoint) + * @property {UserResolvable} [author] Author to limit search + * @property {string} [authorType] One of `user`, `bot`, `webhook`, or add `-` to negate (e.g. `-webhook`) + * @property {string} [sortBy='recent'] `recent` or `relevant` + * @property {string} [sortOrder='desc'] `asc` or `desc` + * @property {number} [contextSize=2] How many messages to get around the matched message (0 to 2) + * @property {number} [limit=25] Maximum number of results to get (1 to 25) + * @property {number} [offset=0] Offset the "pages" of results (since you can only see 25 at a time) + * @property {UserResolvable} [mentions] Mentioned user filter + * @property {boolean} [mentionsEveryone] If everyone is mentioned + * @property {string} [linkHostname] Filter links by hostname + * @property {string} [embedProvider] The name of an embed provider + * @property {string} [embedType] one of `image`, `video`, `url`, `rich` + * @property {string} [attachmentFilename] The name of an attachment + * @property {string} [attachmentExtention] The extension of an attachment + * @property {Date} [before] Date to find messages before + * @property {Date} [after] Date to find messages before + * @property {Date} [during] Date to find messages during (range of date to date + 24 hours) + */ class RESTMethods { constructor(restManager) { @@ -117,13 +147,68 @@ class RESTMethods { ); } - search(type, id, options) { + search(target, 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).shift(222).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, + }; + const queryString = Object.keys(options) .filter(k => options[k]) .map(k => [k, options[k]]) .map(x => x.join('=')) .join('&'); - const url = `${Constants.Endpoints[`${type}Search`](id)}?${queryString}`; + + let type; + if (target instanceof Channel) { + type = 'channel'; + } else if (target instanceof Guild) { + type = 'guild'; + } else { + throw new TypeError('Target must be a TextChannel, DMChannel, GroupDMChannel, or Guild.'); + } + + const url = `${Constants.Endpoints[`${type}Search`](target.id)}?${queryString}`; return this.rest.makeRequest('get', url, true).then(body => body.messages.map(x => x.map(m => new Message(this.client.channels.get(m.channel_id), m, this.client))) ); diff --git a/src/structures/Guild.js b/src/structures/Guild.js index 8a45507eb..472166fbc 100644 --- a/src/structures/Guild.js +++ b/src/structures/Guild.js @@ -3,7 +3,6 @@ const Role = require('./Role'); const Emoji = require('./Emoji'); const Presence = require('./Presence').Presence; const GuildMember = require('./GuildMember'); -const MessageSearch = require('./MessageSearch'); const Constants = require('../util/Constants'); const Collection = require('../util/Collection'); const cloneObject = require('../util/CloneObject'); @@ -707,20 +706,22 @@ class Guild { /** * Performs a search * @param {MessageSearchOptions} [options={}] Options to pass to the search - * @returns {MessageSearch} + * @returns {Promise>} + * An array containing arrays of messages. Each inner array is a search context cluster. + * The message which has triggered the result will have the `hit` property set to `true`. * @example - * guild.search() - * .content('discord.js') - * .before('2016-11-17') - * .execute() - * .then(res => { - * const hit = res[0].find(m => m.hit).content; - * console.log(`I found: **${hit}**`); - * }) - * .catch(console.error); + * guild.search({ + * content: 'discord.js', + * before: '2016-11-17' + * }) + * .then(res => { + * const hit = res[0].find(m => m.hit).content; + * console.log(`I found: **${hit}**`); + * }) + * .catch(console.error); */ search(options) { - return new MessageSearch(this, options); + return this.client.rest.methods.search(this, options); } /** diff --git a/src/structures/MessageSearch.js b/src/structures/MessageSearch.js deleted file mode 100644 index 50135f7b4..000000000 --- a/src/structures/MessageSearch.js +++ /dev/null @@ -1,215 +0,0 @@ -const long = require('long'); -let TextChannel, DMChannel, GroupDMChannel, Guild; - -/** - * @typedef {Object} MessageSearchOptions - * @property {string} [content] Message content - * @property {string} [maxID] Maximum ID for the filter - * @property {string} [minID] Minimum ID for the filter - * @property {string} [has] One of `link`, `embed`, `file`, `video`, `image`, or `sound` - * @property {string} [channelID] Channel ID to limit search to (only for guild search endpoint) - * @property {string} [authorID] Author ID to limit search - * @property {string} [sortBy='recent'] `recent` or `relevant` - * @property {number} [contextSize=2] How many messages to get around the matched message (0 to 2) - * @property {number} [limit=25] Maximum number of results to get (1 to 25) - */ - -/** - * Fluent interface for running a search against a guild or channel - */ -class MessageSearch { - /** - * @param {TextChannel|DMChannel|GroupDMChannel|Guild} target Target of the search - * @param {MessageSearchOptions} [options] Options for the search - */ - constructor(target, options = {}) { - if (!TextChannel) { - TextChannel = require('./TextChannel'); - DMChannel = require('./DMChannel'); - GroupDMChannel = require('./GroupDMChannel'); - Guild = require('./Guild'); - } - - if (target instanceof TextChannel || target instanceof DMChannel || target instanceof GroupDMChannel) { - /** - * The type of search, either `channel` or `guild` - * @type {string} - */ - this.type = 'channel'; - } else if (target instanceof Guild) { - this.type = 'guild'; - } else { - throw new TypeError('Target must be a TextChannel, DMChannel, GroupDMChannel, or Guild.'); - } - - /** - * Client to use - * @type {Client} - */ - this.client = target.client; - - /** - * ID of the search target - * @type {string} - */ - this.id = target.id; - - /** - * Options for the search - * @type {MessageSearchOptions} - */ - this.options = options; - } - - /** - * Sets the content for the search - * @param {string} content Content to search for - * @returns {MessageSearch} - */ - content(content) { - this.options.content = content; - return this; - } - - /** - * Sets the minimum ID for the search - * @param {string} id Snowflake minimum ID - * @returns {MessageSearch} - */ - minID(id) { - this.options.minID = id; - return this; - } - - /** - * Sets the maximum ID for the search - * @param {string} id Snowflake maximum ID - * @returns {MessageSearch} - */ - maxID(id) { - this.options.maxID = id; - return this; - } - - /** - * Sets the before date for the search - * @param {Date} date Date to find messages before - * @returns {MessageSearch} - */ - before(date) { - if (typeof date !== Date) date = new Date(date); - return this.maxID(long.fromNumber(date.getTime() - 14200704e5).shiftLeft(22).toString()); - } - - /** - * Sets the after date for the search - * @param {Date} date Date to find messages after - * @returns {MessageSearch} - */ - after(date) { - if (typeof date !== Date) date = new Date(date); - return this.minID(long.fromNumber(date.getTime() - 14200704e5).shiftLeft(22).toString()); - } - - /** - * Sets the during date for the search - * @param {Date} date Date to find messages during (range of date to date + 24 hours) - * @returns {MessageSearch} - */ - during(date) { - if (typeof date !== Date) date = new Date(date); - const t = date.getTime() - 14200704e5; - this.minID(long.fromNumber(t).shiftLeft(22).toString()); - this.maxID(long.fromNumber(t + 86400000).shift(222).toString()); - return this; - } - - /** - * Sets the filter for the search - * @param {string} type Filter for some type of embed or attachment that can be in the message - * must be one of ['link', 'embed', 'file', 'video', 'image', 'sound'] - * @returns {MessageSearch} - */ - has(type) { - const allowed = ['link', 'embed', 'file', 'video', 'image', 'sound']; - if (!allowed.includes(type)) throw new Error(`Type must be one of [${allowed.join(', ')}]`); - this.options.has = type; - return this; - } - - /** - * Sets the author for the search - * @param {UserResolvable} user User to only find messages from - * @returns {MessageSearch} - */ - from(user) { - this.options.authorID = this.client.resolver.resolverUserID(user); - return this; - } - - /** - * Sets the channel for the search - * @param {ChannelResolvable} channel Channel to only find messages from - * This is only for use with a guild search - * @returns {MessageSearch} - */ - in(channel) { - this.options.channelID = this.client.resolver.resolveChannelID(channel); - return this; - } - - /** - * Sets the maximum results for the search - * @param {number} limit Maximum number of results (1 to 25) - * @returns {MessageSearch} - */ - limit(limit) { - if (limit < 1 || limit > 25) throw new RangeError('Limit must be within 1 to 25.'); - this.options.limit = limit; - return this; - } - - /** - * Sets the context size for the search - * @param {number} size Number of messages to get around the matched message (0 to 2) - * @returns {MessageSearch} - */ - contextSize(size) { - if (size < 0 || size > 2) throw new RangeError('Context size must be within 0 to 2'); - this.options.contextSize = size; - return this; - } - - /** - * Sets the sorting order for the search - * @param {string} [type='recent'] Sorting type (`recent` or `relevant`) - * @returns {MessageSearch} - */ - sort(type) { - if (type !== 'recent' || type !== 'relevant') throw new Error('Sort type must be `recent` or `relevant`.'); - this.options.sortBy = type; - return this; - } - - /** - * Executes the search - * @returns {Promise>} - * An array containing arrays of messages. Each inner array is a search context cluster. - * The message which has triggered the result will have the `hit` property set to `true`. - */ - execute() { - return this.client.rest.methods.search(this.type, this.id, { - content: this.options.content, - max_id: this.options.maxID, - min_id: this.options.minID, - has: this.options.has, - channel_id: this.options.channelID, - author_id: this.options.authorID, - context_size: this.options.contextSize, - sort_by: this.options.sortBy, - limit: this.options.limit, - }); - } -} - -module.exports = MessageSearch; diff --git a/src/structures/interface/TextBasedChannel.js b/src/structures/interface/TextBasedChannel.js index 08ba4f388..1249b57dc 100644 --- a/src/structures/interface/TextBasedChannel.js +++ b/src/structures/interface/TextBasedChannel.js @@ -1,10 +1,8 @@ const path = require('path'); const Message = require('../Message'); const MessageCollector = require('../MessageCollector'); -const MessageSearch = require('../MessageSearch'); const Collection = require('../../util/Collection'); - /** * Interface for classes that have text-channel-like features * @interface @@ -218,20 +216,22 @@ class TextBasedChannel { /** * Performs a search * @param {MessageSearchOptions} [options={}] Options to pass to the search - * @returns {MessageSearch} + * @returns {Promise>} + * An array containing arrays of messages. Each inner array is a search context cluster. + * The message which has triggered the result will have the `hit` property set to `true`. * @example - * channel.search() - * .content('discord.js') - * .before('2016-11-17') - * .execute() - * .then(res => { - * const hit = res[0].find(m => m.hit).content; - * console.log(`I found: **${hit}**`); - * }) - * .catch(console.error); + * channel.search({ + * content: 'discord.js', + * before: '2016-11-17' + * }) + * .then(res => { + * const hit = res[0].find(m => m.hit).content; + * console.log(`I found: **${hit}**`); + * }) + * .catch(console.error); */ search(options) { - return new MessageSearch(this, options); + return this.client.rest.methods.search(this, options); } /** From bde6749d65c3b3d094acd3e595b0e843142f4f31 Mon Sep 17 00:00:00 2001 From: Gus Caplan Date: Fri, 30 Dec 2016 11:21:03 -0600 Subject: [PATCH 11/12] clean up search (#1049) * add more search stuff * clean up the options * fix link hostname * use some resolvers * fix type * move the trasform to a seperate file * pass this param * move typedef --- src/client/rest/RESTMethods.js | 73 +---------------------------- src/util/TransformSearchOptions.js | 75 ++++++++++++++++++++++++++++++ 2 files changed, 77 insertions(+), 71 deletions(-) create mode 100644 src/util/TransformSearchOptions.js diff --git a/src/client/rest/RESTMethods.js b/src/client/rest/RESTMethods.js index ff94719c5..5032957de 100644 --- a/src/client/rest/RESTMethods.js +++ b/src/client/rest/RESTMethods.js @@ -4,6 +4,7 @@ const Collection = require('../../util/Collection'); const splitMessage = require('../../util/SplitMessage'); const parseEmoji = require('../../util/ParseEmoji'); const escapeMarkdown = require('../../util/EscapeMarkdown'); +const transformSearchOptions = require('../../util/TransformSearchOptions'); const User = require('../../structures/User'); const GuildMember = require('../../structures/GuildMember'); @@ -16,33 +17,6 @@ const ClientOAuth2Application = require('../../structures/ClientOAuth2Applicatio const Channel = require('../../structures/Channel'); const Guild = require('../../structures/Guild'); -/** - * @typedef {Object} MessageSearchOptions - * @property {string} [content] Message content - * @property {string} [maxID] Maximum ID for the filter - * @property {string} [minID] Minimum ID for the filter - * @property {string} [has] One of `link`, `embed`, `file`, `video`, `image`, or `sound`, - * or add `-` to negate (e.g. `-file`) - * @property {ChannelResolvable} [channel] Channel to limit search to (only for guild search endpoint) - * @property {UserResolvable} [author] Author to limit search - * @property {string} [authorType] One of `user`, `bot`, `webhook`, or add `-` to negate (e.g. `-webhook`) - * @property {string} [sortBy='recent'] `recent` or `relevant` - * @property {string} [sortOrder='desc'] `asc` or `desc` - * @property {number} [contextSize=2] How many messages to get around the matched message (0 to 2) - * @property {number} [limit=25] Maximum number of results to get (1 to 25) - * @property {number} [offset=0] Offset the "pages" of results (since you can only see 25 at a time) - * @property {UserResolvable} [mentions] Mentioned user filter - * @property {boolean} [mentionsEveryone] If everyone is mentioned - * @property {string} [linkHostname] Filter links by hostname - * @property {string} [embedProvider] The name of an embed provider - * @property {string} [embedType] one of `image`, `video`, `url`, `rich` - * @property {string} [attachmentFilename] The name of an attachment - * @property {string} [attachmentExtention] The extension of an attachment - * @property {Date} [before] Date to find messages before - * @property {Date} [after] Date to find messages before - * @property {Date} [during] Date to find messages during (range of date to date + 24 hours) - */ - class RESTMethods { constructor(restManager) { this.rest = restManager; @@ -148,50 +122,7 @@ class RESTMethods { } search(target, 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).shift(222).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, - }; + options = transformSearchOptions(options, this.client); const queryString = Object.keys(options) .filter(k => options[k]) diff --git a/src/util/TransformSearchOptions.js b/src/util/TransformSearchOptions.js new file mode 100644 index 000000000..e61056aed --- /dev/null +++ b/src/util/TransformSearchOptions.js @@ -0,0 +1,75 @@ +const long = require('long'); + +/** + * @typedef {Object} MessageSearchOptions + * @property {string} [content] Message content + * @property {string} [maxID] Maximum ID for the filter + * @property {string} [minID] Minimum ID for the filter + * @property {string} [has] One of `link`, `embed`, `file`, `video`, `image`, or `sound`, + * or add `-` to negate (e.g. `-file`) + * @property {ChannelResolvable} [channel] Channel to limit search to (only for guild search endpoint) + * @property {UserResolvable} [author] Author to limit search + * @property {string} [authorType] One of `user`, `bot`, `webhook`, or add `-` to negate (e.g. `-webhook`) + * @property {string} [sortBy='recent'] `recent` or `relevant` + * @property {string} [sortOrder='desc'] `asc` or `desc` + * @property {number} [contextSize=2] How many messages to get around the matched message (0 to 2) + * @property {number} [limit=25] Maximum number of results to get (1 to 25) + * @property {number} [offset=0] Offset the "pages" of results (since you can only see 25 at a time) + * @property {UserResolvable} [mentions] Mentioned user filter + * @property {boolean} [mentionsEveryone] If everyone is mentioned + * @property {string} [linkHostname] Filter links by hostname + * @property {string} [embedProvider] The name of an embed provider + * @property {string} [embedType] one of `image`, `video`, `url`, `rich` + * @property {string} [attachmentFilename] The name of an attachment + * @property {string} [attachmentExtention] The extension of an attachment + * @property {Date} [before] Date to find messages before + * @property {Date} [after] Date to find messages before + * @property {Date} [during] Date to find messages during (range of date to date + 24 hours) + */ + +module.exports = function TransformSearchOptions(options, client) { + 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).shift(222).toString(); + } + + if (options.channel) options.channel = client.resolver.resolveChannelID(options.channel); + + if (options.author) options.author = client.resolver.resolveUserID(options.author); + + if (options.mentions) options.mentions = client.resolver.resolveUserID(options.options.mentions); + + return { + 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, + }; +}; From 50dc9addf1bba35b2c3928ecb784f138c56b2897 Mon Sep 17 00:00:00 2001 From: Amish Shah Date: Fri, 30 Dec 2016 17:28:23 +0000 Subject: [PATCH 12/12] Fix ESlint error, remove 'long' module import --- src/client/rest/RESTMethods.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/client/rest/RESTMethods.js b/src/client/rest/RESTMethods.js index 5032957de..213848fe1 100644 --- a/src/client/rest/RESTMethods.js +++ b/src/client/rest/RESTMethods.js @@ -1,4 +1,3 @@ -const long = require('long'); const Constants = require('../../util/Constants'); const Collection = require('../../util/Collection'); const splitMessage = require('../../util/SplitMessage');