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 a verification system #209

Closed
wants to merge 30 commits into from
Closed
Show file tree
Hide file tree
Changes from 9 commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
4bf0a14
Commit early verification stuff
chandler05 May 25, 2021
26380dd
Basic, but mostly working, verification
dericksonmark May 27, 2021
bd41ae5
Fix some syntax issues
dericksonmark May 27, 2021
dd6f3b8
Semicolon!
dericksonmark May 27, 2021
0624714
Spaces and awaits
dericksonmark May 27, 2021
bfae410
Ah, there are the rest of the errors.
dericksonmark May 27, 2021
50ebd29
Adjustments to verification
chandler05 May 27, 2021
57ea559
Fetch pending verifications on startup
chandler05 May 27, 2021
9d5bd84
Add a whois command
dericksonmark May 28, 2021
c386189
Changes to the whois command
dericksonmark May 28, 2021
c17ed96
Remove asterisks from Jira comment
dericksonmark May 28, 2021
b9caa1b
Add 'DISCORD-VERIFICATION-' to start of token name
dericksonmark May 28, 2021
0854143
Add support for temporary role removal
dericksonmark May 29, 2021
5893eb8
Allow for link removal
dericksonmark May 29, 2021
da53163
Switch verification message handler to cache
dericksonmark May 30, 2021
60f9c2b
Schedule deletion of pending verification on startup
dericksonmark May 30, 2021
dffc9db
Improve checking for verification
dericksonmark Jun 1, 2021
caa9c94
Update loop type in whois command
dericksonmark Jun 1, 2021
c23eb48
Fix loop and send message on whois failure
chandler05 Jun 1, 2021
fb36b02
Add footer to 'user not found' embed
dericksonmark Jun 1, 2021
c6f0980
Merge branch 'master' into verification
chandler05 Jun 1, 2021
60667ba
Add accounts to verification confirmation message
dericksonmark Jun 1, 2021
7500a93
Check for a Discord account linked to Mojira account
dericksonmark Jun 6, 2021
c77ffbf
Update help command commands
dericksonmark Jun 10, 2021
f6c9a68
Set request footer to Mojira username
dericksonmark Jun 11, 2021
c2b9138
Merge branch 'master' into verification
dericksonmark Jun 12, 2021
9deebe6
Switch Mojira username to a new embed field
dericksonmark Jun 12, 2021
44cd202
Replace forEach loops with for of loops
dericksonmark Jun 12, 2021
a5f8591
Adjust token generation
dericksonmark Jun 16, 2021
a289bc7
Merge branch 'master' into verification
chandler05 Sep 21, 2021
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
7 changes: 7 additions & 0 deletions config/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -114,3 +114,10 @@ versionFeeds:
- released
- unreleased
- renamed

verification:
verificationTicket: '' # Add a Mojira ticket ID here!
pendingVerificationChannel: '' # Add a pending channel here!
verificationLogChannel: '' # Add a log channel here!
verifiedRole: '' # Add a role here!
verificationInvalidationTime: 900000
17 changes: 17 additions & 0 deletions config/template.yml
Original file line number Diff line number Diff line change
Expand Up @@ -222,3 +222,20 @@ versionFeeds:

- <see above>
- ...

# Settings for user verification on Jira.
verification:
# The ticket ID in which users can leave a special token on to verify and link their Discord and Jira accounts.
verificationTicket: <string>

# The channel that holds the pending verification request information.
pendingVerificationChannel: <string>

# The channel that holds the linked account information after accounts have been linked.
verificationLogChannel: <string>

# The role that is given to verified users.
verifiedRole: <string>

# The amount of time that a verification request can remain open until it expires, in milliseconds.
verificationInvalidationTime: <number>
20 changes: 20 additions & 0 deletions src/BotConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,22 @@ export class RequestConfig {
}
}

export class VerificationConfig {
public verificationTicket: string;
public pendingVerificationChannel: string;
public verificationLogChannel: string;
public verifiedRole: string;
public verificationInvalidationTime: number;

constructor() {
this.verificationTicket = config.get( 'verification.verificationTicket' );
this.pendingVerificationChannel = config.get( 'verification.pendingVerificationChannel' );
this.verificationLogChannel = config.get( 'verification.verificationLogChannel' )
this.verifiedRole = config.get( 'verification.verifiedRole' )
this.verificationInvalidationTime = config.get( 'verification.verificationInvalidationTime' )
}
}

