Skip to content

Commit

Permalink
Add emoji-based role management
Browse files Browse the repository at this point in the history
Based off this discussion in #server-suggestions:
https://discord.com/channels/782201995011817493/793224619012128779/819308969460891648
Which concerns adding channels hidden behind roles and a method
through which to assign those roles

This feature will allow any moderator to create a post (possibly
in the welcome channel) which can be used to auto-assign roles
based on which reactions a user clicks

Example usage:
Making a post like this:
```
!role message
📖: @ReadingRole - For people who want to view the reading channel
✍🏾: @WritingRole - For people who want to view the writing channel
👂: @ListeningRole - For people who want to view the listening channel
```
Would result in the bot auto-adding those 📖, ✍🏾 and 👂 reactions to
the message.
Any time a user clicks one of the reactions they are assigned to
the corresponding role by the bot.
They may also remove their reaction to be unnassigned from that role

The usage rules currently:
- The message must start with "!role message"
- The reaction must be part of the standard set (no custom server
emojis)
- The sequence to form a role/reaction pair is:
{Emoji}{colon}{Role mention}
- Any line that contains the sequence is picked up and parsed
  - The line may include additional details after that sequence
  - Additional lines may be included that don't contain the
sequence (these are ignored)
  - The message can be edited after creation to have none of these
details and will still function as a role assignation message

Tables added to the database:
- RoleMessage
  - messageId
  - channelId
  - roleReactions (Array of objects:)
    - reactionName (emoji character code)
    - roleId (corresponding role for emoji)

Dev dependencies added to package.json:
- @babel/plugin-proposal-unicode-property-regex
  - Required to use Unicode property regexes
    - Allows us to use \p{Emoji} as a pattern to match any emoji

New events added to index.js:
- messageDelete
  - Fires whenever a message is deleted
    - Used to trigger the deletion of a role message from the database
  • Loading branch information
