Skip to content

Commit 82de73f

Browse files
authored
refactor: simplify (#50)
1 parent 10db9aa commit 82de73f

40 files changed

Lines changed: 498 additions & 1223 deletions

src/commands/contact.command.ts

Lines changed: 4 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
1-
import { EmbedBuilder } from "discord.js";
2-
3-
import { COLORS } from "../config/constants";
41
import type { Command } from "../models";
2+
import { buildContactEmbed } from "../utils/build-contact-embed";
53
import { logError } from "../utils/logger";
64

75
const contactCommand: Command = {
@@ -12,86 +10,16 @@ const contactCommand: Command = {
1210
category: "utility",
1311

1412
async execute(message) {
15-
const embed = new EmbedBuilder()
16-
.setColor(COLORS.PRIMARY)
17-
.setTitle("Contact Us")
18-
.setDescription(
19-
"If you would like to contact us, please send a site message to the appropriate team below.",
20-
)
21-
.addFields([
22-
{
23-
name: ":e_mail: Admins and Moderators",
24-
value: `[Send a message to RAdmin](https://retroachievements.org/createmessage.php?t=RAdmin)
25-
- Reporting offensive behavior.
26-
- Reporting copyrighted material.
27-
- Requesting to be untracked.`,
28-
},
29-
{
30-
name: ":e_mail: Developer Compliance",
31-
value: `[Send a message to Developer Compliance](https://retroachievements.org/createmessage.php?t=DevCompliance)
32-
- Requesting set approval or early set release.
33-
- Reporting achievements or sets with unwelcome concepts.
34-
- Reporting sets failing to cover basic progression.`,
35-
},
36-
{
37-
name: ":e_mail: Quality Assurance",
38-
value: `[Send a message to Quality Assurance](https://retroachievements.org/createmessage.php?t=QATeam)
39-
- Reporting a broken set, leaderboard, or rich presence.
40-
- Reporting achievements with grammatical mistakes.
41-
- Requesting a set be playtested.
42-
- Hash compatibility questions.
43-
- Hub organizational questions.
44-
- Getting involved in a QA sub-team.`,
45-
},
46-
{
47-
name: ":e_mail: RAArtTeam",
48-
value: `[Send a message to RAArtTeam](https://retroachievements.org/messages/create?to=RAArtTeam)
49-
- Icon Gauntlets and how to start one.
50-
- Proposing art updates.
51-
- Questions about art-related rule changes.
52-
- Requests for help with creating a new badge or badge set.`,
53-
},
54-
{
55-
name: ":e_mail: WritingTeam",
56-
value: `[Send a message to WritingTeam](https://retroachievements.org/messages/create?to=WritingTeam)
57-
- Reporting achievements with grammatical mistakes.
58-
- Reporting achievements with unclear or confusing descriptions.
59-
- Requesting help from the team with proofreading achievement sets.
60-
- Requesting help for coming up with original titles for achievements.`,
61-
},
62-
{
63-
name: ":e_mail: RANews",
64-
value: `[Send a message to RANews](https://retroachievements.org/createmessage.php?t=RANews)
65-
- Submitting a Play This Set, Wish This Set, or RAdvantage entry.
66-
- Submitting a retrogaming article.
67-
- Proposing a new article idea.
68-
- Getting involved with RANews.`,
69-
},
70-
{
71-
name: ":e_mail: RAEvents",
72-
value: `[Send a message to RAEvents](https://retroachievements.org/createmessage.php?t=RAEvents)
73-
- Submissions, questions, ideas, or reporting issues related to events.`,
74-
},
75-
{
76-
name: ":e_mail: DevQuest",
77-
value: `[Send a message to DevQuest](https://retroachievements.org/createmessage.php?t=DevQuest)
78-
- Submissions, questions, ideas, or reporting issues related to DevQuest.`,
79-
},
80-
{
81-
name: ":e_mail: RACheats",
82-
value: `[Send a message to RACheats](https://retroachievements.org/createmessage.php?t=RACheats)
83-
- 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).`,
84-
},
85-
]);
13+
const embed = buildContactEmbed();
8614

8715
try {
88-
await message.react("📧");
16+
await message.react("\u{1F4E7}");
8917
await message.reply({ embeds: [embed] });
9018
} catch (error) {
9119
logError(error, {
9220
event: "contact_command_react_error",
9321
userId: message.author.id,
94-
guildId: message.guildId || undefined,
22+
guildId: message.guildId,
9523
channelId: message.channelId,
9624
});
9725
await message.reply({ embeds: [embed] });

src/commands/gan.command.ts

Lines changed: 7 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import type { Command } from "../models";
22
import { GameInfoService } from "../services/game-info.service";
33
import { TemplateService } from "../services/template.service";
4-
import { YouTubeService } from "../services/youtube.service";
4+
import { fetchGanData } from "../utils/fetch-gan-data";
55
import { logError } from "../utils/logger";
66

77
const ganCommand: Command = {
@@ -33,24 +33,18 @@ const ganCommand: Command = {
3333
);
3434

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

4140
return;
4241
}
4342

44-
// Get achievement date and YouTube link.
45-
const achievementSetDate = GameInfoService.getMostRecentAchievementDate(gameInfo);
46-
const youtubeLink = await YouTubeService.searchLongplay(gameInfo.title, gameInfo.consoleName);
47-
48-
// Generate template.
4943
const template = TemplateService.generateGanTemplate(
50-
gameInfo,
51-
achievementSetDate,
52-
youtubeLink,
53-
gameId,
44+
ganData.gameInfo,
45+
ganData.achievementSetDate,
46+
ganData.youtubeLink,
47+
ganData.gameId,
5448
);
5549

5650
await sentMsg.edit(

src/commands/mem.command.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ const memCommand: Command = {
2121
cooldown: 3,
2222

2323
async execute(message, args) {
24-
logCommandExecution("mem", message.author.id, message.guildId || undefined, message.channelId);
24+
logCommandExecution("mem", message.author.id, message.guildId, message.channelId);
2525

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

src/commands/poll.command.ts

Lines changed: 13 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
import type { Command } from "../models";
2-
import { EMOJI_ALPHABET } from "../utils/poll-constants";
2+
import {
3+
addPollReactions,
4+
buildPollMessageLines,
5+
getReactionsForOptions,
6+
} from "../utils/build-poll-message";
37

48
const pollCommand: Command = {
59
name: "poll",
@@ -47,18 +51,11 @@ const pollCommand: Command = {
4751
return;
4852
}
4953

50-
// Build poll message.
51-
const reactions = Object.values(EMOJI_ALPHABET).slice(0, opts.length);
52-
let options = "";
53-
54-
for (let i = 0; i < opts.length; i++) {
55-
options += `\n${reactions[i]} ${opts[i]}`;
56-
}
57-
58-
const pollMsg = [
59-
`__*${message.author} started a poll*__:`,
60-
`\n:bar_chart: **${question}**\n${options}`,
61-
];
54+
const pollMsgLines = buildPollMessageLines({
55+
authorMention: String(message.author),
56+
question,
57+
options: opts,
58+
});
6259

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

70-
const sentMsg = await message.channel.send(pollMsg.join("\n"));
67+
const sentMsg = await message.channel.send(pollMsgLines.join("\n"));
7168

72-
// Add reactions.
73-
for (let i = 0; i < opts.length; i++) {
74-
const emoji = reactions[i];
75-
if (emoji) {
76-
await sentMsg.react(emoji);
77-
}
78-
}
69+
const reactions = getReactionsForOptions(opts);
70+
await addPollReactions(sentMsg, reactions);
7971
},
8072
};
8173

src/commands/tpoll.command.ts

Lines changed: 27 additions & 94 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
1-
import type { MessageReaction, User } from "discord.js";
2-
import { Collection } from "discord.js";
3-
41
import type { Command } from "../models";
52
import { PollService } from "../services/poll.service";
6-
import { logError } from "../utils/logger";
7-
import { EMOJI_ALPHABET } from "../utils/poll-constants";
3+
import {
4+
addPollReactions,
5+
buildPollMessageLines,
6+
getReactionsForOptions,
7+
startTimedPollCollector,
8+
} from "../utils/build-poll-message";
89

910
const tpollCommand: Command = {
1011
name: "tpoll",
@@ -75,23 +76,16 @@ const tpollCommand: Command = {
7576
return;
7677
}
7778

78-
// Build poll message.
79-
const reactions = Object.values(EMOJI_ALPHABET).slice(0, opts.length);
80-
let options = "";
81-
82-
for (let i = 0; i < opts.length; i++) {
83-
options += `\n${reactions[i]} ${opts[i]}`;
84-
}
85-
86-
const pollMsg = [
87-
`__*${message.author} started a poll*__:`,
88-
`\n:bar_chart: **${question}**\n${options}`,
89-
];
79+
const pollMsgLines = buildPollMessageLines({
80+
authorMention: String(message.author),
81+
question,
82+
options: opts,
83+
});
9084

9185
const milliseconds = seconds * 1000;
9286

9387
if (milliseconds > 0) {
94-
pollMsg.push(
88+
pollMsgLines.push(
9589
"\n`Notes:\n- only the first reaction is considered a vote\n- unlisted reactions void the vote`",
9690
);
9791
}
@@ -103,25 +97,20 @@ const tpollCommand: Command = {
10397
return;
10498
}
10599

106-
const sentMsg = await message.channel.send(pollMsg.join("\n"));
100+
const sentMsg = await message.channel.send(pollMsgLines.join("\n"));
107101

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

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

118-
// Add reactions.
119-
for (let i = 0; i < opts.length; i++) {
120-
const emoji = reactions[i];
121-
if (emoji) {
122-
await sentMsg.react(emoji);
123-
}
124-
}
112+
const reactions = getReactionsForOptions(opts);
113+
await addPollReactions(sentMsg, reactions);
125114

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

142-
// Track voters and results in memory for this poll session.
143-
const voters = new Set<string>();
144-
const pollResults = new Collection<string, number>();
145-
146-
// Set up reaction collector.
147-
const filter = (reaction: MessageReaction, user: User) => {
148-
// Ignore bot's reactions.
149-
if (client.user?.id === user.id) {
150-
return false;
151-
}
152-
153-
// Do not allow repeated votes.
154-
if (voters.has(user.id)) {
155-
return false;
156-
}
157-
158-
// Do not count invalid reactions.
159-
if (!reaction.emoji.name || !reactions.includes(reaction.emoji.name)) {
160-
return false;
161-
}
162-
163-
// Add voter and count vote.
164-
voters.add(user.id);
165-
166-
const emojiName = reaction.emoji.name!; // Safe after check above
167-
const optionIndex = reactions.indexOf(emojiName);
168-
if (optionIndex !== -1) {
169-
// Add vote to database.
170-
PollService.addVote(poll.id, user.id, optionIndex);
171-
172-
// Track in memory for immediate results.
173-
const currentVotes = pollResults.get(emojiName) || 0;
174-
pollResults.set(emojiName, currentVotes + 1);
175-
}
176-
177-
return true;
178-
};
179-
180-
const collector = sentMsg.createReactionCollector({ filter, time: milliseconds });
181-
182-
collector.on("end", async () => {
183-
try {
184-
// Prepare the final message.
185-
const finalPollMsg = [
186-
`~~${pollMsg[0]}~~\n:no_entry: **THIS POLL IS ALREADY CLOSED** :no_entry:`,
187-
pollMsg[1], // Question and options.
188-
"\n`This poll is closed.`",
189-
"__**RESULTS:**__\n",
190-
];
191-
192-
if (pollResults.size === 0) {
193-
finalPollMsg.push("No one voted");
194-
} else {
195-
// Sort results by vote count.
196-
const sortedResults = [...pollResults.entries()].sort((a, b) => b[1] - a[1]);
197-
for (const [emoji, count] of sortedResults) {
198-
finalPollMsg.push(`${emoji}: ${count}`);
199-
}
200-
}
201-
202-
await sentMsg.edit(finalPollMsg.join("\n"));
131+
startTimedPollCollector({
132+
sentMsg,
133+
pollMsgLines,
134+
client,
135+
reactions,
136+
milliseconds,
137+
pollId: poll.id,
138+
onEnd: async (finalText) => {
139+
await sentMsg.edit(finalText);
203140

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

211147
await message.reply(pollEndedMsg.join("\n"));
212-
} catch (error) {
213-
logError("Error ending timed poll:", { error });
214-
await message.reply("**`poll` error**: Something went wrong with your poll.");
215-
}
148+
},
216149
});
217150
},
218151
};

src/config/constants.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,5 +40,4 @@ export const COLORS = {
4040
SUCCESS: 0x00ff00,
4141
ERROR: 0xff0000,
4242
WARNING: 0xffff00,
43-
INFO: 0x0099ff,
4443
} as const;

src/database/migrate.ts

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,7 @@
1-
import { drizzle } from "drizzle-orm/libsql";
21
import { migrate } from "drizzle-orm/libsql/migrator";
32

43
import { logger } from "../utils/logger";
5-
6-
const db = drizzle({
7-
connection: { url: "file:rabot.db" },
8-
});
4+
import { db } from "./db";
95

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

0 commit comments

Comments
 (0)