Skip to content

Commit

Permalink
Merge pull request #91 from berkingurcan/add-survey-leaderboard
Browse files Browse the repository at this point in the history
Add survey leaderboard
  • Loading branch information
berkingurcan authored Nov 30, 2024
2 parents f9005ce + ad02cd4 commit 7c17585
Show file tree
Hide file tree
Showing 17 changed files with 590 additions and 13 deletions.
16 changes: 16 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,22 @@ To start auto-posting the responses view. It is working which channel that you u

To stop auto-posting the responses view. For example, if you use it in forum channel, it will be activated in that channel.

### /gptsurvey create-survey-leaderboard

Creates leaderboard by calculating respond counts of the users per survey.

### /gptsurvey edit-survey-count

To edit survey respond count of users. Username is the discord username, count is + or - count. For example: "+4", "-1", "0"

### /gptsurvey view-survey-counts

Views survey counts of users which may be edited.

### /gptsurvey view-discord-survey-counts

View survey counts of users responded only by Discord.

## Docker Image

### Build It
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "discord-bot",
"type": "module",
"version": "0.3.8",
"version": "0.4.0",
"description": "",
"main": "index.js",
"scripts": {
Expand Down
19 changes: 19 additions & 0 deletions src/bot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,11 @@ import {
handleEditModal,
handleSetStatus,
handleSummary,
handleEditSurveyCount,
handleEditSurveyCountModal,
handleLeaderboard,
handleViewSurveyCounts,
handleViewDiscordSurveyCounts,
} from "@commands/index";

import {
Expand Down Expand Up @@ -105,6 +110,18 @@ process.on("uncaughtException", (error) => {
case "view":
await handleView(interaction, surveyName, redisClient);
break;
case "create-survey-leaderboard":
await handleLeaderboard(interaction, redisClient);
break;
case "view-survey-counts":
await handleViewSurveyCounts(interaction, redisClient);
break;
case "view-discord-survey-counts":
await handleViewDiscordSurveyCounts(interaction, redisClient);
break;
case "edit-survey-count":
await handleEditSurveyCount(interaction);
break;
case "summary":
const summaryType = options.getString("summarytype");
await handleSummary(
Expand Down Expand Up @@ -157,6 +174,8 @@ process.on("uncaughtException", (error) => {
}
} else if (interaction.customId.startsWith("respondModal")) {
await handleRespondModal(interaction, username, redisClient);
} else if (interaction.customId.startsWith("editSurveyCountModal")) {
await handleEditSurveyCountModal(interaction, username, redisClient);
} else if (interaction.customId.startsWith("deleteModal")) {
const [isDeleted, sn] = await handleDeleteModal(
interaction,
Expand Down
15 changes: 15 additions & 0 deletions src/commands/commandBuilder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,21 @@ export const command = new SlashCommandBuilder()
createStringOption("survey", "Survey name"),
]),
)
.addSubcommand(
createSubcommand("edit-survey-count", "Edit survey count of an user"),
)
.addSubcommand(
createSubcommand("create-survey-leaderboard", "Create survey leaderboard"),
)
.addSubcommand(
createSubcommand("view-survey-counts", "View survey counts of users"),
)
.addSubcommand(
createSubcommand(
"view-discord-survey-counts",
"View survey counts of users responded by Discord",
),
)
.addSubcommand(
createSubcommand(
"summary",
Expand Down
1 change: 1 addition & 0 deletions src/commands/handleDelete.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ export const deleteSurvey = async (redisClient: any, surveyName: any) => {
// Add this when branch changed to multiple respons thing:
// await redisClient.set(`survey:${surveyName}:fields`);
await redisClient.del(`survey:${surveyName}:username`);
await redisClient.del(`survey:${surveyName}:created-at`);
await redisClient.del(`survey:${surveyName}:last-edit-time`);
await redisClient.del(`survey:${surveyName}:last-summary-time`);
};
48 changes: 48 additions & 0 deletions src/commands/handleEditSurveyCount.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import {
ModalBuilder,
TextInputBuilder,
ActionRowBuilder,
TextInputStyle,
ChatInputCommandInteraction,
} from "discord.js";

const editSurveyCountModal = async (
interaction: ChatInputCommandInteraction,
) => {
const modal = new ModalBuilder()
.setCustomId(`editSurveyCountModal`)
.setTitle("Edit Survey Count");

const usernameInput = new TextInputBuilder()
.setCustomId("usernameInput")
.setLabel("USERNAME")
.setStyle(TextInputStyle.Short)
.setMaxLength(45)
.setRequired(true);

const countInput = new TextInputBuilder()
.setCustomId("countInput")
.setLabel("Edit Survey Count +/-NUMBER")
.setStyle(TextInputStyle.Short)
.setMaxLength(10)
.setRequired(true);

const pointsInput = new TextInputBuilder()
.setCustomId("pointsInput")
.setLabel("Edit Survey Points +/-POINTS")
.setStyle(TextInputStyle.Short)
.setMaxLength(10)
.setRequired(true);

const firstActionRow = new ActionRowBuilder().addComponents(usernameInput);
const secondActionRow = new ActionRowBuilder().addComponents(countInput);
const thirdActionRow = new ActionRowBuilder().addComponents(pointsInput);

modal.addComponents(firstActionRow, secondActionRow, thirdActionRow);

await interaction.showModal(modal);
};

export const handleEditSurveyCount = async (interaction: any) => {
await editSurveyCountModal(interaction);
};
99 changes: 99 additions & 0 deletions src/commands/handleLeaderboard.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import { ChatInputCommandInteraction, GuildMember } from "discord.js";

export const handleLeaderboard = async (
interaction: ChatInputCommandInteraction,
redisClient: any,
) => {
const userSurveyPoints = await redisClient.hGetAll("user:survey_points");

if (!userSurveyPoints || Object.keys(userSurveyPoints).length === 0) {
await interaction.reply({
content: "No data available for the leaderboard.",
ephemeral: true,
});
return;
}

const entries = Object.entries(userSurveyPoints)
.map(([username, pointsStr]) => ({
username,
points: parseInt(pointsStr, 10),
}))
.filter((entry) => entry.points > 0);

if (entries.length === 0) {
await interaction.reply({
content: "No users have earned survey points yet.",
ephemeral: true,
});
return;
}

entries.sort((a, b) => b.points - a.points);

const topEntries = entries;

const trophyEmojis = ["🥇", "🥈", "🥉"];
const currentDate = new Date().toLocaleDateString("en-US", {
month: "long",
day: "numeric",
year: "numeric",
});

let leaderboardMessage = `🏆 **Survey Leaderboard | ${currentDate}** 🏆\n\n`;

const members = interaction.guild?.members.cache;

if (members) {
const memberList = members.map((member: GuildMember) => ({
[member.user.username]: member.user.id,
}));
}

for (const [index, entry] of topEntries.entries()) {
const rank = index + 1;
const username = entry.username;
const points = entry.points;

const rankStr =
rank <= trophyEmojis.length ? trophyEmojis[rank - 1] : `${rank}.`;

const member = members.find(
(m: GuildMember) =>
m.user.username === username || m.user.globalName === username,
);

const userDisplayName = member ? `<@${member.user.id}>` : username;

leaderboardMessage += `${rankStr} **${userDisplayName}** **|** ${points} Points\n`;
}

if (leaderboardMessage.length > 2000) {
const messages = splitMessage(leaderboardMessage);
await interaction.reply(messages[0]);
for (let i = 1; i < messages.length; i++) {
await interaction.followUp(messages[i]);
}
} else {
await interaction.reply(leaderboardMessage);
}
};

function splitMessage(message: string, maxLength = 2000): string[] {
const messages = [];
let currentMessage = "";

const lines = message.split("\n");
for (const line of lines) {
if ((currentMessage + line + "\n").length > maxLength) {
messages.push(currentMessage);
currentMessage = line + "\n";
} else {
currentMessage += line + "\n";
}
}
if (currentMessage) {
messages.push(currentMessage);
}
return messages;
}
43 changes: 42 additions & 1 deletion src/commands/handleModals.ts
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,8 @@ export const handleRespondModal = async (
username: any,
redisClient: any,
) => {
await interaction.deferReply({ ephemeral: true });

const surveyName = interaction.customId.split("-").slice(1).join("-");
const surveyType = await redisClient.get(`survey:${surveyName}:type`);
const plural = surveyType === "single" ? "" : "s";
Expand Down Expand Up @@ -222,7 +224,7 @@ export const handleRespondModal = async (
);
await respond(redisClient, surveyName, username, response);

await interaction.reply({
await interaction.editReply({
content: `Your Response was ${hadResponse ? "updated" : "added"} successfully!`,
ephemeral: true,
});
Expand Down Expand Up @@ -253,6 +255,45 @@ export const handleDeleteModal = async (
}
};

export const handleEditSurveyCountModal = async (
interaction: any,
username: any,
redisClient: any,
) => {
const usernameInput = interaction.fields.getTextInputValue(`usernameInput`);
const countInput = interaction.fields.getTextInputValue(`countInput`);
const pointsInput = interaction.fields.getTextInputValue(`pointsInput`);

const adjustment = parseInt(countInput, 10);
const pointsAdjustment = parseInt(pointsInput, 10);

if (isNaN(adjustment) || isNaN(pointsAdjustment)) {
await interaction.reply({
content:
"Invalid count or points input. Please use a format like '+3' or '-2' or '+10' or '-5'.",
ephemeral: true,
});
return;
}

await redisClient.hIncrBy("user:survey_counts", usernameInput, adjustment);
await redisClient.hIncrBy(
"user:survey_points",
usernameInput,
pointsAdjustment,
);

const updatedCount = await redisClient.hGet(
"user:survey_counts",
usernameInput,
);

await interaction.reply({
content: `Survey count for ${usernameInput} has been updated. New count: ${updatedCount}`,
ephemeral: true,
});
};

export function convertToTimestamp(dateString: string): number {
const dateFormat = /^\d{4}-\d{2}-\d{2}-\d{2}-\d{2}$/;

Expand Down
43 changes: 43 additions & 0 deletions src/commands/handleRespond.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { isMeaningful } from "@lib/isMeaningful";
import { ActionRowBuilder, ButtonBuilder, ButtonStyle } from "discord.js";

export const handleRespond = async (
Expand Down Expand Up @@ -38,13 +39,55 @@ export const handleRespond = async (
});
};

const evaluateResponseMeaningfulness = async (
response: string,
): Promise<boolean> => {
const res = await isMeaningful(response);
return res;
};

export const respond = async (
redisClient: any,
surveyName: any,
username: any,
response: any,
) => {
const hasResponded = await redisClient.hExists(
`survey:${surveyName}:responses`,
username,
);

await redisClient.hSet(`survey:${surveyName}:responses`, username, response);
await redisClient.set(`survey:${surveyName}:last-edit-time`, Date.now());
await redisClient.publish("survey-refresh", surveyName);

const isMeaningful = await evaluateResponseMeaningfulness(response);

const surveyCreatedAt = await redisClient.get(
`survey:${surveyName}:created-at`,
);
const surveyCreatedTimestamp = Number(surveyCreatedAt);
const responseTimestamp = Date.now();

const ONE_DAY_MS = 24 * 60 * 60 * 1000; // 24 hours
const ONE_WEEK_MS = 7 * ONE_DAY_MS; // 7 days

const timeElapsed = responseTimestamp - surveyCreatedTimestamp;

let points = 0;
if (isMeaningful) {
if (timeElapsed <= ONE_DAY_MS) {
points = 10;
} else if (timeElapsed <= ONE_WEEK_MS) {
points = 5;
} else {
points = 1;
}
}

if (!hasResponded && isMeaningful) {
await redisClient.hIncrBy("user:survey_counts", username, 1);
await redisClient.hIncrBy("user:survey_counts_discord", username, 1);
await redisClient.hIncrBy("user:survey_points", username, points);
}
};
Loading

0 comments on commit 7c17585

Please sign in to comment.