Repository: 8d2/tct-feedback-bot
Pull request #33: Display rules notice upon first time using contract create
Author: login: Blockmaster100
Labels: name: enhancement
description: New feature or request
Created: 2025-05-07T20:19:47Z | Merged: 2025-05-08T16:04:05Z

Upon the first time a user uses `/contract create`, they will now be granted with some rules that they must accept before they can create a feedback contract.

These rules were also added to the `/mod displaybotinfo` command.

Rules can be adjusted and added in the future if needed.


Diff:
diff --git a/commands/contractCommands.js b/commands/contractCommands.js
index 4413c28..30a0dd3 100644
--- a/commands/contractCommands.js
+++ b/commands/contractCommands.js
@@ -1,9 +1,10 @@
-const { SlashCommandBuilder, SlashCommandSubcommandBuilder, EmbedBuilder, Colors, MessageFlags, CommandInteractionOptionResolver, inlineCode, SlashCommandBooleanOption } = require("discord.js");
+const { SlashCommandBuilder, SlashCommandSubcommandBuilder, EmbedBuilder, Colors, MessageFlags, CommandInteractionOptionResolver, inlineCode, SlashCommandBooleanOption, ButtonBuilder, ButtonStyle, ActionRowBuilder } = require("discord.js");
 
 const { createContractMessage } = require("../handlers/contract");
 const { handleSubcommandExecute } = require("../handlers/commands.js")
 const contractMethods = require("../helpers/contractMethods.js");
 const userMethods = require("../helpers/userMethods.js");
+const messageMethods = require("../helpers/messageMethods.js")
 const { getFeedbackChannelId } = require("../helpers/settingsMethods.js");
 const constants = require("../helpers/constants.js")
 
