nuno ревизий этого фрагмента 1 month ago. К ревизии
1 file changed, 166 insertions
gistfile1.txt(файл создан)
| @@ -0,0 +1,166 @@ | |||
| 1 | + | const { | |
| 2 | + | Client, | |
| 3 | + | GatewayIntentBits, | |
| 4 | + | ButtonBuilder, | |
| 5 | + | ButtonStyle, | |
| 6 | + | ActionRowBuilder, | |
| 7 | + | EmbedBuilder, | |
| 8 | + | } = require("discord.js"); | |
| 9 | + | const cron = require("node-cron"); | |
| 10 | + | ||
| 11 | + | // ─── Config ──────────────────────────────────────────────────────────────── | |
| 12 | + | const TOKEN = process.env.DISCORD_TOKEN; // your bot token | |
| 13 | + | const CHANNEL_ID = process.env.CHANNEL_ID; // TG planning channel ID | |
| 14 | + | const TG_HOUR = 20; // TG starts at 20:00 | |
| 15 | + | const POST_HOUR = 10; // bot posts at 19:00 (1h before) | |
| 16 | + | const TIMEZONE = "Etc/GMT-2"; // GMT+2 (change to e.g. "Europe/Lisbon" if needed) | |
| 17 | + | // ──────────────────────────────────────────────────────────────────────────── | |
| 18 | + | ||
| 19 | + | const client = new Client({ intents: [GatewayIntentBits.Guilds, GatewayIntentBits.GuildMembers] }); | |
| 20 | + | ||
| 21 | + | // vote entry: { username, votedAt, previousYesAt? } | |
| 22 | + | let votes = { yes: new Map(), no: new Map() }; | |
| 23 | + | let currentPollMessageId = null; | |
| 24 | + | ||
| 25 | + | function nowFormatted() { | |
| 26 | + | return new Date().toLocaleTimeString("en-GB", { | |
| 27 | + | timeZone: TIMEZONE, | |
| 28 | + | hour: "2-digit", | |
| 29 | + | minute: "2-digit", | |
| 30 | + | }); | |
| 31 | + | } | |
| 32 | + | ||
| 33 | + | function formatYesVoters() { | |
| 34 | + | if (votes.yes.size === 0) return "—"; | |
| 35 | + | return [...votes.yes.values()] | |
| 36 | + | .map((v) => `${v.username} · ${v.votedAt}`) | |
| 37 | + | .join("\n"); | |
| 38 | + | } | |
| 39 | + | ||
| 40 | + | function formatNoVoters() { | |
| 41 | + | if (votes.no.size === 0) return "—"; | |
| 42 | + | return [...votes.no.values()] | |
| 43 | + | .map((v) => { | |
| 44 | + | const switched = v.previousYesAt | |
| 45 | + | ? ` (was Yes at ${v.previousYesAt})` | |
| 46 | + | : ""; | |
| 47 | + | return `${v.username} · ${v.votedAt}${switched}`; | |
| 48 | + | }) | |
| 49 | + | .join("\n"); | |
| 50 | + | } | |
| 51 | + | ||
| 52 | + | function buildEmbed() { | |
| 53 | + | return new EmbedBuilder() | |
| 54 | + | .setTitle("⚔️ TG — Tonight?") | |
| 55 | + | .setDescription( | |
| 56 | + | `Is **TG happening tonight at ${TG_HOUR}:00**?\n` | |
| 57 | + | ) | |
| 58 | + | .setColor(0xe8a317) | |
| 59 | + | .addFields( | |
| 60 | + | { | |
| 61 | + | name: `✅ Yes (${votes.yes.size})`, | |
| 62 | + | value: formatYesVoters(), | |
| 63 | + | inline: true, | |
| 64 | + | }, | |
| 65 | + | { | |
| 66 | + | name: `❌ No (${votes.no.size})`, | |
| 67 | + | value: formatNoVoters(), | |
| 68 | + | inline: true, | |
| 69 | + | } | |
| 70 | + | ) | |
| 71 | + | .setFooter({ text: "Vote updates live • Anyone can vote • You can switch your vote" }) | |
| 72 | + | .setTimestamp(); | |
| 73 | + | } | |
| 74 | + | ||
| 75 | + | function buildButtons() { | |
| 76 | + | const yesBtn = new ButtonBuilder() | |
| 77 | + | .setCustomId("tg_yes") | |
| 78 | + | .setLabel("✅ Yes") | |
| 79 | + | .setStyle(ButtonStyle.Success); | |
| 80 | + | ||
| 81 | + | const noBtn = new ButtonBuilder() | |
| 82 | + | .setCustomId("tg_no") | |
| 83 | + | .setLabel("❌ No") | |
| 84 | + | .setStyle(ButtonStyle.Danger); | |
| 85 | + | ||
| 86 | + | return new ActionRowBuilder().addComponents(yesBtn, noBtn); | |
| 87 | + | } | |
| 88 | + | ||
| 89 | + | async function updatePollMessage(channel) { | |
| 90 | + | if (!currentPollMessageId) return; | |
| 91 | + | try { | |
| 92 | + | const msg = await channel.messages.fetch(currentPollMessageId); | |
| 93 | + | await msg.edit({ embeds: [buildEmbed()], components: [buildButtons()] }); | |
| 94 | + | } catch (err) { | |
| 95 | + | console.error("Failed to update poll message:", err); | |
| 96 | + | } | |
| 97 | + | } | |
| 98 | + | ||
| 99 | + | async function postDailyPoll() { | |
| 100 | + | try { | |
| 101 | + | const channel = await client.channels.fetch(CHANNEL_ID); | |
| 102 | + | if (!channel) return console.error("Channel not found."); | |
| 103 | + | ||
| 104 | + | votes = { yes: new Map(), no: new Map() }; | |
| 105 | + | currentPollMessageId = null; | |
| 106 | + | ||
| 107 | + | const msg = await channel.send({ | |
| 108 | + | embeds: [buildEmbed()], | |
| 109 | + | components: [buildButtons()], | |
| 110 | + | }); | |
| 111 | + | currentPollMessageId = msg.id; | |
| 112 | + | console.log(`[${new Date().toISOString()}] Poll posted.`); | |
| 113 | + | } catch (err) { | |
| 114 | + | console.error("Failed to post poll:", err); | |
| 115 | + | } | |
| 116 | + | } | |
| 117 | + | ||
| 118 | + | client.on("interactionCreate", async (interaction) => { | |
| 119 | + | if (!interaction.isButton()) return; | |
| 120 | + | if (!["tg_yes", "tg_no"].includes(interaction.customId)) return; | |
| 121 | + | ||
| 122 | + | const userId = interaction.user.id; | |
| 123 | + | const member = await interaction.guild.members.fetch(userId); | |
| 124 | + | const username = member.nickname ?? interaction.user.username; | |
| 125 | + | const votedYes = interaction.customId === "tg_yes"; | |
| 126 | + | const now = nowFormatted(); | |
| 127 | + | ||
| 128 | + | // Ignore if already voted the same option | |
| 129 | + | if (votedYes && votes.yes.has(userId)) return interaction.deferUpdate(); | |
| 130 | + | if (!votedYes && votes.no.has(userId)) return interaction.deferUpdate(); | |
| 131 | + | ||
| 132 | + | if (votedYes) { | |
| 133 | + | // Switching from No → Yes | |
| 134 | + | votes.no.delete(userId); | |
| 135 | + | votes.yes.set(userId, { username, votedAt: now }); | |
| 136 | + | } else { | |
| 137 | + | // Switching from Yes → No — track when they were Yes | |
| 138 | + | const previousYes = votes.yes.get(userId); | |
| 139 | + | votes.yes.delete(userId); | |
| 140 | + | votes.no.set(userId, { | |
| 141 | + | username, | |
| 142 | + | votedAt: now, | |
| 143 | + | previousYesAt: previousYes ? previousYes.votedAt : null, | |
| 144 | + | }); | |
| 145 | + | } | |
| 146 | + | ||
| 147 | + | await interaction.reply({ | |
| 148 | + | content: votedYes | |
| 149 | + | ? `✅ You voted **Yes** for TG tonight!` | |
| 150 | + | : `❌ You voted **No** for TG tonight, you roaching out?`, | |
| 151 | + | ephemeral: true, | |
| 152 | + | }); | |
| 153 | + | ||
| 154 | + | const channel = await client.channels.fetch(CHANNEL_ID); | |
| 155 | + | await updatePollMessage(channel); | |
| 156 | + | }); | |
| 157 | + | ||
| 158 | + | client.once("clientReady", () => { | |
| 159 | + | console.log(`Logged in as ${client.user.tag}`); | |
| 160 | + | ||
| 161 | + | cron.schedule(`0 ${POST_HOUR} * * *`, () => postDailyPoll()); | |
| 162 | + | postDailyPoll() | |
| 163 | + | console.log(`Poll scheduled daily at ${POST_HOUR}:00.`); | |
| 164 | + | }); | |
| 165 | + | ||
| 166 | + | client.login(TOKEN); | |
Новее
Позже