Skip to content

Commit

Permalink
GH-56: Cleanup usage of message to handle command
Browse files Browse the repository at this point in the history
  • Loading branch information
utarwyn committed Nov 10, 2021
1 parent 1f80c20 commit cef8ebb
Show file tree
Hide file tree
Showing 4 changed files with 99 additions and 85 deletions.
138 changes: 72 additions & 66 deletions src/bot/GameCommand.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { GuildMember, Message, TextChannel, User } from 'discord.js';
import TicTacToeBot from '@bot/TicTacToeBot';
import GameChannel from '@bot/channel/GameChannel';
import localize from '@config/localize';
import { GuildMember, Message, TextChannel, User } from 'discord.js';

/**
* Command to start a duel with someone else.
Expand Down Expand Up @@ -73,82 +72,107 @@ export default class GameCommand {
* Handles an incoming message.
*
* @param message discord.js message instance
* @param noTrigger true to bypass trigger checks
*/
public handle(message: Message): void {
public handleMessage(message: Message, noTrigger = false): void {
if (
this.trigger &&
message.member &&
!message.author.bot &&
message.channel.isText() &&
message.content.startsWith(this.trigger)
(noTrigger || (this.trigger && message.content.startsWith(this.trigger)))
) {
this.run(message);
const replying = this.run(
message.channel as TextChannel,
message.member,
message.mentions.members?.first()
);

if (replying) {
message.reply(replying).catch(console.error);
}
}
}

/**
* Executes the command behavior.
* Process game command and start a match based on the command context.
*
* @param message discord message object
* @param channel discord.js text channel object
* @param inviter member who invited someone to play
* @param invited member invited to the duel
* @returns replying message, can be null to do not send answer
* @private
*/
public run(message: Message): void {
const channel = this.bot.getorCreateGameChannel(message.channel as TextChannel);

if (this.canStartGame(message, channel)) {
const mentionned = message.mentions.members?.first();

if (mentionned) {
if (GameCommand.isUserReadyToPlay(mentionned, message)) {
channel!.sendDuelRequest(message, mentionned).catch(console.error);
public run(channel: TextChannel, inviter: GuildMember, invited?: GuildMember): string | null {
if (this.isChannelValid(channel)) {
const gameChannel = this.bot.getorCreateGameChannel(channel);

if (gameChannel && !gameChannel.gameRunning && this.isMemberAllowed(inviter)) {
if (invited) {
if (this.isInvitationValid(channel, inviter, invited)) {
gameChannel.sendDuelRequest(inviter, invited).catch(console.error);
} else {
return localize.__('duel.unknown-user');
}
} else {
message.reply(localize.__('duel.unknown-user')).catch(console.error);
gameChannel.createGame(inviter).catch(console.error);
}
} else {
channel!.createGame(message.member!).catch(console.error);
}
}

return null;
}

/**
* Checks if the command can be executed based on a specific message context.
* Checks if a discord.js channel is able to receive games.
*
* @param message discord.js message
* @param channel game channel where the game will be starte, can be null
* @param channel discord.js text channel object
* @returns true if channel allowed
* @private
*/
private canStartGame(message: Message, channel: GameChannel | null): boolean {
// Disable the command if the channel is not allowed
if (
this.allowedChannelIds.length > 0 &&
!this.allowedChannelIds.includes(message.channel.id)
) {
return false;
}

// Check if game channel created
if (channel == null) {
const name = (message.channel as TextChannel).name;
console.error(
`Cannot operate because of a lack of permissions in the channel #${name}`
);
return false;
}
private isChannelValid(channel: TextChannel): boolean {
return this.allowedChannelIds.length === 0 || this.allowedChannelIds.includes(channel.id);
}

// Disable the command if a game is running or member cooldown active
/**
* Checks if an invitation between two guild members is valid.
*
* @param channel discord.js channel object where invitation takes place
* @param inviter member who invited someone to enter a duel
* @param invited member invited to enter a duel
* @returns true if invited member can duel with the sender, false otherwise
* @private
*/
private isInvitationValid(
channel: TextChannel,
inviter: GuildMember,
invited: GuildMember
): boolean {
return (
!channel.gameRunning &&
message.member != null &&
this.isAllowedMemberByRole(message.member) &&
this.isAllowedUserByCooldown(message.author)
!invited.user.bot &&
inviter !== invited &&
invited.permissionsIn(channel).has('VIEW_CHANNEL')
);
}

/**
* Checks if the command can be executed for a specific member of the guild.
*
* @param member discord.js guild member object
* @returns true if the game can be started based on member permissions
* @private
*/
private isMemberAllowed(member: GuildMember): boolean {
return this.isMemberAllowedByRole(member) && this.isUserAllowedByCooldown(member.user);
}

/**
* Verifies if a member can run the command based on its roles.
*
* @param member discord.js member object
* @returns true if the member is allowed to start game in that guild
* @private
*/
private isAllowedMemberByRole(member: GuildMember): boolean {
private isMemberAllowedByRole(member: GuildMember): boolean {
return (
this.allowedRoleIds.length == 0 ||
member.permissions.has('ADMINISTRATOR') ||
Expand All @@ -160,9 +184,10 @@ export default class GameCommand {
* Verifies if an user can run the command based on its cooldown.
*
* @param author identifier of message author
* @returns true if the user do not have a valid cooldown in progress
* @private
*/
private isAllowedUserByCooldown(author: User): boolean {
private isUserAllowedByCooldown(author: User): boolean {
if (this.cooldown > 0) {
if ((this.memberCooldownEndTimes.get(author.id) ?? 0) > Date.now()) {
return false;
Expand All @@ -173,23 +198,4 @@ export default class GameCommand {

return true;
}

/**
* Retrieves the first valid member mentionned in a message.
* Should be a real person, who has right permissions and not the requester.
*
* @param invited member invited to enter a duel
* @param invitation message used to invite a member to enter a duel
* @return true if invited member can duel with the sender, false otherwise
* @private
* @static
*/
private static isUserReadyToPlay(invited: GuildMember, invitation: Message): boolean {
return (
invited &&
!invited.user.bot &&
invitation.member !== invited &&
invited.permissionsIn(invitation.channel).has('VIEW_CHANNEL')
);
}
}
9 changes: 6 additions & 3 deletions src/bot/TicTacToeBot.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { Client, Message, PermissionString, TextChannel } from 'discord.js';
import GameChannel from '@bot/channel/GameChannel';
import EventHandler from '@bot/EventHandler';
import GameCommand from '@bot/GameCommand';
import Config from '@config/Config';
import { Client, Message, PermissionString, TextChannel } from 'discord.js';

/**
* Manages all interactions with the Discord bot.
Expand Down Expand Up @@ -81,7 +81,7 @@ export default class TicTacToeBot {
* Attaches a new Discord client to the module by preparing command handing.
*/
public attachToClient(client: Client): void {
client.on('message', this.command.handle.bind(this.command));
client.on('message', this.command.handleMessage.bind(this.command));
}

/**
Expand All @@ -90,7 +90,7 @@ export default class TicTacToeBot {
* @param message Discord.js message object
*/
public handleMessage(message: Message): void {
this.command.run(message);
this.command.handleMessage(message, true);
}

/**
Expand All @@ -109,6 +109,9 @@ export default class TicTacToeBot {
this._channels.push(instance);
return instance;
} else {
console.error(
`Cannot operate because of a lack of permissions in the channel #${channel.name}`
);
return null;
}
}
Expand Down
10 changes: 5 additions & 5 deletions src/bot/channel/GameChannel.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { GuildMember, Message, TextChannel } from 'discord.js';
import GameEntity from '@bot/channel/GameEntity';
import GameBoardMessage from '@bot/gameboard/GameBoardMessage';
import DuelRequestMessage from '@bot/request/DuelRequestMessage';
import TicTacToeBot from '@bot/TicTacToeBot';
import localize from '@config/localize';
import AI from '@tictactoe/AI';
import { GuildMember, TextChannel } from 'discord.js';

/**
* Manages a channel in which games can be played.
Expand Down Expand Up @@ -59,12 +59,12 @@ export default class GameChannel {
/**
* Sends a new duel request managed by the bot.
*
* @param original original message sent by the requester
* @param invited user invited to a duel
* @param inviter member who has invited to a duel
* @param invited member invited to a duel
*/
public async sendDuelRequest(original: Message, invited: GuildMember): Promise<void> {
public async sendDuelRequest(inviter: GuildMember, invited: GuildMember): Promise<void> {
const expireTime = this.bot.configuration.requestExpireTime;
const message = new DuelRequestMessage(this, original, invited, expireTime);
const message = new DuelRequestMessage(this, inviter, invited, expireTime);
this.requests.push(message);
await message.send();
}
Expand Down
27 changes: 16 additions & 11 deletions src/bot/request/DuelRequestMessage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,11 @@ export default class DuelRequestMessage {
*/
private readonly channel: GameChannel;
/**
* Message sent by the user who wants to start a duel.
* Member who has created the invitation
*/
private readonly request: Message;
private readonly inviter: GuildMember;
/**
* Invited member in the same guild.
* Member who has been invited to play
*/
private readonly invited: GuildMember;
/**
Expand All @@ -40,13 +40,18 @@ export default class DuelRequestMessage {
* Constructs a new duel request based on a message.
*
* @param channel game channel object
* @param message request message object
* @param invited invited user object
* @param inviter inviter member object
* @param invited invited member object
* @param expireTime expiration time of the mesage
*/
constructor(channel: GameChannel, message: Message, invited: GuildMember, expireTime?: number) {
constructor(
channel: GameChannel,
inviter: GuildMember,
invited: GuildMember,
expireTime?: number
) {
this.channel = channel;
this.request = message;
this.inviter = inviter;
this.invited = invited;
this.expireTime = expireTime ?? 60;
}
Expand Down Expand Up @@ -86,7 +91,7 @@ export default class DuelRequestMessage {
await this.message.delete();
}
if (message) {
await this.request.channel.send(message);
await this.channel.channel.send(message);
}
}

Expand All @@ -99,7 +104,7 @@ export default class DuelRequestMessage {
collected: Collection<Snowflake, MessageReaction>
): Promise<void> {
if (collected.first()!.emoji.name === DuelRequestMessage.REACTIONS[0]) {
await this.channel.createGame(this.request.member!, this.invited);
await this.channel.createGame(this.inviter, this.invited);
} else {
await this.channel.closeDuelRequest(
this,
Expand All @@ -126,12 +131,12 @@ export default class DuelRequestMessage {
const content =
localize.__('duel.challenge', {
invited: this.invited.toString(),
initier: formatDiscordName(this.request.member?.displayName ?? '')
initier: formatDiscordName(this.inviter.displayName ?? '')
}) +
'\n' +
localize.__('duel.action');

return this.request.channel.send({
return this.channel.channel.send({
embed: {
color: '#2980b9',
title: localize.__('duel.title'),
Expand Down

0 comments on commit cef8ebb

Please sign in to comment.