Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for search command arguments and JQL queries #261

Draft
wants to merge 1 commit into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions config/default.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,25 @@ projects:
- REALMS
- WEB

fieldShortcuts:
category: cf[11901]
confirmation: Confirmation Status
gamemode: Game Mode
mp: Mojang Priority
version: affectedVersion

containsSearchSyntax:
- ado
- comment
- description
- environment
- summary
- text

prebuiltClauses:
commenter: issueFunction in commented("by $?")
transitionedby: status changed by $!

request:
invalidTicketEmoji: ⏸
noLinkEmoji: ⛔
Expand Down
26 changes: 26 additions & 0 deletions config/template.yml
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,32 @@ projects:
- <string>
- ...

# Key-value pairs of field names that should be replaced when using search shortcuts.
# The key should contain the search shortcut to be used.
# The value should contain the valid JQL name for the field.
fieldShortcuts:
<string1>: <stringA>
<string2>: <stringB>
<string3>: <stringC>
# ...

# Jira field names that use the "contains" JQL operator (~), rather than the "equals" operator (=).
containsSearchSyntax:
- <string>
- <string>
- <string>

# Key-value pairs of prepared JQL clauses that are filled with the search argument.
# The key should contain the search shortcut to be used.
# The value should contain the prepared clauses, with a dollar sign where the argument should be inserted.
# The dollar sign should be followed by a question mark if quotes within the argument should be escaped.
# The dollar sign should be followed by an exclamation point if quotes within the argument should not be escaped.
prebuiltClauses:
<string1>: <stringA>
<string2>: <stringB>
<string3>: <stringC>
# ...

