Skip to content

Commit

Permalink
feat: role pings for dungeons and afk-checks (#273)
Browse files Browse the repository at this point in the history
* feat: role pings for dungeons and afk-checks

* feat: configchannel has a channel to handle assign

* feat: update roleping message when config update

* feat: role pings for dungeons and afk-checks

* feat: configchannel has a channel to handle assign

* fix: custom dungeons, dynamic updates, config embed, error

* fix: undefined access

---------

Co-authored-by: ewang2002 <37031713+ewang2002@users.noreply.github.com>
  • Loading branch information
nyapat and ewang2002 authored Oct 1, 2023
1 parent e5665a3 commit 80e425c
Show file tree
Hide file tree
Showing 9 changed files with 294 additions and 23 deletions.
149 changes: 145 additions & 4 deletions src/commands/config/ConfigChannels.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import {
IBaseDatabaseEntryInfo,
IConfigCommand
} from "./common/ConfigCommon";
import { Guild, Message, MessageButton, MessageEmbed, TextChannel } from "discord.js";
import { ButtonInteraction, Client, Guild, GuildMember, Message, MessageButton, MessageEmbed, TextChannel } from "discord.js";
import { AdvancedCollector } from "../../utilities/collectors/AdvancedCollector";
import { StringBuilder } from "../../utilities/StringBuilder";
import { GuildFgrUtilities } from "../../utilities/fetch-get-request/GuildFgrUtilities";
Expand All @@ -21,6 +21,7 @@ import { MainLogType, SectionLogType } from "../../definitions/Types";
import { ButtonConstants } from "../../constants/ButtonConstants";
import { MessageUtilities } from "../../utilities/MessageUtilities";
import getCachedChannel = GuildFgrUtilities.getCachedChannel;
import { DUNGEON_DATA } from "../../constants/dungeons/DungeonData";

enum ChannelCategoryType {
Raiding,
Expand Down Expand Up @@ -216,6 +217,19 @@ export class ConfigChannels extends BaseCommand implements IConfigCommand {
if (!section.isMainSection) throw new Error("storage channel is main-only.");
return guildDoc.channels.storageChannelId;
}
},
{
name: "Role Ping Channel",
description: "This channel will allow users to assign roles to be pinged when a headcount or AFK-check"
+ " for a specific dungeon begins.",
guildDocPath: "channels.rolePingChannelId",
sectionPath: "",
channelType: ChannelCategoryType.Other,
configTypeOrInstructions: ConfigType.Channel,
getCurrentValue: (guildDoc, section) => {
if (!section.isMainSection) throw new Error("role ping channel is main-only");
return guildDoc.channels.rolePingChannelId;
}
}
];

Expand Down Expand Up @@ -468,8 +482,10 @@ export class ConfigChannels extends BaseCommand implements IConfigCommand {

if (displayFilter & DisplayFilter.Other) {
const botUpdatesChan = getCachedChannel<TextChannel>(guild, guildDoc.channels.botUpdatesChannelId);
const rolePingChan = getCachedChannel<TextChannel>(guild, guildDoc.channels.rolePingChannelId);
currentConfiguration.append("__**Other Channels**__").appendLine()
.append(`⇒ Bot Updates Channel: ${botUpdatesChan ?? ConfigChannels.NA}`).appendLine();
.append(`⇒ Bot Updates Channel: ${botUpdatesChan ?? ConfigChannels.NA}`).appendLine()
.append(`⇒ Role Ping Channel: ${rolePingChan ?? ConfigChannels.NA}`).appendLine();
}
}

Expand Down Expand Up @@ -725,7 +741,7 @@ export class ConfigChannels extends BaseCommand implements IConfigCommand {
section,
botMsg,
ConfigChannels.CHANNEL_MONGO.filter(x => x.channelType === ChannelCategoryType.Raiding
&& section.isMainSection ? true : !!x.sectionPath),
&& section.isMainSection ? true : !!x.sectionPath),
"Raids"
);
break;
Expand All @@ -737,7 +753,7 @@ export class ConfigChannels extends BaseCommand implements IConfigCommand {
botMsg,
ConfigChannels.CHANNEL_MONGO
.filter(x => x.channelType === ChannelCategoryType.Verification
&& section.isMainSection ? true : !!x.sectionPath),
&& section.isMainSection ? true : !!x.sectionPath),
"Verification"
);
break;
Expand All @@ -760,6 +776,123 @@ export class ConfigChannels extends BaseCommand implements IConfigCommand {
}
}

