From 54052d9237264b6bffb5ee24400cac6fa97709ef Mon Sep 17 00:00:00 2001 From: roflmaoqwerty Date: Sun, 26 Jan 2020 09:14:06 +1100 Subject: [PATCH 1/4] implemented performBanSearch --- backend/src/plugins/Utility.ts | 163 +++++++++++++++++++++++++++++---- 1 file changed, 145 insertions(+), 18 deletions(-) diff --git a/backend/src/plugins/Utility.ts b/backend/src/plugins/Utility.ts index 5f41d2fe..758d4808 100644 --- a/backend/src/plugins/Utility.ts +++ b/backend/src/plugins/Utility.ts @@ -123,6 +123,13 @@ type MemberSearchParams = { "status-search"?: boolean; }; +type BanSearchParams = { + query?: string; + sort?: string; + "case-sensitive"?: boolean; + regex?: boolean; +}; + class SearchError extends Error {} export class UtilityPlugin extends ZeppelinPlugin { @@ -342,6 +349,63 @@ export class UtilityPlugin extends ZeppelinPlugin { msg.channel.createMessage(`The permission level of ${member.username}#${member.discriminator} is **${level}**`); } + protected async performBanSearch( + args: BanSearchParams, + page = 1, + perPage = SEARCH_RESULTS_PER_PAGE, + ): Promise<{ results: User[]; totalResults: number; page: number; lastPage: number; from: number; to: number }> { + let matchingBans = (await this.guild.getBans()).map(x => x.user); + + if (args.query) { + let queryRegex: RegExp; + if (args.regex) { + queryRegex = new RegExp(args.query.trimStart(), args["case-sensitive"] ? "" : "i"); + } else { + queryRegex = new RegExp(escapeStringRegexp(args.query.trimStart()), args["case-sensitive"] ? "" : "i"); + } + + if (!safeRegex(queryRegex)) { + throw new SearchError("Unsafe/too complex regex (star depth is limited to 1)"); + } + + matchingBans = matchingBans.filter(user => { + const fullUsername = `${user.username}#${user.discriminator}`; + if (fullUsername.match(queryRegex)) return true; + }); + } + + const [, sortDir, sortBy] = args.sort ? args.sort.match(/^(-?)(.*)$/) : [null, "ASC", "name"]; + const realSortDir = sortDir === "-" ? "DESC" : "ASC"; + + if (sortBy === "id") { + matchingBans.sort(sorter(m => BigInt(m.id), realSortDir)); + } else { + matchingBans.sort( + multiSorter([ + [m => m.username.toLowerCase(), realSortDir], + [m => m.discriminator, realSortDir], + ]), + ); + } + + const lastPage = Math.max(1, Math.ceil(matchingBans.length / perPage)); + page = Math.min(lastPage, Math.max(1, page)); + + const from = (page - 1) * perPage; + const to = Math.min(from + perPage, matchingBans.length); + + const pageMembers = matchingBans.slice(from, to); + + return { + results: pageMembers, + totalResults: matchingBans.length, + page, + lastPage, + from: from + 1, + to, + }; + } + protected async performMemberSearch( args: MemberSearchParams, page = 1, @@ -457,6 +521,21 @@ export class UtilityPlugin extends ZeppelinPlugin { }; } + protected formatSearchResultList(members: Member[]): string { + const longestId = members.reduce((longest, member) => Math.max(longest, member.id.length), 0); + const lines = members.map(member => { + const paddedId = member.id.padEnd(longestId, " "); + let line = `${paddedId} ${member.user.username}#${member.user.discriminator}`; + if (member.nick) line += ` (${member.nick})`; + return line; + }); + return lines.join("\n"); + } + + protected formatSearchResultIdList(members: Member[]): string { + return members.map(m => m.id).join(" "); + } + @d.command("search", "[query:string$]", { aliases: ["s"], options: [ @@ -542,21 +621,6 @@ export class UtilityPlugin extends ZeppelinPlugin { "status-search"?: boolean; }, ) { - const formatSearchResultList = (members: Member[]): string => { - const longestId = members.reduce((longest, member) => Math.max(longest, member.id.length), 0); - const lines = members.map(member => { - const paddedId = member.id.padEnd(longestId, " "); - let line = `${paddedId} ${member.user.username}#${member.user.discriminator}`; - if (member.nick) line += ` (${member.nick})`; - return line; - }); - return lines.join("\n"); - }; - - const formatSearchResultIdList = (members: Member[]): string => { - return members.map(m => m.id).join(" "); - }; - // If we're exporting the results, we don't need all the fancy schmancy pagination stuff. // Just get the results and dump them in an archive. if (args.export) { @@ -575,7 +639,9 @@ export class UtilityPlugin extends ZeppelinPlugin { return this.sendErrorMessage(msg.channel, "No results found"); } - const resultList = args.ids ? formatSearchResultIdList(results.results) : formatSearchResultList(results.results); + const resultList = args.ids + ? this.formatSearchResultIdList(results.results) + : this.formatSearchResultList(results.results); const archiveId = await this.archives.create( trimLines(` @@ -640,8 +706,8 @@ export class UtilityPlugin extends ZeppelinPlugin { : `Found ${searchResult.totalResults} ${resultWord}`; const resultList = args.ids - ? formatSearchResultIdList(searchResult.results) - : formatSearchResultList(searchResult.results); + ? this.formatSearchResultIdList(searchResult.results) + : this.formatSearchResultList(searchResult.results); const result = trimLines(` ${headerText} @@ -694,6 +760,67 @@ export class UtilityPlugin extends ZeppelinPlugin { loadSearchPage(currentPage); } + @d.command("bansearch", "[query:string$]", { + aliases: ["bs"], + options: [ + { + name: "page", + shortcut: "p", + type: "number", + }, + { + name: "sort", + type: "string", + }, + { + name: "case-sensitive", + shortcut: "cs", + isSwitch: true, + }, + { + name: "export", + shortcut: "e", + isSwitch: true, + }, + { + name: "ids", + isSwitch: true, + }, + { + name: "regex", + shortcut: "re", + isSwitch: true, + }, + ], + extra: { + info: { + description: "Search banned users", + basicUsage: "!bansearch dragory", + optionDescriptions: { + sort: + "Change how the results are sorted. Possible values are 'id' and 'name'. Prefix with a dash, e.g. '-id', to reverse sorting.", + "case-sensitive": "By default, the search is case-insensitive. Use this to make it case-sensitive instead.", + export: "If set, the full search results are exported as an archive", + }, + }, + }, + }) + @d.permission("can_search") + async banSearchCmd( + msg: Message, + args: { + query?: string; + page?: number; + sort?: string; + "case-sensitive"?: boolean; + export?: boolean; + ids?: boolean; + regex?: boolean; + }, + ) { + // TODO: implement command + } + async cleanMessages(channel: Channel, savedMessages: SavedMessage[], mod: User) { this.logs.ignoreLog(LogType.MESSAGE_DELETE, savedMessages[0].id); this.logs.ignoreLog(LogType.MESSAGE_DELETE_BULK, savedMessages[0].id); From d7e2cf8721e8383a1cfc7787775ad1cea477c944 Mon Sep 17 00:00:00 2001 From: roflmaoqwerty <36663568+roflmaoqwerty@users.noreply.github.com> Date: Wed, 29 Jan 2020 07:42:27 +1100 Subject: [PATCH 2/4] refactored search result format methods --- backend/src/plugins/Utility.ts | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/backend/src/plugins/Utility.ts b/backend/src/plugins/Utility.ts index 758d4808..3e0a6625 100644 --- a/backend/src/plugins/Utility.ts +++ b/backend/src/plugins/Utility.ts @@ -521,18 +521,23 @@ export class UtilityPlugin extends ZeppelinPlugin { }; } - protected formatSearchResultList(members: Member[]): string { + protected formatSearchResultList(members: Array): string { const longestId = members.reduce((longest, member) => Math.max(longest, member.id.length), 0); const lines = members.map(member => { const paddedId = member.id.padEnd(longestId, " "); - let line = `${paddedId} ${member.user.username}#${member.user.discriminator}`; - if (member.nick) line += ` (${member.nick})`; + let line; + if (member instanceof Member) { + line = `${paddedId} ${member.user.username}#${member.user.discriminator}`; + if (member.nick) line += ` (${member.nick})`; + } else { + line = `${paddedId} ${member.username}#${member.discriminator}`; + } return line; }); return lines.join("\n"); } - protected formatSearchResultIdList(members: Member[]): string { + protected formatSearchResultIdList(members: Array): string { return members.map(m => m.id).join(" "); } From f376b173ccff1e10702140217ee9848182bc13bc Mon Sep 17 00:00:00 2001 From: roflmaoqwerty <36663568+roflmaoqwerty@users.noreply.github.com> Date: Mon, 18 May 2020 23:59:45 +1000 Subject: [PATCH 3/4] implemented ban search command. moved search rendering code to new method --- backend/src/plugins/Utility.ts | 199 ++++++++++++++++++--------------- 1 file changed, 107 insertions(+), 92 deletions(-) diff --git a/backend/src/plugins/Utility.ts b/backend/src/plugins/Utility.ts index 3e0a6625..ff186d68 100644 --- a/backend/src/plugins/Utility.ts +++ b/backend/src/plugins/Utility.ts @@ -130,6 +130,10 @@ type BanSearchParams = { regex?: boolean; }; +enum SearchType { + MemberSearch, + BanSearch, +} class SearchError extends Error {} export class UtilityPlugin extends ZeppelinPlugin { @@ -663,6 +667,101 @@ export class UtilityPlugin extends ZeppelinPlugin { return; } + await this.displaySearchResults(args, SearchType.MemberSearch, msg); + } + + @d.command("bansearch", "[query:string$]", { + aliases: ["bs"], + options: [ + { + name: "page", + shortcut: "p", + type: "number", + }, + { + name: "sort", + type: "string", + }, + { + name: "case-sensitive", + shortcut: "cs", + isSwitch: true, + }, + { + name: "export", + shortcut: "e", + isSwitch: true, + }, + { + name: "ids", + isSwitch: true, + }, + { + name: "regex", + shortcut: "re", + isSwitch: true, + }, + ], + extra: { + info: { + description: "Search banned users", + basicUsage: "!bansearch dragory", + optionDescriptions: { + sort: + "Change how the results are sorted. Possible values are 'id' and 'name'. Prefix with a dash, e.g. '-id', to reverse sorting.", + "case-sensitive": "By default, the search is case-insensitive. Use this to make it case-sensitive instead.", + export: "If set, the full search results are exported as an archive", + }, + }, + }, + }) + @d.permission("can_search") + async banSearchCmd( + msg: Message, + args: { + query?: string; + page?: number; + sort?: string; + "case-sensitive"?: boolean; + export?: boolean; + ids?: boolean; + regex?: boolean; + }, + ) { + await this.displaySearchResults(args, SearchType.BanSearch, msg); + } + + async cleanMessages(channel: Channel, savedMessages: SavedMessage[], mod: User) { + this.logs.ignoreLog(LogType.MESSAGE_DELETE, savedMessages[0].id); + this.logs.ignoreLog(LogType.MESSAGE_DELETE_BULK, savedMessages[0].id); + + // Delete & archive in ID order + savedMessages = Array.from(savedMessages).sort((a, b) => (a.id > b.id ? 1 : -1)); + const idsToDelete = savedMessages.map(m => m.id); + + // Make sure the deletions aren't double logged + idsToDelete.forEach(id => this.logs.ignoreLog(LogType.MESSAGE_DELETE, id)); + this.logs.ignoreLog(LogType.MESSAGE_DELETE_BULK, idsToDelete[0]); + + // Actually delete the messages + await this.bot.deleteMessages(channel.id, idsToDelete); + await this.savedMessages.markBulkAsDeleted(idsToDelete); + + // Create an archive + const archiveId = await this.archives.createFromSavedMessages(savedMessages, this.guild); + const archiveUrl = this.archives.getUrl(this.knub.getGlobalConfig().url, archiveId); + + this.logs.log(LogType.CLEAN, { + mod: stripObjectToScalars(mod), + channel: stripObjectToScalars(channel), + count: savedMessages.length, + archiveUrl, + }); + + return { archiveUrl }; + } + + async displaySearchResults(args: any, searchType: SearchType, msg: Message) { // If we're not exporting, load 1 page of search results at a time and allow the user to switch pages with reactions let originalSearchMsg: Message = null; let searching = false; @@ -689,7 +788,14 @@ export class UtilityPlugin extends ZeppelinPlugin { let searchResult; try { - searchResult = await this.performMemberSearch(args, page, perPage); + switch (searchType) { + case SearchType.MemberSearch: + searchResult = await this.performMemberSearch(args, page, perPage); + break; + case SearchType.BanSearch: + searchResult = await this.performBanSearch(args, page, perPage); + break; + } } catch (e) { if (e instanceof SearchError) { return this.sendErrorMessage(msg.channel, e.message); @@ -765,97 +871,6 @@ export class UtilityPlugin extends ZeppelinPlugin { loadSearchPage(currentPage); } - @d.command("bansearch", "[query:string$]", { - aliases: ["bs"], - options: [ - { - name: "page", - shortcut: "p", - type: "number", - }, - { - name: "sort", - type: "string", - }, - { - name: "case-sensitive", - shortcut: "cs", - isSwitch: true, - }, - { - name: "export", - shortcut: "e", - isSwitch: true, - }, - { - name: "ids", - isSwitch: true, - }, - { - name: "regex", - shortcut: "re", - isSwitch: true, - }, - ], - extra: { - info: { - description: "Search banned users", - basicUsage: "!bansearch dragory", - optionDescriptions: { - sort: - "Change how the results are sorted. Possible values are 'id' and 'name'. Prefix with a dash, e.g. '-id', to reverse sorting.", - "case-sensitive": "By default, the search is case-insensitive. Use this to make it case-sensitive instead.", - export: "If set, the full search results are exported as an archive", - }, - }, - }, - }) - @d.permission("can_search") - async banSearchCmd( - msg: Message, - args: { - query?: string; - page?: number; - sort?: string; - "case-sensitive"?: boolean; - export?: boolean; - ids?: boolean; - regex?: boolean; - }, - ) { - // TODO: implement command - } - - async cleanMessages(channel: Channel, savedMessages: SavedMessage[], mod: User) { - this.logs.ignoreLog(LogType.MESSAGE_DELETE, savedMessages[0].id); - this.logs.ignoreLog(LogType.MESSAGE_DELETE_BULK, savedMessages[0].id); - - // Delete & archive in ID order - savedMessages = Array.from(savedMessages).sort((a, b) => (a.id > b.id ? 1 : -1)); - const idsToDelete = savedMessages.map(m => m.id); - - // Make sure the deletions aren't double logged - idsToDelete.forEach(id => this.logs.ignoreLog(LogType.MESSAGE_DELETE, id)); - this.logs.ignoreLog(LogType.MESSAGE_DELETE_BULK, idsToDelete[0]); - - // Actually delete the messages - await this.bot.deleteMessages(channel.id, idsToDelete); - await this.savedMessages.markBulkAsDeleted(idsToDelete); - - // Create an archive - const archiveId = await this.archives.createFromSavedMessages(savedMessages, this.guild); - const archiveUrl = this.archives.getUrl(this.knub.getGlobalConfig().url, archiveId); - - this.logs.log(LogType.CLEAN, { - mod: stripObjectToScalars(mod), - channel: stripObjectToScalars(channel), - count: savedMessages.length, - archiveUrl, - }); - - return { archiveUrl }; - } - @d.command("clean", "", { options: [ { From dddbf5c096b04a962357e66919bba16f7a44d34c Mon Sep 17 00:00:00 2001 From: roflmaoqwerty <36663568+roflmaoqwerty@users.noreply.github.com> Date: Sat, 23 May 2020 11:53:31 +1000 Subject: [PATCH 4/4] refactored archive searcg into its own method. Updated ban and regular search to use these methods --- backend/src/plugins/Utility.ts | 87 ++++++++++++++++++++-------------- 1 file changed, 51 insertions(+), 36 deletions(-) diff --git a/backend/src/plugins/Utility.ts b/backend/src/plugins/Utility.ts index ff186d68..8ff49e8c 100644 --- a/backend/src/plugins/Utility.ts +++ b/backend/src/plugins/Utility.ts @@ -633,41 +633,10 @@ export class UtilityPlugin extends ZeppelinPlugin { // If we're exporting the results, we don't need all the fancy schmancy pagination stuff. // Just get the results and dump them in an archive. if (args.export) { - let results; - try { - results = await this.performMemberSearch(args, 1, SEARCH_EXPORT_LIMIT); - } catch (e) { - if (e instanceof SearchError) { - return this.sendErrorMessage(msg.channel, e.message); - } - - throw e; - } - - if (results.totalResults === 0) { - return this.sendErrorMessage(msg.channel, "No results found"); - } - - const resultList = args.ids - ? this.formatSearchResultIdList(results.results) - : this.formatSearchResultList(results.results); - - const archiveId = await this.archives.create( - trimLines(` - Search results (total ${results.totalResults}): - - ${resultList} - `), - moment().add(1, "hour"), - ); - const url = await this.archives.getUrl(this.knub.getGlobalConfig().url, archiveId); - - msg.channel.createMessage(`Exported search results: ${url}`); - - return; + return this.archiveSearch(args, SearchType.MemberSearch, msg); + } else { + return this.displaySearch(args, SearchType.MemberSearch, msg); } - - await this.displaySearchResults(args, SearchType.MemberSearch, msg); } @d.command("bansearch", "[query:string$]", { @@ -728,7 +697,11 @@ export class UtilityPlugin extends ZeppelinPlugin { regex?: boolean; }, ) { - await this.displaySearchResults(args, SearchType.BanSearch, msg); + if (args.export) { + return this.archiveSearch(args, SearchType.BanSearch, msg); + } else { + return this.displaySearch(args, SearchType.BanSearch, msg); + } } async cleanMessages(channel: Channel, savedMessages: SavedMessage[], mod: User) { @@ -761,7 +734,49 @@ export class UtilityPlugin extends ZeppelinPlugin { return { archiveUrl }; } - async displaySearchResults(args: any, searchType: SearchType, msg: Message) { + async archiveSearch(args: any, searchType: SearchType, msg: Message) { + let results; + try { + switch (searchType) { + case SearchType.MemberSearch: + results = await this.performMemberSearch(args, 1, SEARCH_EXPORT_LIMIT); + break; + case SearchType.BanSearch: + results = await this.performBanSearch(args, 1, SEARCH_EXPORT_LIMIT); + break; + } + } catch (e) { + if (e instanceof SearchError) { + return this.sendErrorMessage(msg.channel, e.message); + } + + throw e; + } + + if (results.totalResults === 0) { + return this.sendErrorMessage(msg.channel, "No results found"); + } + + const resultList = args.ids + ? this.formatSearchResultIdList(results.results) + : this.formatSearchResultList(results.results); + + const archiveId = await this.archives.create( + trimLines(` + Search results (total ${results.totalResults}): + + ${resultList} + `), + moment().add(1, "hour"), + ); + const url = await this.archives.getUrl(this.knub.getGlobalConfig().url, archiveId); + + msg.channel.createMessage(`Exported search results: ${url}`); + + return; + } + + async displaySearch(args: any, searchType: SearchType, msg: Message) { // If we're not exporting, load 1 page of search results at a time and allow the user to switch pages with reactions let originalSearchMsg: Message = null; let searching = false;