1import Color from "color";
2import * as Discord from "discord.js";
3import NodeCache from "node-cache";
4import fs from 'fs';
5import path from 'path';
6import 'dotenv/config';
7
8const helpMessage = `**I'm the Name Painter. I let users customize their name color.**
9Some 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
15To use me, simply run
16> \`/paint <color>\`
17> Where <color> is any valid HEX or CSS color value.
18
19Created by paper clover, <https://paperclover.net>, support \`namepaint@paperclover.net\``;
20
21const client = new Discord.Client({
22 intents: [
23 Discord.IntentsBitField.Flags.Guilds,
24 Discord.IntentsBitField.Flags.GuildMembers,
25 ],
26});
27
28async 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
33client.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
46const rateLimitCache = new NodeCache({
47 stdTTL: 2,
48})
49
50async 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
74client.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
156client.on('guildMemberRemove', (member) => {
157 cleanup(member.guild);
158});
159
160client.on('guildDelete', guild => {
161 updateClientStatus().catch(err => console.error(err));
162});
163
164async 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
184client.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
191client.login(process.env.TOKEN);
192
193if (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.
201client.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.
213client.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.)
225client.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});