# Settings about channels that handle user requests.
request:
# The IDs of the server's request channels.
Expand Down
8 changes: 8 additions & 0 deletions src/BotConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,10 @@ export default class BotConfig {

public static projects: string[];

public static fieldShortcuts: Map<string, string>;
public static containsSearchSyntax: string[];
public static prebuiltClauses: Map<string, string>;

public static request: RequestConfig;

public static roleGroups: RoleGroupConfig[];
Expand Down Expand Up @@ -149,6 +153,10 @@ export default class BotConfig {

this.projects = config.get( 'projects' );

this.fieldShortcuts = new Map( Object.entries( config.get( 'fieldShortcuts' ) ) );
this.containsSearchSyntax = config.get( 'containsSearchSyntax' );
this.prebuiltClauses = new Map( Object.entries( config.get( 'prebuiltClauses' ) ) );

this.request = new RequestConfig();

this.roleGroups = getOrDefault( 'roleGroups', [] );
Expand Down
4 changes: 4 additions & 0 deletions src/commands/HelpCommand.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,10 @@ export default class HelpCommand extends PrefixCommand {
\`!jira ping\` - Sends a message to check if the bot is running.

\`!jira search <text>\` - Searches for text and returns the results from the bug tracker.

\`!jira search :jql <query>\` - Searches using the content of a JQL query and returns the results.

\`!jira search _<field> <content>\` - Searches a field for specific content and returns the results. These arguments can be used multiple times for multiple fields.

\`!jira tips\` - Sends helpful info on how to use the bug tracker and this Discord server.`
)
Expand Down
90 changes: 80 additions & 10 deletions src/commands/SearchCommand.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Message, MessageEmbed, Util } from 'discord.js';
import { Message, MessageEmbed } from 'discord.js';
import PrefixCommand from './PrefixCommand';
import BotConfig from '../BotConfig';
import MojiraBot from '../MojiraBot';
Expand All @@ -11,37 +11,107 @@ export default class SearchCommand extends PrefixCommand {
return false;
}

const plainArgs = args.replace( /"|<|>/g, '' );
const plainArgs = args.replace( /[<>!]/g, '' );
let searchFilter: string;
if ( plainArgs.includes( ':jql' ) ) {
searchFilter = plainArgs.split( ':jql' ).slice( 1 ).join( ' ' ).trim();
} else {
const modifierRegex = new RegExp( /(^|\s)(_|-)[a-z]+\s(([a-zA-Z0-9_]+)|(["'][^"']+["']))/, 'g' );
const modifiers = plainArgs.match( modifierRegex );
const textArgs = plainArgs.replace( modifierRegex, '' ).trim();
const modifierStrings = [];

if ( modifiers ) {
for ( const spacedModifier of modifiers ) {
const modifier = spacedModifier.trim();
const operation = modifier.charAt( 0 );
let field = modifier.split( /\s/g )[0].substring( 1 );
const value = modifier.split( /\s/g ).slice( 1 ).join( ' ' );

BotConfig.fieldShortcuts.forEach( ( replaced: string, original: string ) => {
const fieldRegex = new RegExp( original, 'g' );
const quoteChar = replaced.split( /\s/g ).length > 1 ? '"' : '';
const quotedReplaced = `${ quoteChar }${ replaced }${ quoteChar }`;
field = field.replace( fieldRegex, quotedReplaced );
} );

let forcedPush = '';
BotConfig.prebuiltClauses.forEach( ( fun: string, original: string ) => {
if ( field == original ) {
const quotedReplaced = value.replace( /["]/g, '\\$&' );
const filledFunction = fun
.replace( /\$\?/g, quotedReplaced )
.replace( /\$!/g, value );
forcedPush = filledFunction;
}
} );

if ( forcedPush.length > 0 ) {
if ( operation == '-' ) forcedPush = `NOT (${ forcedPush })`;
modifierStrings.push( forcedPush );
continue;
}

let clause: string;
if ( value.toUpperCase() == 'EMPTY' ) {
clause = `${ field } is ${ operation == '-' ? 'NOT ' : '' }EMPTY`;
} else {
clause = `${ field } ${ operation == '-' ? '!' : '' }${ BotConfig.containsSearchSyntax.includes( field ) ? '~' : '=' } ${ value }`;
}

modifierStrings.push( clause );
}
} else {
modifierStrings.push( `text ~ "${ textArgs }"` );
}

searchFilter = modifierStrings.join( ' AND ' );

if ( !searchFilter.toLowerCase().includes( 'text ~ ' ) && textArgs.length > 0 ) {
searchFilter += ` AND text ~ "${ textArgs.replace( /["']/g, '\\&' ) }"`;
}
if ( !searchFilter.toUpperCase().includes( ' ORDER BY ' ) ) {
searchFilter += ' ORDER BY created, updated DESC';
}
}


try {
const embed = new MessageEmbed();
const searchFilter = `text ~ "${ plainArgs }" AND project in (${ BotConfig.projects.join( ', ' ) })`;
const embed = new MessageEmbed()
.setColor( 'BLUE' );

const searchResults = await MojiraBot.jira.issueSearch.searchForIssuesUsingJql( {
jql: searchFilter,
maxResults: BotConfig.maxSearchResults,
fields: [ 'key', 'summary' ],
} );

if ( !searchResults.issues ) {
embed.setTitle( `No results found for "${ Util.escapeMarkdown( plainArgs ) }"` );
if ( searchFilter.length > 0 ) {
embed.addField( 'JQL query', `\`\`\`${ searchFilter.replace( /```/g, '` ` `' ) }\`\`\``, false );
}

if ( !searchResults.issues || searchResults.issues.length == 0 ) {
embed.setTitle( 'No results found' );
await message.channel.send( { embeds: [embed] } );
return false;
}

embed.setTitle( '**Results:**' );
embed.setTitle( `${ searchResults.total } result${ searchResults.total != 1 ? 's' : '' }` );
embed.setFooter( { text: message.author.tag, iconURL: message.author.avatarURL() } );

for ( const issue of searchResults.issues ) {
embed.addField( issue.key, `[${ issue.fields.summary }](https://bugs.mojang.com/browse/${ issue.key })` );
}

const escapedJql = encodeURIComponent( searchFilter ).replace( /\(/g, '%28' ).replace( /\)/g, '%29' );
embed.setDescription( `__[See all results](https://bugs.mojang.com/issues/?jql=${ escapedJql })__` );
embed.setDescription( `[See all results](https://bugs.mojang.com/issues/?jql=${ escapedJql })` );

await message.channel.send( { embeds: [embed] } );
} catch {
const embed = new MessageEmbed();
embed.setTitle( `No results found for "${ Util.escapeMarkdown( plainArgs ) }"` );
const embed = new MessageEmbed()
.setTitle( 'Failed to search issues' )
.setColor( 'RED' )
.addField( 'JQL query', `\`\`\`${ searchFilter.replace( /```/g, '` ` `' ) }\`\`\`` );
await message.channel.send( { embeds: [embed] } );
return false;
}
Expand Down