Fox-Islam committed Mar 11, 2021
1 parent fb0ed1b commit 8df80cb
Show file tree
Hide file tree
Showing 6 changed files with 184 additions and 3 deletions.
9 changes: 7 additions & 2 deletions .babelrc
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
{
"plugins": ["@babel/plugin-proposal-throw-expressions"],
"presets": ["@babel/preset-env"]
"plugins": [
"@babel/plugin-proposal-throw-expressions",
"@babel/plugin-proposal-unicode-property-regex"
],
"presets": [
"@babel/preset-env"
]
}
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
"@babel/core": "^7.12.10",
"@babel/node": "^7.12.10",
"@babel/plugin-proposal-throw-expressions": "^7.12.1",
"@babel/plugin-proposal-unicode-property-regex": "^7.12.13",
"@babel/preset-env": "^7.12.11",
"nodemon": "^2.0.7"
}
Expand Down
11 changes: 11 additions & 0 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ const { unPin50thMsg, getAllChannels, logMessageDate, ping } = require("./script
const { typingGame, typingGameListener, endTypingGame, gameExplanation } = require("./scripts/activities/games");
const { createStudySession, getUpcomingStudySessions, subscribeStudySession, unsubscribeStudySession, cancelConfirmationStudySession } = require("./scripts/activities/study-session");
const { loadMessageReaction } = require("./utils/cache");
const { handleRoleMessage, handleRoleMessageDelete, handleRolesOnReactionAdd, handleRolesOnReactionRemove } = require("./scripts/users/role-manager");
const runScheduler = require("./scheduler").default;
/* ------------------------------------------------------ */

Expand Down Expand Up @@ -127,9 +128,15 @@ client.on("message", (message) => {

// Find upcoming study sessions
if (text.startsWith("!upcoming study")) getUpcomingStudySessions(message);

handleRoleMessage(message);
});
/* --------------------------------------------------- */

client.on("messageDelete", function (message) {
handleRoleMessageDelete(message);
});

/* ________________ MAIN MESSAGE REACTION ADD LISTENER ________________ */

client.on("messageReactionAdd", async (messageReaction, user) => {
Expand All @@ -147,6 +154,8 @@ client.on("messageReactionAdd", async (messageReaction, user) => {

// Cancel study session
if (text.startsWith("!study") && emoji.name === "❌") cancelConfirmationStudySession(message, user);

handleRolesOnReactionAdd(user, message, emoji);
});
/* --------------------------------------------------- */

Expand All @@ -163,6 +172,8 @@ client.on("messageReactionRemove", async (messageReaction, user) => {

// Unsubscribe to a study session
if (text.startsWith("!study") && emoji.name === "⭐") unsubscribeStudySession(message, user);

handleRolesOnReactionRemove(user, message, emoji);
});
/* --------------------------------------------------- */

Expand Down
17 changes: 17 additions & 0 deletions src/models/RoleMessageSchema.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
const mongoose = require('mongoose');

const RoleMessageSchema = new mongoose.Schema({
messageId: { type: String, unique: true, required: true },
channelId: { type: String, unique: false, required: true },
roleReactions: [{
roleId: { type: String, unique: false, required: true },
reactionName: { type: String, unique: false, required: true },
}]
}, { collection: 'RoleMessage' }).index({
messageId: 1,
channelId: 1
}, {
unique: true
});

mongoose.model('RoleMessage', RoleMessageSchema);
3 changes: 2 additions & 1 deletion src/models/index.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
const StudySessionSchema = require('./StudySessionSchema');
const RoleMessageSchema = require('./RoleMessageSchema');

module.exports = { StudySessionSchema };
module.exports = { StudySessionSchema, RoleMessageSchema };
146 changes: 146 additions & 0 deletions src/scripts/users/role-manager.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
const RoleMessage = require("mongoose").model("RoleMessage");

const COMMAND = "!role message";

function handleRoleMessage(message) {
if (!isRoleMessage(message)) {
return;
}
if (!messageAuthorIsMod(message)) {
return;
}
const roleReactionStrings = getLinesWhichContainRoleReactionPairs(message);
if (!roleReactionStrings) {
return;
}
const roleReactions = createRoleReactionForDatabase(roleReactionStrings);
if (!roleReactions || roleReactions.length === 0) {
return;
}

createRoleReactionsForMessage(message, roleReactions);
}

function isRoleMessage(message) {
return message.content.toLowerCase().startsWith(COMMAND);
}

function messageAuthorIsMod(message) {
return Boolean(message.member.roles.cache.get(process.env.MODERATORS));
}

function getLinesWhichContainRoleReactionPairs(message) {
return message.content.split('\n').filter(function (line) {
const validRoleReactionRegex = /[\p{Emoji}]{1}.*:.*<@&\d+>/u;
return validRoleReactionRegex.test(line);
});
}

function createRoleReactionForDatabase(roleReactionStrings) {
return roleReactionStrings.map(function (roleReactionString) {
const reaction = roleReactionString.match(/[\p{Emoji}]{1}/u)[0];
const role = roleReactionString.split(':')[1].match(/\d+/g)[0];
return {
reactionName: reaction,
roleId: role
}
});
}

function createRoleReactionsForMessage(message, roleReactions) {
RoleMessage.create({
messageId: message.id,
channelId: message.channel.id,
roleReactions: roleReactions
}).then(function () {
addReactionsToMessage(message, roleReactions);
}).catch(function (error) {
console.log(error);
});
}

function addReactionsToMessage(message, roleReactions) {
roleReactions.forEach(function (roleReaction) {
message.react(roleReaction.reactionName).catch(function (error) {
console.log(error);
});
});
}

function handleRolesOnReactionAdd(user, message, emoji) {
RoleMessage.findOne({
messageId: message.id,
channelId: message.channel.id,
"roleReactions.reactionName": emoji.name
}, null, {}, function (error, roleMessageModel) {
if (error) {
console.log(error);
return;
}
if (!roleMessageModel) {
return;
}
const roleReaction = getRoleReactionFromModel(emoji, roleMessageModel);
addUserToRole(user, roleReaction);
}).lean();
}

function getRoleReactionFromModel(emoji, roleMessageModel) {
return roleMessageModel.roleReactions.find(function (roleReaction) {
return roleReaction.reactionName === emoji.name;
});
}

function addUserToRole(user, roleReaction) {
user.client.guilds.cache
.get(process.env.SERVER_ID).members.cache
.get(user.id).roles
.add(roleReaction.roleId)
.catch(function (error) {
console.log(error);
});
}

function handleRolesOnReactionRemove(user, message, emoji) {
RoleMessage.findOne({
messageId: message.id,
channelId: message.channel.id,
"roleReactions.reactionName": emoji.name
}, null, {}, function (error, roleMessageModel) {
if (error) {
console.log(error);
return;
}
if (!roleMessageModel) {
return;
}
const roleReaction = getRoleReactionFromModel(emoji, roleMessageModel);
removeUserFromRole(user, roleReaction);
}).lean();
}

function removeUserFromRole(user, roleReaction) {
user.client.guilds.cache
.get(process.env.SERVER_ID).members.cache
.get(user.id).roles
.remove(roleReaction.roleId)
.catch(function (error) {
console.log(error);
});
}

function handleRoleMessageDelete(message) {
RoleMessage.deleteOne({
messageId: message.id,
channelId: message.channel.id
}).catch(function (error) {
console.log(error);
});
}

export {
handleRolesOnReactionAdd,
handleRolesOnReactionRemove,
handleRoleMessage,
handleRoleMessageDelete
}

0 comments on commit 8df80cb

Please sign in to comment.