| 1 | import Color from "color"; |
| 2 | import * as Discord from "discord.js"; |
| 3 | import NodeCache from "node-cache"; |
| 4 | import fs from 'fs'; |
| 5 | import path from 'path'; |
| 6 | import 'dotenv/config'; |
| 7 | |
| 8 | const helpMessage = `**I'm the Name Painter. I let users customize their name color.** |
| 9 | Some things to note |
| 10 | |
| 11 | - The paint command is available to **all users** in the server by default. |
| 12 | - You should not have existing roles that start with \`#\` as this is how the bot identifies color roles. |
| 13 | - Roles always are added at the bottom of the role list, meaning none of your other roles should have colors. |
| 14 | |
| 15 | To use me, simply run |
| 16 | > \`/paint <color>\` |
| 17 | > Where <color> is any valid HEX or CSS color value. |
| 18 | |
| 19 | Created by paper clover, <https://paperclover.net>, support \`namepaint@paperclover.net\``; |
| 20 | |
| 21 | const client = new Discord.Client({ |
| 22 | intents: [ |
| 23 | Discord.IntentsBitField.Flags.Guilds, |
| 24 | Discord.IntentsBitField.Flags.GuildMembers, |
| 25 | ], |
| 26 | }); |
| 27 | |
| 28 | async function updateClientStatus() { |
| 29 | const totalGuilds = (await client.shard.fetchClientValues("guilds.cache.size")).reduce((a, b) => a + b); |
| 30 | client.user?.setActivity(`/paint | ${totalGuilds} servers.`) |
| 31 | } |
| 32 | |
| 33 | client.on('ready', async() => { |
| 34 | console.log(`Logged in as ${client.user?.tag}!`); |
| 35 | updateClientStatus().catch((err) => { console.error(err); }); |
| 36 | |
| 37 | // client.application.commands.create({ |
| 38 | // name: 'paint', |
| 39 | // description: 'Customize your username color', |
| 40 | // options: [ |
| 41 | // { name: 'color', description: 'Color to change your name to (HEX or CSS)', type: 'STRING', required: true }, |
| 42 | // ] |
| 43 | // }); |
| 44 | }); |
| 45 | |
| 46 | const rateLimitCache = new NodeCache({ |
| 47 | stdTTL: 2, |
| 48 | }) |
| 49 | |
| 50 | async function cleanup(guild: Discord.Guild) { |
| 51 | const now = Date.now(); |
| 52 | let n = 0; |
| 53 | const rolesToCheck = guild.roles.cache.filter(role => role.name.startsWith('#')); |
| 54 | const allMembers = await guild.members.fetch(); |
| 55 | const colorUses: Record<string, boolean> = {}; |
| 56 | allMembers.forEach(x => { |
| 57 | x.roles.cache.forEach(role => { |
| 58 | if (role.name.startsWith('#')) { |
| 59 | colorUses[role.id] = true; |
| 60 | } |
| 61 | }); |
| 62 | }); |
| 63 | await Promise.all(rolesToCheck.map(async (role) => { |
| 64 | if (!colorUses[role.id]) { |
| 65 | n++; |
| 66 | await role.delete(); |
| 67 | } |
| 68 | })); |
| 69 | const time = Date.now() - now; |
| 70 | console.log(`[guild ${guild.id}] cleaned up ${n} roles in ${time}ms [${guild.roles.cache.size} roles left]`); |
| 71 | return n; |
| 72 | } |
| 73 | |
| 74 | client.on('interactionCreate', async (i) => { |
| 75 | if (i.isCommand() && i.commandName === 'paint') { |
| 76 | const input = String(i.options.get('color')!.value); |
| 77 | |
| 78 | let color; |
| 79 | try { |
| 80 | color = Color(input); |
| 81 | } catch (error) { |
| 82 | try { |
| 83 | color = Color('#' + input); |
| 84 | } catch (error) { |
| 85 | i.reply({ |
| 86 | ephemeral: true, |
| 87 | content: `Could not get a color from \`\`${input.substring(0, 500).replace(/\n/g, ' ').replace(/`/g, '\u2063`\u2063')}\`\``, |
| 88 | }) |
| 89 | return; |
| 90 | } |
| 91 | } |
| 92 | |
| 93 | const hex = color.hex(); |
| 94 | |
| 95 | if (rateLimitCache.has(i.user.id)) { |
| 96 | i.reply({ |
| 97 | ephemeral: true, |
| 98 | content: 'You have reached the rate limit. Please wait a few seconds before running the command again.', |
| 99 | }); |
| 100 | return; |
| 101 | } |
| 102 | |
| 103 | rateLimitCache.set(i.user.id , 'true'); |
| 104 | |
| 105 | let guild = i.guild ?? await client.guilds.fetch(i.guildId!); |
| 106 | |
| 107 | let role = guild.roles.cache.find(role => role.name === hex); |
| 108 | |
| 109 | if (!role) { |
| 110 | if (guild.roles.cache.size === 250) { |
| 111 | i.reply({ |
| 112 | ephemeral: true, |
| 113 | content: `The role limit of **250 Roles** has been hit, I cannot assign you this name color.` |
| 114 | }); |
| 115 | return; |
| 116 | } else { |
| 117 | try { |
| 118 | role = await guild.roles.create({ |
| 119 | name: hex, |
| 120 | color: color.rgbNumber(), |
| 121 | permissions: [], |
| 122 | }); |
| 123 | } catch (error) { |
| 124 | console.log(error) |
| 125 | i.reply({ |
| 126 | ephemeral: true, |
| 127 | content: `Error creating a role for you, contact an admin to check my permissions.` |
| 128 | }); |
| 129 | return; |
| 130 | } |
| 131 | } |
| 132 | } |
| 133 | |
| 134 | try { |
| 135 | const member = i.member!; |
| 136 | const memberRoles = member.roles as Discord.GuildMemberRoleManager; |
| 137 | const rolesToRemove = memberRoles.cache.filter(role => role.name.startsWith('#') && role.name !== hex); |
| 138 | rolesToRemove.map(async (role) => { |
| 139 | memberRoles.remove(role); |
| 140 | }); |
| 141 | await memberRoles.add(role); |
| 142 | i.reply({ |
| 143 | ephemeral: true, |
| 144 | content: `You\'ve been painted to **${hex}**` |
| 145 | }); |
| 146 | await cleanup(guild); |
| 147 | } catch (error) { |
| 148 | i.reply({ |
| 149 | ephemeral: true, |
| 150 | content: `Error assigning your role, contact an admin to check my permissions.` |
| 151 | }); |
| 152 | } |
| 153 | } |
| 154 | }); |
| 155 | |
| 156 | client.on('guildMemberRemove', (member) => { |
| 157 | cleanup(member.guild); |
| 158 | }); |
| 159 | |
| 160 | client.on('guildDelete', guild => { |
| 161 | updateClientStatus().catch(err => console.error(err)); |
| 162 | }); |
| 163 | |
| 164 | async function getDefaultChannel(guild: Discord.Guild) { |
| 165 | const channels = await guild.channels.fetch(); |
| 166 | const array = [...channels.values()].filter(Boolean).sort((a, b) => (a as any).rawPosition - (b as any).rawPosition) as Discord.TextChannel[]; |
| 167 | |
| 168 | console.log(array.map(x => x.name)) |
| 169 | |
| 170 | // Check for a "general" channel, which is often default chat |
| 171 | const generalChannel = array.find(channel => channel.name === "general" && channel.permissionsFor(guild.client.user)?.has("SendMessages")); |
| 172 | if (generalChannel) |
| 173 | return generalChannel; |
| 174 | |
| 175 | // Now we get into the heavy stuff: first channel in order where everyone (and the bot) can speak |
| 176 | // hold on to your hats! |
| 177 | const sendableChannels = array |
| 178 | .filter(c => c.type === Discord.ChannelType.GuildText && c.permissionsFor(guild.client.user)?.has("SendMessages")) |
| 179 | .filter(c => c.permissionsFor(guild.id)?.has("ViewChannel") && c.permissionsFor(guild.id)?.has("SendMessages")) |
| 180 | |
| 181 | return sendableChannels[0]; |
| 182 | } |
| 183 | |
| 184 | client.on('guildCreate', async(guild) => { |
| 185 | let defaultChannel = await getDefaultChannel(guild) as Discord.TextChannel; |
| 186 | defaultChannel && defaultChannel.send(helpMessage); |
| 187 | |
| 188 | updateClientStatus().catch(err => console.error(err)); |
| 189 | }); |
| 190 | |
| 191 | client.login(process.env.TOKEN); |
| 192 | |
| 193 | if (process.env.HEALTHCHECKS_URL) { |
| 194 | var https = require('https'); |
| 195 | setInterval(() => { |
| 196 | https.get(process.env.HEALTHCHECKS_URL).on('error', () => {}); |
| 197 | }, 5 * 60 * 1000); |
| 198 | } |
| 199 | |
| 200 | // Client error handler. |
| 201 | client.on('error', function (error) { |
| 202 | // Writes a log of what happened to a log file. |
| 203 | fs.writeFileSync( |
| 204 | path.join('logs', `${Date.now()}.err`), |
| 205 | `${error.name}: ${error.message}` |
| 206 | ); |
| 207 | |
| 208 | // Exits the process (and hopefully restarts!) |
| 209 | process.exit(); |
| 210 | }); |
| 211 | |
| 212 | // Invalidation handler. |
| 213 | client.on('invalidated', function () { |
| 214 | // Writes a log of what happened to a log file. |
| 215 | fs.writeFileSync( |
| 216 | path.join('logs', `${Date.now()}.inv`), |
| 217 | 'Error: The client was invalidated by Discord.' |
| 218 | ); |
| 219 | |
| 220 | // Exits the process (and hopefully restarts!) |
| 221 | process.exit(); |
| 222 | }); |
| 223 | |
| 224 | // Client warning handler (which might not require restarting but just to be safe.) |
| 225 | client.on('warn', function (info) { |
| 226 | // Writes a log of what happened to a log file. |
| 227 | fs.writeFileSync( |
| 228 | path.join('logs', `${Date.now()}.warn`), |
| 229 | `Warning: ${info}` |
| 230 | ); |
| 231 | }); |