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 all 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
9 changes: 9 additions & 0 deletions config/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -121,3 +121,12 @@ 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
removeLinkEmoji: '✂️'
removeLinkWaitTime: 5000
23 changes: 23 additions & 0 deletions config/template.yml
Original file line number Diff line number Diff line change
Expand Up @@ -244,3 +244,26 @@ 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>

# The emoji which, when used to react to a verification link embed, removes the link.
removeLinkEmoji: <string>

# The amount of time before a verification link is removed, when the message is reacted to with the remove link emoji.
removeLinkWaitTime: <number>
24 changes: 24 additions & 0 deletions src/BotConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,26 @@ export class RequestConfig {
}
}

export class VerificationConfig {
public verificationTicket: string;
public pendingVerificationChannel: string;
public verificationLogChannel: string;
public verifiedRole: string;
public verificationInvalidationTime: number;
public removeLinkEmoji: string;
public removeLinkWaitTime: 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' );
this.removeLinkEmoji = config.get( 'verification.removeLinkEmoji' );
this.removeLinkWaitTime = config.get( 'verification.removeLinkWaitTime' );
}
}

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

public static request: RequestConfig;

public static verification: VerificationConfig;

public static roleGroups: RoleGroupConfig[];

public static filterFeeds: FilterFeedConfig[];
Expand All @@ -148,6 +170,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
66 changes: 66 additions & 0 deletions src/MojiraBot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import FilterFeedTask from './tasks/FilterFeedTask';
import CachedFilterFeedTask from './tasks/CachedFilterFeedTask';
import TaskScheduler from './tasks/TaskScheduler';
import VersionFeedTask from './tasks/VersionFeedTask';
import RemovePendingVerificationTask from './tasks/RemovePendingVerificationTask';
import DiscordUtil from './util/DiscordUtil';
import { RoleSelectionUtil } from './util/RoleSelectionUtil';

Expand Down Expand Up @@ -100,6 +101,71 @@ export default class MojiraBot {
const internalChannels = new Map<Snowflake, Snowflake>();
const requestLimits = new Map<Snowflake, number>();

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

// Schedule invalidation of pending verification requests
const handler = new RemovePendingVerificationTask();
for ( const message of allMessages ) {
const expiration = BotConfig.verification.verificationInvalidationTime - ( Date.now() - message.createdTimestamp );
if ( expiration > 0 ) {
this.logger.info( `Scheduling deletion of message ${ message.id } in ${ expiration } ms` );
TaskScheduler.addOneTimeMessageTask(
message,
handler,
expiration
);
} else {
this.logger.info( `Deleted pending verification request ${ message.id } (scheduled to be deleted ${ -expiration } ms ago)` );
await message.delete();
}
}
}
}

if ( BotConfig.verification.verificationLogChannel ) {
const verificationLogChannel = await DiscordUtil.getChannel( BotConfig.verification.verificationLogChannel );
if ( verificationLogChannel 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 verificationLogChannel.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 "${ verificationLogChannel.name }"` );
}
}

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();
}
4 changes: 3 additions & 1 deletion src/commands/HelpCommand.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,9 @@ export default class HelpCommand extends PrefixCommand {

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

\`!jira tips\` - Sends helpful info on how to use the bug tracker and this Discord server.`
\`!jira tips\` - Sends helpful info on how to use the bug tracker and this Discord server.

\`!jira verify\` - Begins a process to link your Discord account with your Mojira account.`
)
.setFooter( message.author.tag, message.author.avatarURL() );
await message.channel.send( { embeds: [embed] } );
Expand Down
117 changes: 117 additions & 0 deletions src/commands/VerifyCommand.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
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 );
const logChannel = await DiscordUtil.getChannel( BotConfig.verification.verificationLogChannel );

if ( pendingChannel instanceof TextChannel && logChannel instanceof TextChannel ) {

let foundUser = false;
let isVerified = false;

const allMessages = pendingChannel.messages.cache;
const verifications = logChannel.messages.cache;

for ( const loop of allMessages ) {
const thisMessage = loop[1];
if ( thisMessage.embeds.length == 0 ) continue;
if ( thisMessage.embeds[0].fields[0].value.replace( /[<>@!]/g, '' ) == message.author.id ) {
foundUser = true;
break;
}
}

for ( const loop of verifications ) {
const thisMessage = loop[1];
if ( thisMessage.embeds.length == 0 ) continue;
if ( thisMessage.embeds[0].fields[0].value.replace( /[<>@!]/g, '' ) == message.author.id ) {
isVerified = true;
break;
}
}

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 ) || isVerified ) {
await message.channel.send( `${ message.author }, you have already linked your accounts!` );
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 = 'DISCORD-VERIFICATION-' + this.randomString( 15, '23456789abcdefghijkmnpqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ' );
// Omitted the following characters due to possible confusion: 0, 1, l, o, I, O

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 )
.setFooter( 'Pending verification' )
.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';
}
}
75 changes: 75 additions & 0 deletions src/commands/WhoisCommand.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import { 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( origin: Message, args: string ): Promise<boolean> {
if ( !args.length ) {
return false;
}

let foundEmbed = false;

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

const logChannel = await DiscordUtil.getChannel( BotConfig.verification.verificationLogChannel );

if ( logChannel instanceof TextChannel ) {
const cachedMessages = logChannel.messages.cache;
try {
for ( const loop of cachedMessages ) {
const thisMessage = loop[1];
const content = thisMessage.embeds;
if ( content.length == 0 ) 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;

const embed = new MessageEmbed()
.setTitle( 'User information' );

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

embed.setDescription( `${ discordMember.user }'s Mojira account is ${ mojiraMember } ` )
.setFooter( origin.author.tag, origin.author.avatarURL() );
await origin.channel.send( embed );
} else {
if ( mojiraMember.split( '?name=' )[1].split( ')' )[0] != args ) continue;
foundEmbed = true;

embed.setDescription( `${ mojiraMember }'s Discord account is ${ discordId }` )
.setFooter( origin.author.tag, origin.author.avatarURL() );
await origin.channel.send( embed );
}
}

if ( !foundEmbed ) {
const embed = new MessageEmbed()
.setTitle( 'User information' )
.setDescription( `User ${ args } not found` )
.setFooter( origin.author.tag, origin.author.avatarURL() );
await origin.channel.send( embed );
return false;
}

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

public asString( args: string ): string {
return `!jira whois ${ args }`;
}
}
Loading