@@ -24,6 +25,8 @@ const COMMAND_FUNCTIONS = {
 
         // Check if the interaction occurred within a feedback thread
         const feedbackThread = await contractMethods.getFeedbackThreadFromInteraction(interaction);
+        // Check if user has read and accepted the rules
+        const acceptedRules = await userMethods.getRulesAccepted(interaction.user.id)
         if (!feedbackThread) {
             // Get the actual feedback thread ID to include in the error message
             const realFeedbackThread = await getFeedbackChannelId();
@@ -46,6 +49,44 @@ const COMMAND_FUNCTIONS = {
             await interaction.reply({embeds: [responseEmbed], flags: MessageFlags.Ephemeral});
             return false;
         }
+        // Check if user has read and accepted the rules
+        else if (!acceptedRules) {
+            const messages = await messageMethods.getPointsInfoDisplayMessages(interaction);
+             const rulesEmbed = new EmbedBuilder()
+                .setDescription(messages[3])
+                .setColor(Colors.Orange);
+            const acceptButton = new ButtonBuilder()
+                .setCustomId("accept")
+                .setLabel("Accept")
+                .setStyle(ButtonStyle.Success)
+            const row = new ActionRowBuilder()
+                .addComponents(acceptButton)
+            
+            const response = await interaction.reply({embeds: [rulesEmbed], components: [row], flags: MessageFlags.Ephemeral, withResponse: true});
+            try {
+                // Await for the response with a time limit
+                const confirmation = await response.resource.message.awaitMessageComponent({ time: 600_000 });
+                if (confirmation.customId === "accept") {
+                    // Changes accepted_rules to true to stop this from triggering again
+                    await userMethods.setRulesAccepted(interaction.user.id);
+
+                    // Embed to use when under success
+                    const updatedResponseEmbed = new EmbedBuilder()
+                        .setTimestamp()
+                        .setColor(Colors.Green)
+                        .setDescription("## Rules accepted \n Run the `/contract create` command again to get started!")
+                    await confirmation.update({embeds: [updatedResponseEmbed], components: [], flags: MessageFlags.Ephemeral})
+                }
+            } catch {
+                // Embed to use when the interaction failed for whatever reason
+                const failedResponseEmbed = new EmbedBuilder()
+                        .setTimestamp()
+                        .setColor(Colors.Red)
+                        .setDescription("## Cancelled \n Rules acknowledgement cancelled, you probably timed out or an unknown error occured. Run `/contract create` again.")
+                await interaction.editReply({embeds: [failedResponseEmbed], components: [], flags: MessageFlags.Ephemeral});
+            }
+            return false;
+        }
         else {
             // Component creation has been outsourced to handlers </3
             // Pings the thread owner if they have allow pings on.
diff --git a/commands/modCommands.js b/commands/modCommands.js
index 3f2b723..959eea5 100644
--- a/commands/modCommands.js
+++ b/commands/modCommands.js
@@ -100,7 +100,10 @@ const COMMAND_FUNCTIONS = {
         const givingFeedbackEmbed = new EmbedBuilder()
             .setDescription(messages[2])
             .setColor(Colors.Aqua);
-        await handleSendMessage(feedbackChannel, {embeds: [titleEmbed, gettingFeedbackEmbed, givingFeedbackEmbed]}, messageEmbed)
+        const rulesEmbed = new EmbedBuilder()
+            .setDescription(messages[3])
+            .setColor(Colors.Orange);
+        await handleSendMessage(feedbackChannel, {embeds: [titleEmbed, gettingFeedbackEmbed, givingFeedbackEmbed, rulesEmbed]}, messageEmbed)
         return false;
     }
 };
diff --git a/database.sqlite b/database.sqlite
index a5c0ec4..f12ba9f 100644
Binary files a/database.sqlite and b/database.sqlite differ
diff --git a/helpers/messageMethods.js b/helpers/messageMethods.js
index 9b6c623..256ef57 100644
--- a/helpers/messageMethods.js
+++ b/helpers/messageMethods.js
@@ -54,10 +54,16 @@ async function getPointsInfoDisplayMessages(interaction) {
 
         "## Giving Feedback:\n" +
         "After giving a tower feedback, run the `/contract create` command in the thread. The builder will then rate your feedback, which will give you *feedback points*:\n" +
-        `${getRatingPointsMessage()}\n` +
-        "*Please don't abuse this contract command, or you will be blocked from using it and from earning feedback points.*\n\n" +
+        `${getRatingPointsMessage()}\n\n` +
         "**If you get enough feedback points, you can earn exclusive roles:**\n" +
-        `${await getRoleRequirementMessage(interaction)}\n\n` +
+        `${await getRoleRequirementMessage(interaction)}`,
+
+        "## Rules:\n" +
+        "This bot is a powerful tool for giving and receiving feedback and as such, we have the power to punish those who misuse it. Along with common sense, here is a list of DONTS:\n" +
+        "- DONT submit empty feedback\n" +
+        "- DONT be unfair / bias when feedbacking\n" +
+        "- DONT offer bribes (robux, feedback, etc) in return for positive feedback\n" +
+        "**If you are found to be breaking these rules or abusing the bot in any other way, we will block you from using the bot, denying your ability to make feedback contracts and receive feedback points.**\n\n" +
         "*Happy feedbacking!*"
     ];
 }
diff --git a/helpers/userMethods.js b/helpers/userMethods.js
index ebddd24..85bd19f 100644
--- a/helpers/userMethods.js
+++ b/helpers/userMethods.js
@@ -136,14 +136,31 @@ async function setAllowPings(id, allow) {
     return setAllowPingsFromUser(user, allow);
 }
 
+/**
+ * Handles the user reading and accepting the rules, as well as checks for if they have done so.
+ */
+async function setRulesAccepted(id) {
+    // Since this is a 1 use function, there is no need for a fromUser version
+    const user = await getOrCreateUserInfo(id);
+    user.accepted_rules = true;
+    user.save()
+}
+
+async function getRulesAccepted(id) {
+    const user = await getOrCreateUserInfo(id);
+    return user ? user.accepted_rules : false;
+}
+
 module.exports = {
     getUserInfo,
     getPoints,
     getIsBlocked,
     getAllowPings,
+    getRulesAccepted,
     setPoints,
     setIsBlocked,
     setAllowPings,
+    setRulesAccepted,
 
     /**
      * Initialize users collection from database.
diff --git a/models/Users.js b/models/Users.js
index a16526c..160950f 100644
--- a/models/Users.js
+++ b/models/Users.js
@@ -1,24 +1,32 @@
 // Database definition for Users
 // Each user is stored by user id in userMethods
 
+const { dataTypes } = require("../dbDefinition");
+
 module.exports = (sequelize, DataTypes) => {
 	return sequelize.define('users', {
 		user_id: {
             // User id.
 			type: DataTypes.STRING,
-			primaryKey: true,
+			primaryKey: true
 		},
+        accepted_rules: {
+            // If user has read and accepted the rules.
+            type: dataTypes.BOOLEAN,
+            defaultValue: false,
+            allowNull: false
+        },
         is_blocked: {
             // If user is blocked from making feedback contracts.
             type: DataTypes.BOOLEAN,
             defaultValue: false,
-            allowNull: false,
+            allowNull: false
         },
         allow_pings: {
             // If the user will receive a ping on contract creation in their thread.
             type: DataTypes.BOOLEAN,
             defaultValue: true,
-            allowNull: false,
+            allowNull: false
         },
         feedback_points: {
             // How many feedback points user has.


Reviews:
user: login: frosster
body: LGTM, just some small formatting errors
state: APPROVED
author_association: COLLABORATOR
submitted_at: 2025-05-08T15:56:10Z

Review comments:
diff_hunk: @@ -54,10 +54,16 @@ async function getPointsInfoDisplayMessages(interaction) {
 
         "## Giving Feedback:\n" +
         "After giving a tower feedback, run the `/contract create` command in the thread. The builder will then rate your feedback, which will give you *feedback points*:\n" +
-        `${getRatingPointsMessage()}\n` +
-        "*Please don't abuse this contract command, or you will be blocked from using it and from earning feedback points.*\n\n" +
+        `${getRatingPointsMessage()}\n\n` +
         "**If you get enough feedback points, you can earn exclusive roles:**\n" +
-        `${await getRoleRequirementMessage(interaction)}\n\n` +
+        `${await getRoleRequirementMessage(interaction)}`,
+
+        "## Rules:\n" +
+        "This bot is a powerful tool for giving and receiving feedback and as such, we have the power to punish those who misuse it. Along with common sense, here is a list of DONTS:\n" +
+        "- DONT submit empty feedback\n" +
+        "- DONT be unfair / bias when feedbacking\n" +
path: helpers/messageMethods.js
user: login: frosster
body: Change to `DONT be unfair/biased when feedbacking\n`
created_at: 2025-05-08T15:35:51Z
author_association: COLLABORATOR
diff_hunk: @@ -136,14 +136,31 @@ async function setAllowPings(id, allow) {
     return setAllowPingsFromUser(user, allow);
 }
 
+/**
+ * Handles the user reading and accepting the rules, as well as checks for if they have done so.
+ */
path: helpers/userMethods.js
user: login: frosster
body: Add documentation for the `id` parameter
created_at: 2025-05-08T15:36:36Z
author_association: COLLABORATOR
diff_hunk: @@ -136,14 +136,31 @@ async function setAllowPings(id, allow) {
     return setAllowPingsFromUser(user, allow);
 }
 
+/**
+ * Handles the user reading and accepting the rules, as well as checks for if they have done so.
+ */
+async function setRulesAccepted(id) {
+    // Since this is a 1 use function, there is no need for a fromUser version
+    const user = await getOrCreateUserInfo(id);
+    user.accepted_rules = true;
+    user.save()
+}
+
+async function getRulesAccepted(id) {
path: helpers/userMethods.js
user: login: frosster
body: Add missing documentation
created_at: 2025-05-08T15:36:47Z
author_association: COLLABORATOR
diff_hunk: @@ -46,6 +49,44 @@ const COMMAND_FUNCTIONS = {
             await interaction.reply({embeds: [responseEmbed], flags: MessageFlags.Ephemeral});
             return false;
         }
+        // Check if user has read and accepted the rules
+        else if (!acceptedRules) {
+            const messages = await messageMethods.getPointsInfoDisplayMessages(interaction);
+             const rulesEmbed = new EmbedBuilder()
path: commands/contractCommands.js
user: login: frosster
body: Small indent misalignment
created_at: 2025-05-08T15:39:16Z
author_association: COLLABORATOR
diff_hunk: @@ -1,24 +1,32 @@
 // Database definition for Users
 // Each user is stored by user id in userMethods
 
+const { dataTypes } = require("../dbDefinition");
+
 module.exports = (sequelize, DataTypes) => {
 	return sequelize.define('users', {
 		user_id: {
             // User id.
 			type: DataTypes.STRING,
-			primaryKey: true,
+			primaryKey: true
 		},
+        accepted_rules: {
+            // If user has read and accepted the rules.
+            type: dataTypes.BOOLEAN,
path: models/Users.js
user: login: frosster
body: I know that `dataTypes` is just a different variable for `DataTypes` but I would probably remove the `dataTypes` require and switch this to a capital `DataTypes`.
created_at: 2025-05-08T15:43:28Z
author_association: COLLABORATOR
diff_hunk: @@ -46,6 +49,44 @@ const COMMAND_FUNCTIONS = {
             await interaction.reply({embeds: [responseEmbed], flags: MessageFlags.Ephemeral});
             return false;
         }
+        // Check if user has read and accepted the rules
+        else if (!acceptedRules) {
+            const messages = await messageMethods.getPointsInfoDisplayMessages(interaction);
+             const rulesEmbed = new EmbedBuilder()
+                .setDescription(messages[3])
+                .setColor(Colors.Orange);
+            const acceptButton = new ButtonBuilder()
+                .setCustomId("accept")
+                .setLabel("Accept")
+                .setStyle(ButtonStyle.Success)
+            const row = new ActionRowBuilder()
+                .addComponents(acceptButton)
+            
+            const response = await interaction.reply({embeds: [rulesEmbed], components: [row], flags: MessageFlags.Ephemeral, withResponse: true});
+            try {
+                // Await for the response with a time limit
+                const confirmation = await response.resource.message.awaitMessageComponent({ time: 600_000 });
+                if (confirmation.customId === "accept") {
+                    // Changes accepted_rules to true to stop this from triggering again
+                    await userMethods.setRulesAccepted(interaction.user.id);
+
+                    // Embed to use when under success
+                    const updatedResponseEmbed = new EmbedBuilder()
+                        .setTimestamp()
+                        .setColor(Colors.Green)
+                        .setDescription("## Rules accepted \n Run the `/contract create` command again to get started!")
+                    await confirmation.update({embeds: [updatedResponseEmbed], components: [], flags: MessageFlags.Ephemeral})
+                }
+            } catch {
path: commands/contractCommands.js
user: login: frosster
body: I personally would put catch on a new line to avoid putting a key word after an ending brace on the same line
created_at: 2025-05-08T15:50:57Z
author_association: COLLABORATOR

PR comments:
user: login: frosster
created_at: 2025-05-08T16:03:53Z
body: NOTE: Blockmaster is fixing these issues in a different branch, so expect them to be fixed in a future PR. Merging
author_association: COLLABORATOR
user: login: Blockmaster100
created_at: 2025-05-08T16:26:15Z
body: These issues have now been fixed and will be present in a future PR.
author_association: COLLABORATOR