/**
* Pushes new roles to the role-pings channel. Creates a message from saved ping-roles in each dungeon config.
* Giving roles is not handled here.
*
* @param {Client} client The bot client. Necessary for getting channels from the client cache
* @param {IGuildInfo} guildDoc The guild document. Necessary for getting each dungeon's roles
*/
public static async createNewRolePingMessage(client: Client, guildDoc: IGuildInfo): Promise<void> {
// store dungeon name, emoji react to make buttons look fancy
const dungeonRoles = new Map<string, Record<string, unknown>>();

for (const dungeon of guildDoc.properties.dungeonOverride) {
if (dungeon.mentionRoles && dungeon.mentionRoles.length > 0) {
const dungeonBase = DUNGEON_DATA.find(d => d.codeName === dungeon.codeName);
if (dungeonBase) {
const dungeonName = dungeonBase.dungeonName;
const dungeonEmoji = dungeonBase.portalEmojiId;

dungeonRoles.set(dungeonName, { dungeonEmoji, codeName: dungeon.codeName });
}
}
}

for (const dungeon of guildDoc.properties.customDungeons) {
if (dungeon.mentionRoles && dungeon.mentionRoles.length > 0) {
dungeonRoles.set(dungeon.dungeonName, { dungeonEmoji: dungeon.portalEmojiId, codeName: dungeon.codeName });
}
}

const buttons: MessageButton[] = [];
dungeonRoles.forEach((dungeonInfo, name) => {
const button = new MessageButton()
.setLabel(name)
// do not use _, it's used in dungeon codenames
.setCustomId(`ping-${dungeonInfo.codeName as string}`)
.setStyle("PRIMARY")
.setEmoji(dungeonInfo.dungeonEmoji as string);

buttons.push(button);
});

const rows = AdvancedCollector.getActionRowsFromComponents(buttons);
const embed = new MessageEmbed()
.setTitle("Assign Role Pings")
.setDescription("Click a button **to be pinged for the corresponding dungeon**."
+ " This role will be pinged for __every headcount and AFK-check__.");

const roleChannel = client.channels.cache.get(guildDoc.channels.rolePingChannelId) as TextChannel;
let roleMessage = await GuildFgrUtilities.fetchMessage(roleChannel, guildDoc.properties.rolePingMessageId);
if (roleMessage) {
roleMessage = await roleChannel.messages.edit(guildDoc.properties.rolePingMessageId, {
embeds: [embed],
components: rows
});
} else {
roleMessage = await roleChannel.send({
embeds: [embed],
components: rows
});
}

await MongoManager.updateAndFetchGuildDoc({
guildId: roleChannel.guildId,
}, {
$set: {
"properties.rolePingMessageId": roleMessage.id,
}
});
}

/**
* Handles the role ping message button interactions.
*
* @param {ButtonInteraction} interaction The button interaction
* @param {IGuildInfo} guildDoc The guild document, necessary to obtain override information
*/
public static async handleRolePingInteraction(interaction: ButtonInteraction, guildDoc: IGuildInfo): Promise<void> {
const codeNameToCheck = interaction.customId.split("-")[1];

if (!codeNameToCheck) return;
// we know this is a GuildMember -- we do not use http only interactions
const member = interaction.member as GuildMember;
// get the dungeon override data
const dungeonBase = guildDoc.properties.dungeonOverride.find(d => d.codeName === codeNameToCheck)
?? guildDoc.properties.customDungeons.find(d => d.codeName === codeNameToCheck);

if (dungeonBase) {
// get roles that are associated with this dungeon
const roles = dungeonBase.mentionRoles;

// check if the member already has the roles
const hasRoles = member.roles.cache.hasAll(...roles);
if (hasRoles && roles.length > 0) {
// remove them
member.roles.remove(roles);

return interaction.reply({
content: "Removed the ping roles for that dungeon from you.",
ephemeral: true
});
} else if (!hasRoles && roles.length > 0) {
// add them
member.roles.add(roles);

return interaction.reply({
content: "Gave the ping roles for that dungeon to you.",
ephemeral: true
});
}
}

return interaction.reply({
content: "How did you get here? There are no roles to ping for that dungeon.",
ephemeral: true,
});
}