export interface RoleConfig {
emoji: string;
title: string;
Expand Down Expand Up @@ -121,6 +137,8 @@ export default class BotConfig {

public static request: RequestConfig;

public static verification: VerificationConfig;

public static roleGroups: RoleGroupConfig[];

public static filterFeeds: FilterFeedConfig[];
Expand All @@ -144,6 +162,8 @@ export default class BotConfig {

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

this.verification = new VerificationConfig();

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

this.request = new RequestConfig();
Expand Down
24 changes: 24 additions & 0 deletions src/MojiraBot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,30 @@ export default class MojiraBot {
const requestChannels: TextChannel[] = [];
const internalChannels = new Map<string, string>();

if ( BotConfig.verification.pendingVerificationChannel ) {
const pendingChannel = await DiscordUtil.getChannel( BotConfig.verification.pendingVerificationChannel );
if ( pendingChannel instanceof TextChannel ) {
// https://stackoverflow.com/questions/55153125/fetch-more-than-100-messages
const allMessages: Message[] = [];
let lastId: string | undefined;
let continueSearch = true;

while ( continueSearch ) {
const options: ChannelLogsQueryOptions = { limit: 50 };
if ( lastId ) {
options.before = lastId;
}
const messages = await pendingChannel.messages.fetch( options );
allMessages.push( ...messages.array() );
lastId = messages.last()?.id;
if ( messages.size !== 50 || !lastId ) {
continueSearch = false;
}
}
this.logger.info( `Fetched ${ allMessages.length } messages from "${ pendingChannel.name }"` );
dericksonmark marked this conversation as resolved.
Show resolved Hide resolved
}
}

if ( BotConfig.request.channels ) {
for ( let i = 0; i < BotConfig.request.channels.length; i++ ) {
const requestChannelId = BotConfig.request.channels[i];
Expand Down
4 changes: 4 additions & 0 deletions src/commands/CommandRegistry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import SendCommand from './SendCommand';
import SearchCommand from './SearchCommand';
import ShutdownCommand from './ShutdownCommand';
import TipsCommand from './TipsCommand';
import VerifyCommand from './VerifyCommand';
import WhoisCommand from './WhoisCommand';

export default class CommandRegistry {
public static BUG_COMMAND = new BugCommand();
Expand All @@ -20,4 +22,6 @@ export default class CommandRegistry {
public static SEARCH_COMMAND = new SearchCommand();
public static SHUTDOWN_COMMAND = new ShutdownCommand();
public static TIPS_COMMAND = new TipsCommand();
public static VERIFY_COMMAND = new VerifyCommand();
public static WHOIS_COMMAND = new WhoisCommand();
}
101 changes: 101 additions & 0 deletions src/commands/VerifyCommand.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import { Message, MessageEmbed, TextChannel } from 'discord.js';
import PrefixCommand from './PrefixCommand';
import BotConfig from '../BotConfig';
import Command from './Command';
import DiscordUtil from '../util/DiscordUtil';
import TaskScheduler from '../tasks/TaskScheduler';
import RemovePendingVerificationTask from '../tasks/RemovePendingVerificationTask';

export default class VerifyCommand extends PrefixCommand {
public readonly aliases = ['verify'];

public async run( message: Message, args: string ): Promise<boolean> {
if ( args.length ) {
return false;
}

const pendingChannel = await DiscordUtil.getChannel( BotConfig.verification.pendingVerificationChannel );

if ( pendingChannel instanceof TextChannel ) {

let foundUser = false;

const allMessages = pendingChannel.messages.cache;

allMessages.forEach( async thisMessage => {
if ( thisMessage.embeds === undefined ) return undefined;
if ( thisMessage.embeds[0].fields[0].value.replace( /[<>@!]/g, '' ) == message.author.id ) {
foundUser = true;
}
} );

try {
const role = await pendingChannel.guild.roles.fetch( BotConfig.verification.verifiedRole );
const targetUser = await message.guild.members.fetch( message.author.id );

if ( targetUser.roles.cache.has( role.id ) ) {
await message.channel.send( `${ message.author }, your account has already been verified!` );
await message.react( '❌' );
return false;
}
} catch ( error ) {
Command.logger.error( error );
}

if ( foundUser ) {
await message.channel.send( `${ message.author }, you already have a pending verification request!` );
await message.react( '❌' );
return false;
}

}

try {
const token = this.randomString( 15, '23456789abcdeghjkmnpqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ' );
dericksonmark marked this conversation as resolved.
Show resolved Hide resolved

const userEmbed = new MessageEmbed()
.setDescription( `In order to verify, please comment the following token on the ticket [${ BotConfig.verification.verificationTicket }](https://bugs.mojang.com/browse/${ BotConfig.verification.verificationTicket }) using your Jira account. Make sure you only have added one comment to the ticket!\nAfter you are done, please send \`link <jira-username>\` here and I will verify the account!\n\nToken: **${ token }**` );

await message.author.send( userEmbed );

if ( pendingChannel instanceof TextChannel ) {

const pendingEmbed = new MessageEmbed()
.setColor( 'YELLOW' )
.setAuthor( message.author.tag, message.author.avatarURL() )
.addField( 'User', message.author, true )
.addField( 'Token', token, true )
.setTimestamp( new Date() );

const internalEmbed = await pendingChannel.send( pendingEmbed );

try {
TaskScheduler.addOneTimeMessageTask(
internalEmbed,
new RemovePendingVerificationTask(),
BotConfig.verification.verificationInvalidationTime
);
} catch ( error ) {
Command.logger.error( error );
}
}

await message.react( '✅' );

} catch ( error ) {
Command.logger.error( error );
}
return true;
}

// https://stackoverflow.com/questions/10726909/random-alpha-numeric-string-in-javascript
private randomString( length: number, chars: string ): string {
let result = '';
for ( let i = length; i > 0; --i ) result += chars[Math.floor( Math.random() * chars.length )];
return result;
}

public asString(): string {
return '!jira verify';
}
}
93 changes: 93 additions & 0 deletions src/commands/WhoisCommand.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import { ChannelLogsQueryOptions, Message, MessageEmbed, TextChannel } from 'discord.js';
import BotConfig from '../BotConfig';
import DiscordUtil from '../util/DiscordUtil';
import PrefixCommand from './PrefixCommand';
import Command from './Command';

export default class WhoisCommand extends PrefixCommand {
public readonly aliases = ['who', 'whois'];

public async run( message: Message, args: string ): Promise<boolean> {
if ( !args.length ) {
return false;
}

let fromDiscordWhois = false;
if ( args.startsWith( '<@' ) ) {
fromDiscordWhois = true;
}

if ( message.deletable ) {
try {
await message.delete();
} catch ( error ) {
Command.logger.error( error );
}
} else {
Command.logger.log( 'Message not deletable' );
}


const logChannel = await DiscordUtil.getChannel( BotConfig.verification.verificationLogChannel );
const allMessages: Message[] = [];
let lastId: string | undefined;
let continueSearch = true;

try {
while ( continueSearch ) {
dericksonmark marked this conversation as resolved.
Show resolved Hide resolved
const options: ChannelLogsQueryOptions = { limit: 50 };
if ( lastId ) {
options.before = lastId;
}
if ( logChannel instanceof TextChannel ) {

const messages = await logChannel.messages.fetch( options );
allMessages.push( ...messages.array() );
lastId = messages.last()?.id;
if ( messages.size !== 50 || !lastId ) {
continueSearch = false;

for ( let i = 0; i < allMessages.length; i++ ) {
const content = allMessages[i].embeds;
if ( content === undefined ) continue;

const discordId = content[0].fields[0].value;
const discordMember = await DiscordUtil.getMember( logChannel.guild, discordId.replace( /[<>!@]/g, '' ) );
const mojiraMember = content[0].fields[1].value;

if ( fromDiscordWhois ) {
if ( discordId.replace( /[<>!@]/g, '' ) != args.replace( /[<>!@]/g, '' ) ) continue;

const embed = new MessageEmbed()
.setTitle( 'User information' )
.setDescription( `${ discordMember.user }'s Mojira account is ${ mojiraMember } ` )
.setFooter( message.author.tag, message.author.avatarURL() );
await message.channel.send( embed );

return true;
} else {
if ( mojiraMember.split( '?name=' )[1].split( ')' )[0] != args ) continue;

const embed = new MessageEmbed()
.setTitle( 'User information' )
.setDescription( `${ mojiraMember }'s Discord account is ${ discordId }` )
.setFooter( message.author.tag, message.author.avatarURL() );
await message.channel.send( embed );

return true;
}
}
await message.channel.send( `${ args } has not been verified!` );
}
}
}
} catch {
return false;
}
return true;
}

public asString( args: string ): string {
return `!jira whois ${ args }`;
}
}
10 changes: 9 additions & 1 deletion src/events/message/MessageEventHandler.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import { Message } from 'discord.js';
import { DMChannel, Message } from 'discord.js';
import BotConfig from '../../BotConfig';
import CommandExecutor from '../../commands/CommandExecutor';
import DiscordUtil from '../../util/DiscordUtil';
import EventHandler from '../EventHandler';
import RequestEventHandler from '../request/RequestEventHandler';
import TestingRequestEventHandler from '../request/TestingRequestEventHandler';
import InternalProgressEventHandler from '../internal/InternalProgressEventHandler';
import VerificationMessageEventHandler from '../verification/VerificationMessageEventHandler';

export default class MessageEventHandler implements EventHandler<'message'> {
public readonly eventName = 'message';
Expand All @@ -15,13 +16,15 @@ export default class MessageEventHandler implements EventHandler<'message'> {
private readonly requestEventHandler: RequestEventHandler;
private readonly testingRequestEventHandler: TestingRequestEventHandler;
private readonly internalProgressEventHandler: InternalProgressEventHandler;
private readonly verificationMessageEventHandler: VerificationMessageEventHandler;

constructor( botUserId: string, internalChannels: Map<string, string> ) {
this.botUserId = botUserId;

this.requestEventHandler = new RequestEventHandler( internalChannels );
this.testingRequestEventHandler = new TestingRequestEventHandler();
this.internalProgressEventHandler = new InternalProgressEventHandler();
this.verificationMessageEventHandler = new VerificationMessageEventHandler();
}

// This syntax is used to ensure that `this` refers to the `MessageEventHandler` object
Expand Down Expand Up @@ -56,6 +59,11 @@ export default class MessageEventHandler implements EventHandler<'message'> {
await this.internalProgressEventHandler.onEvent( message );

// Don't reply in internal request channels
return;
} else if ( message.channel.type === "dm" ) {
// This message is a direct message
await this.verificationMessageEventHandler.onEvent( message );

return;
}

Expand Down
Loading