Skip to content

Commit

Permalink
GH-56: Rework the whole interactions management
Browse files Browse the repository at this point in the history
  • Loading branch information
utarwyn committed Nov 20, 2021
1 parent 7580304 commit 729078f
Show file tree
Hide file tree
Showing 18 changed files with 692 additions and 577 deletions.
2 changes: 1 addition & 1 deletion src/__tests__/GameBoardBuilder.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import GameBoardBuilder from '@bot/gameboard/GameBoardBuilder';
import GameBoardBuilder from '@bot/entity/GameBoardBuilder';
import localize from '@i18n/localize';
import AI from '@tictactoe/AI';
import { Player } from '@tictactoe/Player';
Expand Down
166 changes: 25 additions & 141 deletions src/bot/GameCommand.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import TicTacToeBot from '@bot/TicTacToeBot';
import TextMessagingTunnel from '@bot/messaging/TextMessagingTunnel';
import GameStateManager from '@bot/state/GameStateManager';
import CommandConfig from '@config/CommandConfig';
import localize from '@i18n/localize';
import { GuildMember, Message, TextChannel, User } from 'discord.js';
import { GuildMember, Message } from 'discord.js';

/**
* Command to start a duel with someone else.
Expand All @@ -11,54 +12,24 @@ import { GuildMember, Message, TextChannel, User } from 'discord.js';
*/
export default class GameCommand {
/**
* Game bot handling object.
* Global game manager of the module.
* @private
*/
private readonly bot: TicTacToeBot;

/**
* Trigger of the command.
* @private
*/
private readonly trigger?: string;

/**
* Amount of seconds to wait after executing command.
* @private
*/
private readonly cooldown: number;

/**
* List of channel identifiers where the command can be used.
* @private
*/
private readonly allowedChannelIds: string[];

/**
* List of role identifiers that can use the command.
* @private
*/
private readonly allowedRoleIds: string[];

private readonly manager: GameStateManager;
/**
* Stores member cooldown end times.
* Configuration of the game command.
* @private
*/
private memberCooldownEndTimes: Map<string, number>;
private readonly config: CommandConfig;

/**
* Constructs the command to start a game.
*
* @param bot game client bot
* @param config custom configuration of the command
* @param manager game state manager
*/
constructor(bot: TicTacToeBot, config: CommandConfig) {
this.bot = bot;
this.trigger = config.command;
this.cooldown = config.requestCooldownTime ?? 0;
this.allowedChannelIds = config.allowedChannelIds ?? [];
this.allowedRoleIds = config.allowedRoleIds ?? [];
this.memberCooldownEndTimes = new Map();
constructor(manager: GameStateManager) {
this.manager = manager;
this.config = manager.bot.configuration;
}

/**
Expand All @@ -67,128 +38,41 @@ export default class GameCommand {
* @param message discord.js message instance
* @param noTrigger true to bypass trigger checks
*/
public handleMessage(message: Message, noTrigger = false): void {
public async handleMessage(message: Message, noTrigger = false): Promise<void> {
if (
message.member &&
!message.author.bot &&
message.channel.isText() &&
(noTrigger || (this.trigger && message.content.startsWith(this.trigger)))
(noTrigger || (this.config.command && message.content.startsWith(this.config.command)))
) {
const replying = this.run(
message.channel as TextChannel,
message.member,
message.mentions.members?.first()
);

if (replying) {
message.reply(replying).catch(console.error);
}
}
}
const tunnel = new TextMessagingTunnel(message);
const invited = message.mentions.members?.first();

/**
* Process game command and start a match based on the command context.
*
* @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(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');
}
if (invited) {
if (this.isInvitationValid(message, invited)) {
await this.manager.requestDuel(tunnel, invited);
} else {
gameChannel.createGame(inviter).catch(console.error);
await message.reply(localize.__('duel.unknown-user'));
}
} else {
await this.manager.createGame(tunnel);
}
}

return null;
}

/**
* Checks if a discord.js channel is able to receive games.
*
* @param channel discord.js text channel object
* @returns true if channel allowed
* @private
*/
private isChannelValid(channel: TextChannel): boolean {
return this.allowedChannelIds.length === 0 || this.allowedChannelIds.includes(channel.id);
}

/**
* 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 message discord.js message instance
* @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 {
private isInvitationValid(message: Message, invited: GuildMember): boolean {
return (
!invited.user.bot &&
inviter !== invited &&
invited.permissionsIn(channel).has('VIEW_CHANNEL')
message.member !== invited &&
invited.permissionsIn(message.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 isMemberAllowedByRole(member: GuildMember): boolean {
return (
this.allowedRoleIds.length == 0 ||
member.permissions.has('ADMINISTRATOR') ||
member.roles.cache.some(role => this.allowedRoleIds.includes(role.id))
);
}

/**
* 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 isUserAllowedByCooldown(author: User): boolean {
if (this.cooldown > 0) {
if ((this.memberCooldownEndTimes.get(author.id) ?? 0) > Date.now()) {
return false;
} else {
this.memberCooldownEndTimes.set(author.id, Date.now() + this.cooldown * 1000);
}
}

return true;
}
}
62 changes: 7 additions & 55 deletions src/bot/TicTacToeBot.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import GameChannel from '@bot/channel/GameChannel';
import EventHandler from '@bot/EventHandler';
import GameCommand from '@bot/GameCommand';
import GameStateManager from '@bot/state/GameStateManager';
import Config from '@config/Config';
import { Client, Message, PermissionString, TextChannel } from 'discord.js';
import { Client, Message } from 'discord.js';

/**
* Manages all interactions with the Discord bot.
Expand All @@ -11,18 +11,6 @@ import { Client, Message, PermissionString, TextChannel } from 'discord.js';
* @since 2.0.0
*/
export default class TicTacToeBot {
/**
* List with all permissions that the bot needs to work properly.
* @private
*/
private static readonly PERM_LIST: PermissionString[] = [
'ADD_REACTIONS',
'MANAGE_MESSAGES',
'READ_MESSAGE_HISTORY',
'SEND_MESSAGES',
'VIEW_CHANNEL'
];

/**
* Game configuration object
* @private
Expand All @@ -38,11 +26,6 @@ export default class TicTacToeBot {
* @private
*/
private readonly command: GameCommand;
/**
* Collection with all channels in which games are handled.
* @private
*/
private _channels: Array<GameChannel>;

/**
* Constructs the Discord bot interaction object.
Expand All @@ -53,8 +36,7 @@ export default class TicTacToeBot {
constructor(configuration: Config, eventHandler: EventHandler) {
this._configuration = configuration;
this._eventHandler = eventHandler;
this._channels = [];
this.command = new GameCommand(this, configuration);
this.command = new GameCommand(new GameStateManager(this));
}

/**
Expand All @@ -72,7 +54,10 @@ export default class TicTacToeBot {
}

/**
* Attaches a new Discord client to the module by preparing command handing.
* Attaches a new Discord client
* to the module by preparing command handing.
*
* @param client discord.js client obbject
*/
public attachToClient(client: Client): void {
client.on('message', this.command.handleMessage.bind(this.command));
Expand All @@ -86,37 +71,4 @@ export default class TicTacToeBot {
public handleMessage(message: Message): void {
this.command.handleMessage(message, true);
}

/**
* Retrieves a game channel from the Discord object.
* Creates a new game channel innstance if not found in the cache.
*
* @param channel parent Discord channel object
* @return created game channel, null if bot does not have proper permissions
*/
public getorCreateGameChannel(channel: TextChannel): GameChannel | null {
const found = this._channels.find(gameChannel => gameChannel.channel === channel);
if (found) {
return found;
} else if (TicTacToeBot.hasPermissionsInChannel(channel)) {
const instance = new GameChannel(this, channel);
this._channels.push(instance);
return instance;
} else {
console.error(
`Cannot operate because of a lack of permissions in the channel #${channel.name}`
);
return null;
}
}

/**
* Checks if bot has permissions to operate in a specific channel.
*
* @param channel discord.js text channel object
* @return true if bot got all permissions, false otherwise
*/
private static hasPermissionsInChannel(channel: TextChannel): boolean {
return channel.guild.me?.permissionsIn(channel)?.has(TicTacToeBot.PERM_LIST) ?? false;
}
}

0 comments on commit 729078f

Please sign in to comment.