Última atividade 1 month ago

gistfile1.txt Bruto
1const {
2 Client,
3 GatewayIntentBits,
4 ButtonBuilder,
5 ButtonStyle,
6 ActionRowBuilder,
7 EmbedBuilder,
8 REST,
9 Routes,
10 SlashCommandBuilder,
11 } = require("discord.js");
12 const cron = require("node-cron");
13 const fs = require("fs");
14
15 // ─── Config ────────────────────────────────────────────────────────────────
16 const TOKEN = process.env.DISCORD_TOKEN;
17 const CHANNEL_ID = process.env.CHANNEL_ID;
18 const CLIENT_ID = process.env.CLIENT_ID;
19 const GUILD_ID = process.env.GUILD_ID;
20 const TG_HOUR = 20;
21 const POST_HOUR = 10;
22 const TIMEZONE = "Etc/GMT-2"; // change to e.g. "Europe/Lisbon"
23 const LOCK_AT = 10; // clicks before a user is locked
24 const OFFICER_ROLES = ["Ice King"]; // roles allowed to use /tg-poll
25 // ────────────────────────────────────────────────────────────────────────────
26
27 let MESSAGES = loadMessages();
28
29 function loadMessages() {
30 try {
31 return JSON.parse(fs.readFileSync("./messages.json", "utf8"));
32 } catch (err) {
33 console.error("Failed to load messages.json:", err);
34 return { public: { yes: [], no: [], users: {} }, ephemeral: { yes: [], no: [], users: {} } };
35 }
36 }
37
38 const client = new Client({
39 intents: [GatewayIntentBits.Guilds, GatewayIntentBits.GuildMembers],
40 });
41
42 // vote entry: { username, votedAt, previousNoAt?, previousYesAt?, publicMessage? }
43 let votes = { yes: new Map(), no: new Map() };
44 let locks = new Map(); // userId → { yes: number, no: number }
45 let pollLocked = false;
46 let currentPollMessageId = null;
47
48 function nowFormatted() {
49 return new Date().toLocaleTimeString("en-GB", {
50 timeZone: TIMEZONE,
51 hour: "2-digit",
52 minute: "2-digit",
53 });
54 }
55
56 // Resolves the best message for a given block (public/ephemeral), vote type, click count, username
57 function resolveMessage(block, voteType, clicks, username) {
58 const userPool = MESSAGES[block]?.users?.[username]?.[voteType];
59 const globalPool = MESSAGES[block]?.[voteType] ?? [];
60
61 function bestMatch(pool) {
62 if (!pool || pool.length === 0) return null;
63 const sorted = [...pool].filter((m) => m.clicks <= clicks).sort((a, b) => b.clicks - a.clicks);
64 return sorted[0]?.message ?? null;
65 }
66
67 return bestMatch(userPool) ?? bestMatch(globalPool) ?? null;
68 }
69
70 function hasOfficerRole(member) {
71 return member.roles.cache.some((r) => OFFICER_ROLES.includes(r.name));
72 }
73
74 function formatVoters(map) {
75 if (map.size === 0) return "—";
76 return [...map.values()]
77 .map((v) => {
78 let line = `${v.username} · ${v.votedAt}`;
79 if (v.previousNoAt) line += ` (changed their mind from No at ${v.previousNoAt})`;
80 if (v.previousYesAt) line += ` (switched from Yes at ${v.previousYesAt})`;
81 if (v.publicMessage) line += `\n*${v.publicMessage}*`;
82 return line;
83 })
84 .join("\n");
85 }
86
87 function buildEmbed() {
88 return new EmbedBuilder()
89 .setTitle(`⚔️ TG — Tonight?${pollLocked ? " 🔒" : ""}`)
90 .setDescription(`Is **TG happening tonight at ${TG_HOUR}:00**?\n`)
91 .setColor(pollLocked ? 0x888888 : 0xe8a317)
92 .addFields(
93 { name: `✅ Yes (${votes.yes.size})`, value: formatVoters(votes.yes), inline: true },
94 { name: `❌ No (${votes.no.size})`, value: formatVoters(votes.no), inline: true }
95 )
96 .setFooter({
97 text: pollLocked
98 ? "Poll is locked • Ice King can unlock with /tg-poll unlock"
99 : "Vote updates live • Anyone can vote • You can switch your vote",
100 })
101 .setTimestamp();
102 }
103
104 function buildButtons() {
105 const yesBtn = new ButtonBuilder()
106 .setCustomId("tg_yes")
107 .setLabel("✅ Yes")
108 .setStyle(ButtonStyle.Success)
109 .setDisabled(pollLocked);
110
111 const noBtn = new ButtonBuilder()
112 .setCustomId("tg_no")
113 .setLabel("❌ No")
114 .setStyle(ButtonStyle.Danger)
115 .setDisabled(pollLocked);
116
117 return new ActionRowBuilder().addComponents(yesBtn, noBtn);
118 }
119
120 async function updatePollMessage(channel) {
121 if (!currentPollMessageId) return;
122 try {
123 const msg = await channel.messages.fetch(currentPollMessageId);
124 await msg.edit({ embeds: [buildEmbed()], components: [buildButtons()] });
125 } catch (err) {
126 console.error("Failed to update poll message:", err);
127 }
128 }
129
130 async function postDailyPoll() {
131 try {
132 const channel = await client.channels.fetch(CHANNEL_ID);
133 if (!channel) return console.error("Channel not found.");
134
135 votes = { yes: new Map(), no: new Map() };
136 locks = new Map();
137 pollLocked = false;
138 currentPollMessageId = null;
139
140 const msg = await channel.send({
141 embeds: [buildEmbed()],
142 components: [buildButtons()],
143 });
144 currentPollMessageId = msg.id;
145 console.log(`[${new Date().toISOString()}] Poll posted.`);
146 } catch (err) {
147 console.error("Failed to post poll:", err);
148 }
149 }
150
151 async function registerCommands() {
152 const command = new SlashCommandBuilder()
153 .setName("tg-poll")
154 .setDescription("Manage the TG poll (Ice King only)")
155 .addSubcommand((sub) =>
156 sub.setName("start").setDescription("Post a fresh TG poll")
157 )
158 .addSubcommand((sub) =>
159 sub.setName("unlock").setDescription("Unlock a locked TG poll")
160 );
161
162 const rest = new REST({ version: "10" }).setToken(TOKEN);
163 await rest.put(Routes.applicationGuildCommands(CLIENT_ID, GUILD_ID), {
164 body: [command.toJSON()],
165 });
166 console.log("Slash commands registered.");
167 }
168
169 client.on("interactionCreate", async (interaction) => {
170 // ── Slash commands ──────────────────────────────────────────────────────
171 if (interaction.isChatInputCommand() && interaction.commandName === "tg-poll") {
172 const member = await interaction.guild.members.fetch(interaction.user.id);
173
174 if (!hasOfficerRole(member)) {
175 return interaction.reply({
176 content: "❌ You don't have permission to use this command.",
177 ephemeral: true,
178 });
179 }
180
181 const sub = interaction.options.getSubcommand();
182
183 if (sub === "start") {
184 await interaction.reply({ content: "⚔️ Posting a fresh TG poll...", ephemeral: true });
185 await postDailyPoll();
186 return;
187 }
188
189 if (sub === "unlock") {
190 if (!pollLocked) {
191 return interaction.reply({ content: "ℹ️ The poll isn't locked.", ephemeral: true });
192 }
193 pollLocked = false;
194 MESSAGES = loadMessages();
195 const channel = await client.channels.fetch(CHANNEL_ID);
196 await updatePollMessage(channel);
197 return interaction.reply({ content: "🔓 Poll unlocked!", ephemeral: true });
198 }
199 }
200
201 // ── Button interactions ─────────────────────────────────────────────────
202 if (!interaction.isButton()) return;
203 if (!["tg_yes", "tg_no"].includes(interaction.customId)) return;
204
205 if (pollLocked) return interaction.deferUpdate();
206
207 const userId = interaction.user.id;
208 const member = await interaction.guild.members.fetch(userId);
209 const username = member.nickname ?? interaction.user.username;
210 const votedYes = interaction.customId === "tg_yes";
211 const now = nowFormatted();
212
213 if (!locks.has(userId)) locks.set(userId, { yes: 0, no: 0 });
214 const userLocks = locks.get(userId);
215
216 // Check if user is locked on this button
217 if (votedYes && userLocks.yes >= LOCK_AT) return interaction.deferUpdate();
218 if (!votedYes && userLocks.no >= LOCK_AT) return interaction.deferUpdate();
219
220 // Increment click counter
221 if (votedYes) userLocks.yes += 1;
222 else userLocks.no += 1;
223
224 const clicks = votedYes ? userLocks.yes : userLocks.no;
225
226 // Resolve messages
227 const publicMsg = resolveMessage("public", votedYes ? "yes" : "no", clicks, username);
228 const ephemeralMsg = resolveMessage("ephemeral", votedYes ? "yes" : "no", clicks, username);
229
230 if (votedYes) {
231 const previousNo = votes.no.get(userId);
232 votes.no.delete(userId);
233 votes.yes.set(userId, {
234 username,
235 votedAt: now,
236 previousNoAt: previousNo ? previousNo.votedAt : null,
237 publicMessage: publicMsg,
238 });
239 } else {
240 const previousYes = votes.yes.get(userId);
241 votes.yes.delete(userId);
242 votes.no.set(userId, {
243 username,
244 votedAt: now,
245 previousYesAt: previousYes ? previousYes.votedAt : null,
246 publicMessage: publicMsg,
247 });
248 }
249
250 const locked = clicks >= LOCK_AT;
251 if (locked) pollLocked = true;
252
253 await interaction.reply({
254 content: ephemeralMsg
255 ? `${ephemeralMsg}${locked ? "\n🔒 *You've been locked in.*" : ""}`
256 : locked
257 ? "🔒 You've been locked in."
258 : votedYes ? "✅ Vote registered!" : "❌ Vote registered!",
259 ephemeral: true,
260 });
261
262 const channel = await client.channels.fetch(CHANNEL_ID);
263 await updatePollMessage(channel);
264 });
265
266 client.once("clientReady", async () => {
267 console.log(`Logged in as ${client.user.tag}`);
268 await registerCommands();
269 cron.schedule(`0 ${POST_HOUR} * * *`, () => postDailyPoll());
270 console.log(`Poll scheduled daily at ${POST_HOUR}:00.`);
271 });
272
273 client.login(TOKEN);