/**
* Edits the database entries. This is the function that is responsible for editing the database.
* @param {ICommandContext} ctx The command context.
Expand Down Expand Up @@ -833,6 +966,10 @@ export class ConfigChannels extends BaseCommand implements IConfigCommand {
: entries[selected].sectionPath;

if (result instanceof TextChannel) {
// sorry, this is lazy, but it's the only channel that i want to have an action performed
// when the value changes.
const previousRolePingChannel = ctx.guildDoc?.channels.rolePingChannelId;

ctx.guildDoc = await MongoManager.updateAndFetchGuildDoc(query, {
$set: {
// hacky fix since the possible properties that our query has
Expand All @@ -843,6 +980,10 @@ export class ConfigChannels extends BaseCommand implements IConfigCommand {
});
section = MongoManager.getAllSections(ctx.guildDoc!)
.find(x => x.uniqueIdentifier === section.uniqueIdentifier)!;

if (previousRolePingChannel !== ctx.guildDoc?.channels.rolePingChannelId) {
ConfigChannels.createNewRolePingMessage(ctx.channel.client, ctx.guildDoc!);
}
continue;
}

Expand Down
59 changes: 55 additions & 4 deletions src/commands/config/ConfigDungeons.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ import { DungeonUtilities } from "../../utilities/DungeonUtilities";
import { DEFAULT_MODIFIERS, DUNGEON_MODIFIERS } from "../../constants/dungeons/DungeonModifiers";
import { ButtonConstants } from "../../constants/ButtonConstants";
import { MessageUtilities } from "../../utilities/MessageUtilities";
import { ConfigChannels } from "./ConfigChannels";

enum ValidatorResult {
// Success = ValidationReturnType#res is not null
Expand Down Expand Up @@ -176,7 +177,8 @@ export class ConfigDungeons extends BaseCommand {
return dgnOverride.nitroEarlyLocationLimit === -1
&& dgnOverride.vcLimit === -1
&& dgnOverride.pointCost === 0
&& dgnOverride.roleRequirement.length === 0;
&& dgnOverride.roleRequirement.length === 0
&& dgnOverride.mentionRoles.length === 0;
}

/**
Expand Down Expand Up @@ -210,7 +212,8 @@ export class ConfigDungeons extends BaseCommand {
roleRequirement: [],
logFor: null,
allowedModifiers: DEFAULT_MODIFIERS.map(x => x.modifierId),
locationToProgress: dgn.locationToProgress
locationToProgress: dgn.locationToProgress,
mentionRoles: [],
} as ICustomDungeonInfo;
}

Expand Down Expand Up @@ -422,7 +425,8 @@ export class ConfigDungeons extends BaseCommand {
pointCost: 0,
roleRequirement: [],
allowedModifiers: DEFAULT_MODIFIERS.map(x => x.modifierId),
locationToProgress: res.locationToProgress
locationToProgress: res.locationToProgress,
mentionRoles: []
} as IDungeonOverrideInfo));
return;
}
Expand Down Expand Up @@ -629,7 +633,8 @@ export class ConfigDungeons extends BaseCommand {
roleRequirement: [],
vcLimit: -1,
allowedModifiers: DEFAULT_MODIFIERS.map(x => x.modifierId),
locationToProgress: false
locationToProgress: false,
mentionRoles: [],
};

const embed = new MessageEmbed();
Expand All @@ -650,6 +655,10 @@ export class ConfigDungeons extends BaseCommand {
.setLabel("Points to Enter")
.setCustomId("points_enter")
.setStyle("PRIMARY");
const mentionRolesButton = new MessageButton()
.setLabel("Roles to Mention")
.setCustomId("mention_roles")
.setStyle("PRIMARY");
const nitroLimitButton = new MessageButton()
.setLabel("Nitro Limit")
.setCustomId("nitro_limit")
Expand Down Expand Up @@ -731,6 +740,7 @@ export class ConfigDungeons extends BaseCommand {

buttons.push(
pointsToEnterButton,
mentionRolesButton,
nitroLimitButton,
vcLimitButton,
roleReqButton,
Expand Down Expand Up @@ -837,6 +847,10 @@ export class ConfigDungeons extends BaseCommand {
"Click on the `Points to Enter` button to set how many points a user needs in order to automatically"
+ " join the VC and gain early location. This is currently set to: "
+ StringUtil.codifyString(ptCostStr)
).addField(
"Mention Roles",
"Roles to mention. Click on the button to edit the roles mentioned when a headcount or raid for this"
+ " dungeon starts."
).addField(
"Number of Nitro Early Location",
"Click on the `Nitro Limit` button to set how many people can join the VC and gain early"
Expand Down Expand Up @@ -903,7 +917,9 @@ export class ConfigDungeons extends BaseCommand {
return;
}
case ButtonConstants.SAVE_ID: {
let oldDungeonData: IDungeonOverrideInfo | undefined;
if (dungeon) {
oldDungeonData = ctx.guildDoc?.properties.dungeonOverride.find(x => x.codeName === cDungeon.codeName);
ctx.guildDoc = await MongoManager.updateAndFetchGuildDoc({ guildId: ctx.guild!.id }, {
$pull: {
[operationOnStr]: {
Expand All @@ -925,6 +941,12 @@ export class ConfigDungeons extends BaseCommand {
}
});

const oldRoles = oldDungeonData?.mentionRoles?.every(item => cDungeon.mentionRoles.includes(item));
const newRoles = cDungeon.mentionRoles?.every(item => (oldDungeonData as IDungeonOverrideInfo).mentionRoles.includes(item));
if (oldRoles && newRoles) {
ConfigChannels.createNewRolePingMessage(ctx.channel.client, ctx.guildDoc!);
}

await this.mainMenu(ctx, botMsg);
return;
}
Expand Down Expand Up @@ -1211,6 +1233,35 @@ export class ConfigDungeons extends BaseCommand {
}
break;
}
case "mention_roles": {
if (!cDungeon.mentionRoles) cDungeon.mentionRoles = [];
const mentionRoles = await this.configSetting<Role>(
ctx,
botMsg,
cDungeon.mentionRoles.map(x => GuildFgrUtilities.getCachedRole(ctx.guild!, x))
.filter(x => !!x) as Role[],
{
nameOfPrompt: "Roles to mention",
descOfPrompt: "Pick the roles that will be mentioned in a headcount or AFK check",
expectedType: "Role Mention or ID",
itemName: "Role",
embedDescResolver: input => `Role ID: ${input.id}`,
embedTitleResolver: input => input.name,
validator: msg => {
const role = ParseUtilities.parseRole(msg);
return role ? role : null;
}
}
);

if (!mentionRoles) {
await this.dispose(ctx, botMsg);
return;
}

cDungeon.mentionRoles = mentionRoles.map(x => x.id);
break;
}
case "nitro_limit": {
const res = await this.askInput<number>(ctx, botMsg, {
currentValue: nitroEarlyStr,
Expand Down
16 changes: 14 additions & 2 deletions src/definitions/DungeonRaidInterfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,13 +98,20 @@ export interface IDungeonOverrideInfo {
* @type {string[]}
*/
allowedModifiers: string[];

/**
* Whether or not location is required to progress the afk check.
*
* @type {boolean}
*/
locationToProgress?: boolean;

/**
* A list of roles to ping when a raid starts. These are stored as snowflakes in an array.
*
* @type {string[]}
*/
mentionRoles: string[];
}

/**
Expand Down Expand Up @@ -317,6 +324,11 @@ export interface ICustomDungeonInfo extends IDungeonInfo {
* @type {string | null}
*/
logFor: string | null;

/**
* A list of roles to ping when a raid starts. These are stored as snowflakes in an array.
*/
mentionRoles: string[];
}

/**
Expand Down Expand Up @@ -523,7 +535,7 @@ export interface IAfkCheckProperties {
* @type {boolean}
*/
createLogChannel: boolean;

/**
* Default position for the voice channel to avoid confusing staff.
*
Expand Down
Loading

0 comments on commit 80e425c

Please sign in to comment.