Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
80 changes: 4 additions & 76 deletions src/commands/contact.command.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
import { EmbedBuilder } from "discord.js";

import { COLORS } from "../config/constants";
import type { Command } from "../models";
import { buildContactEmbed } from "../utils/build-contact-embed";
import { logError } from "../utils/logger";

const contactCommand: Command = {
Expand All @@ -12,86 +10,16 @@ const contactCommand: Command = {
category: "utility",

async execute(message) {
const embed = new EmbedBuilder()
.setColor(COLORS.PRIMARY)
.setTitle("Contact Us")
.setDescription(
"If you would like to contact us, please send a site message to the appropriate team below.",
)
.addFields([
{
name: ":e_mail: Admins and Moderators",
value: `[Send a message to RAdmin](https://retroachievements.org/createmessage.php?t=RAdmin)
- Reporting offensive behavior.
- Reporting copyrighted material.
- Requesting to be untracked.`,
},
{
name: ":e_mail: Developer Compliance",
value: `[Send a message to Developer Compliance](https://retroachievements.org/createmessage.php?t=DevCompliance)
- Requesting set approval or early set release.
- Reporting achievements or sets with unwelcome concepts.
- Reporting sets failing to cover basic progression.`,
},
{
name: ":e_mail: Quality Assurance",
value: `[Send a message to Quality Assurance](https://retroachievements.org/createmessage.php?t=QATeam)
- Reporting a broken set, leaderboard, or rich presence.
- Reporting achievements with grammatical mistakes.
- Requesting a set be playtested.
- Hash compatibility questions.
- Hub organizational questions.
- Getting involved in a QA sub-team.`,
},
{
name: ":e_mail: RAArtTeam",
value: `[Send a message to RAArtTeam](https://retroachievements.org/messages/create?to=RAArtTeam)
- Icon Gauntlets and how to start one.
- Proposing art updates.
- Questions about art-related rule changes.
- Requests for help with creating a new badge or badge set.`,
},
{
name: ":e_mail: WritingTeam",
value: `[Send a message to WritingTeam](https://retroachievements.org/messages/create?to=WritingTeam)
- Reporting achievements with grammatical mistakes.
- Reporting achievements with unclear or confusing descriptions.
- Requesting help from the team with proofreading achievement sets.
- Requesting help for coming up with original titles for achievements.`,
},
{
name: ":e_mail: RANews",
value: `[Send a message to RANews](https://retroachievements.org/createmessage.php?t=RANews)
- Submitting a Play This Set, Wish This Set, or RAdvantage entry.
- Submitting a retrogaming article.
- Proposing a new article idea.
- Getting involved with RANews.`,
},
{
name: ":e_mail: RAEvents",
value: `[Send a message to RAEvents](https://retroachievements.org/createmessage.php?t=RAEvents)
- Submissions, questions, ideas, or reporting issues related to events.`,
},
{
name: ":e_mail: DevQuest",
value: `[Send a message to DevQuest](https://retroachievements.org/createmessage.php?t=DevQuest)
- Submissions, questions, ideas, or reporting issues related to DevQuest.`,
},
{
name: ":e_mail: RACheats",
value: `[Send a message to RACheats](https://retroachievements.org/createmessage.php?t=RACheats)
- If you believe someone is in violation of our [Global Leaderboard and Achievement Hunting Rules](https://docs.retroachievements.org/guidelines/users/global-leaderboard-and-achievement-hunting-rules.html#not-allowed).`,
},
]);
const embed = buildContactEmbed();

try {
await message.react("📧");
await message.react("\u{1F4E7}");
await message.reply({ embeds: [embed] });
} catch (error) {
logError(error, {
event: "contact_command_react_error",
userId: message.author.id,
guildId: message.guildId || undefined,
guildId: message.guildId,
channelId: message.channelId,
});
await message.reply({ embeds: [embed] });
Expand Down
20 changes: 7 additions & 13 deletions src/commands/gan.command.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import type { Command } from "../models";
import { GameInfoService } from "../services/game-info.service";
import { TemplateService } from "../services/template.service";
import { YouTubeService } from "../services/youtube.service";
import { fetchGanData } from "../utils/fetch-gan-data";
import { logError } from "../utils/logger";

const ganCommand: Command = {
Expand Down Expand Up @@ -33,24 +33,18 @@ const ganCommand: Command = {
);

try {
// Fetch game info.
const gameInfo = await GameInfoService.fetchGameInfo(gameId);
if (!gameInfo) {
const ganData = await fetchGanData(gameId);
if (!ganData) {
await sentMsg.edit(`Unable to get info from the game ID \`${gameId}\`... :frowning:`);

return;
}

// Get achievement date and YouTube link.
const achievementSetDate = GameInfoService.getMostRecentAchievementDate(gameInfo);
const youtubeLink = await YouTubeService.searchLongplay(gameInfo.title, gameInfo.consoleName);

// Generate template.
const template = TemplateService.generateGanTemplate(
gameInfo,
achievementSetDate,
youtubeLink,
gameId,
ganData.gameInfo,
ganData.achievementSetDate,
ganData.youtubeLink,
ganData.gameId,
);

await sentMsg.edit(
Expand Down
4 changes: 2 additions & 2 deletions src/commands/mem.command.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ const memCommand: Command = {
cooldown: 3,

async execute(message, args) {
logCommandExecution("mem", message.author.id, message.guildId || undefined, message.channelId);
logCommandExecution("mem", message.author.id, message.guildId, message.channelId);

if (!args[0]) {
await message.reply("Please provide an achievement ID, URL, or MemAddr string.");
Expand Down Expand Up @@ -115,7 +115,7 @@ function createCodeNotesEmbed(
codeNotes: Array<{ Address: string; Note: string }>,
): EmbedBuilder | null {
const embed = new EmbedBuilder()
.setColor(COLORS.INFO)
.setColor(COLORS.PRIMARY)
.setTitle("Code Notes")
.setURL(`https://retroachievements.org/codenotes.php?g=${gameId}`);

Expand Down
34 changes: 13 additions & 21 deletions src/commands/poll.command.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
import type { Command } from "../models";
import { EMOJI_ALPHABET } from "../utils/poll-constants";
import {
addPollReactions,
buildPollMessageLines,
getReactionsForOptions,
} from "../utils/build-poll-message";

const pollCommand: Command = {
name: "poll",
Expand Down Expand Up @@ -47,18 +51,11 @@ const pollCommand: Command = {
return;
}

// Build poll message.
const reactions = Object.values(EMOJI_ALPHABET).slice(0, opts.length);
let options = "";

for (let i = 0; i < opts.length; i++) {
options += `\n${reactions[i]} ${opts[i]}`;
}

const pollMsg = [
`__*${message.author} started a poll*__:`,
`\n:bar_chart: **${question}**\n${options}`,
];
const pollMsgLines = buildPollMessageLines({
authorMention: String(message.author),
question,
options: opts,
});

// Send the poll message.
if (!("send" in message.channel)) {
Expand All @@ -67,15 +64,10 @@ const pollCommand: Command = {
return;
}

const sentMsg = await message.channel.send(pollMsg.join("\n"));
const sentMsg = await message.channel.send(pollMsgLines.join("\n"));

// Add reactions.
for (let i = 0; i < opts.length; i++) {
const emoji = reactions[i];
if (emoji) {
await sentMsg.react(emoji);
}
}
const reactions = getReactionsForOptions(opts);
await addPollReactions(sentMsg, reactions);
},
};

Expand Down
121 changes: 27 additions & 94 deletions src/commands/tpoll.command.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import type { MessageReaction, User } from "discord.js";
import { Collection } from "discord.js";

import type { Command } from "../models";
import { PollService } from "../services/poll.service";
import { logError } from "../utils/logger";
import { EMOJI_ALPHABET } from "../utils/poll-constants";
import {
addPollReactions,
buildPollMessageLines,
getReactionsForOptions,
startTimedPollCollector,
} from "../utils/build-poll-message";

const tpollCommand: Command = {
name: "tpoll",
Expand Down Expand Up @@ -75,23 +76,16 @@ const tpollCommand: Command = {
return;
}

// Build poll message.
const reactions = Object.values(EMOJI_ALPHABET).slice(0, opts.length);
let options = "";

for (let i = 0; i < opts.length; i++) {
options += `\n${reactions[i]} ${opts[i]}`;
}

const pollMsg = [
`__*${message.author} started a poll*__:`,
`\n:bar_chart: **${question}**\n${options}`,
];
const pollMsgLines = buildPollMessageLines({
authorMention: String(message.author),
question,
options: opts,
});

const milliseconds = seconds * 1000;

if (milliseconds > 0) {
pollMsg.push(
pollMsgLines.push(
"\n`Notes:\n- only the first reaction is considered a vote\n- unlisted reactions void the vote`",
);
}
Expand All @@ -103,25 +97,20 @@ const tpollCommand: Command = {
return;
}

const sentMsg = await message.channel.send(pollMsg.join("\n"));
const sentMsg = await message.channel.send(pollMsgLines.join("\n"));

if (milliseconds > 0) {
const endTime = new Date(sentMsg.createdTimestamp);
endTime.setTime(endTime.getTime() + milliseconds);

// Use Discord timestamp formatting for local time display
const endTimestamp = Math.floor(endTime.getTime() / 1000);
pollMsg.push(`:stopwatch: *This poll ends <t:${endTimestamp}:F>*`);
await sentMsg.edit(pollMsg.join("\n"));
pollMsgLines.push(`:stopwatch: *This poll ends <t:${endTimestamp}:F>*`);
await sentMsg.edit(pollMsgLines.join("\n"));
}

// Add reactions.
for (let i = 0; i < opts.length; i++) {
const emoji = reactions[i];
if (emoji) {
await sentMsg.react(emoji);
}
}
const reactions = getReactionsForOptions(opts);
await addPollReactions(sentMsg, reactions);

// If no timer, just return.
if (milliseconds === 0) {
Expand All @@ -139,80 +128,24 @@ const tpollCommand: Command = {
endTime,
);

// Track voters and results in memory for this poll session.
const voters = new Set<string>();
const pollResults = new Collection<string, number>();

// Set up reaction collector.
const filter = (reaction: MessageReaction, user: User) => {
// Ignore bot's reactions.
if (client.user?.id === user.id) {
return false;
}

// Do not allow repeated votes.
if (voters.has(user.id)) {
return false;
}

// Do not count invalid reactions.
if (!reaction.emoji.name || !reactions.includes(reaction.emoji.name)) {
return false;
}

// Add voter and count vote.
voters.add(user.id);

const emojiName = reaction.emoji.name!; // Safe after check above
const optionIndex = reactions.indexOf(emojiName);
if (optionIndex !== -1) {
// Add vote to database.
PollService.addVote(poll.id, user.id, optionIndex);

// Track in memory for immediate results.
const currentVotes = pollResults.get(emojiName) || 0;
pollResults.set(emojiName, currentVotes + 1);
}

return true;
};

const collector = sentMsg.createReactionCollector({ filter, time: milliseconds });

collector.on("end", async () => {
try {
// Prepare the final message.
const finalPollMsg = [
`~~${pollMsg[0]}~~\n:no_entry: **THIS POLL IS ALREADY CLOSED** :no_entry:`,
pollMsg[1], // Question and options.
"\n`This poll is closed.`",
"__**RESULTS:**__\n",
];

if (pollResults.size === 0) {
finalPollMsg.push("No one voted");
} else {
// Sort results by vote count.
const sortedResults = [...pollResults.entries()].sort((a, b) => b[1] - a[1]);
for (const [emoji, count] of sortedResults) {
finalPollMsg.push(`${emoji}: ${count}`);
}
}

await sentMsg.edit(finalPollMsg.join("\n"));
startTimedPollCollector({
sentMsg,
pollMsgLines,
client,
reactions,
milliseconds,
pollId: poll.id,
onEnd: async (finalText) => {
await sentMsg.edit(finalText);

// Notify the poll creator.
const pollEndedMsg = [
"**Your poll has ended.**",
"**Click this link to see the results:**",
`<${sentMsg.url}>`,
];

await message.reply(pollEndedMsg.join("\n"));
} catch (error) {
logError("Error ending timed poll:", { error });
await message.reply("**`poll` error**: Something went wrong with your poll.");
}
},
});
},
};
Expand Down
1 change: 0 additions & 1 deletion src/config/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,5 +40,4 @@ export const COLORS = {
SUCCESS: 0x00ff00,
ERROR: 0xff0000,
WARNING: 0xffff00,
INFO: 0x0099ff,
} as const;
6 changes: 1 addition & 5 deletions src/database/migrate.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,7 @@
import { drizzle } from "drizzle-orm/libsql";
import { migrate } from "drizzle-orm/libsql/migrator";

import { logger } from "../utils/logger";

const db = drizzle({
connection: { url: "file:rabot.db" },
});
import { db } from "./db";

await migrate(db, { migrationsFolder: "./drizzle" });

Expand Down
Loading