blob: ef62816b2c28e91530b99a0e16f757cf451e3c57 [file] [log] [blame]
PineaFan0d06edc2023-01-17 22:10:31 +00001import { LoadingEmbed } from './../../utils/defaults.js';
Skyler Grey75ea9172022-08-06 10:22:23 +01002import Discord, {
Skyler Grey75ea9172022-08-06 10:22:23 +01003 CommandInteraction,
4 GuildMember,
TheCodedProf21c08592022-09-13 14:14:43 -04005 ActionRowBuilder,
6 ButtonBuilder,
PineaFan1dee28f2023-01-16 22:09:07 +00007 ButtonStyle,
PineaFan0d06edc2023-01-17 22:10:31 +00008 NonThreadGuildBasedChannel,
9 StringSelectMenuOptionBuilder,
TheCodedProf4a6d5712023-01-19 15:54:40 -050010 StringSelectMenuBuilder,
11 APIMessageComponentEmoji
Skyler Grey75ea9172022-08-06 10:22:23 +010012} from "discord.js";
TheCodedProff86ba092023-01-27 17:10:07 -050013import type { SlashCommandSubcommandBuilder } from "discord.js";
PineaFan1dee28f2023-01-16 22:09:07 +000014import type { GuildBasedChannel } from "discord.js";
pineafan4edb7762022-06-26 19:21:04 +010015import EmojiEmbed from "../../utils/generateEmojiEmbed.js";
pineafanc6158ab2022-06-17 16:34:07 +010016import getEmojiByName from "../../utils/getEmojiByName.js";
pineafan4edb7762022-06-26 19:21:04 +010017import pageIndicator from "../../utils/createPageIndicator.js";
pineafan4f164f32022-02-26 22:07:12 +000018
19const command = (builder: SlashCommandSubcommandBuilder) =>
20 builder
pineafan63fc5e22022-08-04 22:04:10 +010021 .setName("viewas")
22 .setDescription("View the server as a specific member")
Skyler Grey11236ba2022-08-08 21:13:33 +010023 .addUserOption((option) => option.setName("member").setDescription("The member to view as").setRequired(true));
pineafan4f164f32022-02-26 22:07:12 +000024
pineafanbd02b4a2022-08-05 22:01:38 +010025const callback = async (interaction: CommandInteraction): Promise<void> => {
TheCodedProf51296102023-01-18 22:35:02 -050026
PineaFan0d06edc2023-01-17 22:10:31 +000027 const m = await interaction.reply({embeds: LoadingEmbed, ephemeral: true, fetchReply: true})
PineaFan1dee28f2023-01-16 22:09:07 +000028
PineaFan0d06edc2023-01-17 22:10:31 +000029 let channels: Record<string, GuildBasedChannel[]> = {"": []};
PineaFan1dee28f2023-01-16 22:09:07 +000030
PineaFan0d06edc2023-01-17 22:10:31 +000031 const channelCollection = await interaction.guild!.channels.fetch();
32
33 channelCollection.forEach(channel => {
34 if (!channel) return; // if no channel
35 if (channel.type === Discord.ChannelType.GuildCategory) {
36 if(!channels[channel!.id]) channels[channel!.id] = [];
37 } else if (channel.parent) {
38 if (!channels[channel.parent.id]) channels[channel.parent.id] = [channel];
39 else (channels[channel.parent.id as string])!.push(channel);
40 } else {
41 channels[""]!.push(channel);
42 }
pineafan63fc5e22022-08-04 22:04:10 +010043 });
PineaFan1dee28f2023-01-16 22:09:07 +000044
45 const member = interaction.options.getMember("member") as Discord.GuildMember;
46 const autoSortBelow = [Discord.ChannelType.GuildVoice, Discord.ChannelType.GuildStageVoice];
PineaFan0d06edc2023-01-17 22:10:31 +000047
48 for (const category in channels) {
49 channels[category] = channels[category]!.sort((a: GuildBasedChannel, b: GuildBasedChannel) => {
50 const disallowedTypes = [Discord.ChannelType.PublicThread, Discord.ChannelType.PrivateThread, Discord.ChannelType.AnnouncementThread];
51 if (disallowedTypes.includes(a.type) || disallowedTypes.includes(b.type)) return 0;
52 a = a as NonThreadGuildBasedChannel;
53 b = b as NonThreadGuildBasedChannel;
54 if (autoSortBelow.includes(a.type) && autoSortBelow.includes(b.type)) return a.position - b.position;
Skyler Grey75ea9172022-08-06 10:22:23 +010055 if (autoSortBelow.includes(a.type)) return 1;
56 if (autoSortBelow.includes(b.type)) return -1;
PineaFan0d06edc2023-01-17 22:10:31 +000057 return a.position - b.position;
Skyler Grey75ea9172022-08-06 10:22:23 +010058 });
pineafanc6158ab2022-06-17 16:34:07 +010059 }
PineaFan0d06edc2023-01-17 22:10:31 +000060 for (const category in channels) {
61 channels[category] = channels[category]!.filter((c) => {
62 return c.permissionsFor(member).has("ViewChannel");
63 });
64 }
65 for (const category in channels) {
66 channels[category] = channels[category]!.filter((c) => {
67 return !(c.type === Discord.ChannelType.PublicThread || c.type === Discord.ChannelType.PrivateThread || c.type === Discord.ChannelType.AnnouncementThread)
68 });
69 }
70 channels = Object.fromEntries(Object.entries(channels).filter(([_, v]) => v.length > 0));
71 let page = 0;
72 let closed = false;
73 const categoryIDs = Object.keys(channels);
74 const categoryNames = Object.values(channels).map((c) => {
75 return c[0]!.parent?.name ?? "Uncategorised";
76 });
77 // Split the category names into the first and last 25, ignoring the last 25 if there are 25 or less
78 const first25 = categoryNames.slice(0, 25);
79 const last25 = categoryNames.slice(25);
80 const categoryNames25: string[][] = [first25];
81 if (last25.length > 0) categoryNames25.push(last25);
PineaFan1dee28f2023-01-16 22:09:07 +000082
PineaFan0d06edc2023-01-17 22:10:31 +000083 const channelTypeEmoji: Record<number, string> = {
84 0: "GUILD_TEXT", // Text channel
85 2: "GUILD_VOICE", // Voice channel
86 5: "GUILD_NEWS", // Announcement channel
87 13: "GUILD_STAGE_VOICE", // Stage channel
88 15: "FORUM", // Forum channel
89 99: "RULES" // Rules channel
90 };
91 const NSFWAvailable: number[] = [0, 2, 5, 13];
92 const rulesChannel = interaction.guild!.rulesChannel?.id;
93
94 async function nameFromChannel(channel: GuildBasedChannel): Promise<string> {
PineaFan638eb132023-01-19 10:41:22 +000095 let channelType: Discord.ChannelType | 99 = channel.type;
PineaFan0d06edc2023-01-17 22:10:31 +000096 if (channelType === Discord.ChannelType.GuildCategory) return "";
97 if (channel.id === rulesChannel) channelType = 99
98 let threads: Discord.ThreadChannel[] = [];
99 if ("threads" in channel) {
100 threads = channel.threads.cache.toJSON().map((t) => t as Discord.ThreadChannel);
101 }
102 const nsfw = ("nsfw" in channel ? channel.nsfw : false) && NSFWAvailable.includes(channelType)
TheCodedProf51296102023-01-18 22:35:02 -0500103 const emojiName = channelTypeEmoji[channelType.valueOf()] + (nsfw ? "_NSFW" : "");
PineaFan0d06edc2023-01-17 22:10:31 +0000104 const emoji = getEmojiByName("ICONS.CHANNEL." + (threads.length ? "THREAD_CHANNEL" : emojiName));
105 let current = `${emoji} ${channel.name}`;
106 if (threads.length) {
107 for (const thread of threads) {
108 current += `\n${getEmojiByName("ICONS.CHANNEL.THREAD_PIPE")} ${thread.name}`;
109 }
110 }
111 return current;
112 }
113
114 while (!closed) {
115 const category = categoryIDs[page]!;
116 let description = "";
117 for (const channel of channels[category]!) {
118 description += `${await nameFromChannel(channel)}\n`;
119 }
120
121 const parsedCategorySelectMenu: ActionRowBuilder<StringSelectMenuBuilder | ButtonBuilder>[] = categoryNames25.map(
122 (categories, set) => { return new ActionRowBuilder<StringSelectMenuBuilder>().addComponents(new StringSelectMenuBuilder()
123 .setCustomId("category")
124 .setMinValues(1)
125 .setMaxValues(1)
126 .setOptions(categories.map((c, i) => {
127 return new StringSelectMenuOptionBuilder()
128 .setLabel(c)
129 .setValue((set * 25 + i).toString())
TheCodedProf4a6d5712023-01-19 15:54:40 -0500130 .setEmoji(getEmojiByName("ICONS.CHANNEL.CATEGORY", "id") as APIMessageComponentEmoji) // Again, this is valid but TS doesn't think so
PineaFan0d06edc2023-01-17 22:10:31 +0000131 .setDefault((set * 25 + i) === page)
132 }))
133 )}
134 );
135
136 const components: ActionRowBuilder<ButtonBuilder | StringSelectMenuBuilder>[] = parsedCategorySelectMenu
137 components.push(new ActionRowBuilder<ButtonBuilder>().addComponents(
138 new ButtonBuilder()
139 .setCustomId("back")
140 .setStyle(ButtonStyle.Secondary)
141 .setDisabled(page === 0)
142 .setEmoji(getEmojiByName("CONTROL.LEFT", "id")),
143 new ButtonBuilder()
144 .setCustomId("right")
145 .setStyle(ButtonStyle.Secondary)
146 .setDisabled(page === categoryIDs.length - 1)
147 .setEmoji(getEmojiByName("CONTROL.RIGHT", "id"))
148 ));
149
150 await interaction.editReply({
151 embeds: [new EmojiEmbed()
152 .setEmoji("MEMBER.JOIN")
153 .setTitle("Viewing as " + member.displayName)
154 .setStatus("Success")
155 .setDescription(description + "\n" + pageIndicator(categoryIDs.length, page))
156 ], components: components
157 });
158 let i;
159 try {
TheCodedProf267563a2023-01-21 17:00:57 -0500160 i = await m.awaitMessageComponent({filter: (i) => i.user.id === interaction.user.id && i.message.id === m.id, time: 30000});
PineaFan0d06edc2023-01-17 22:10:31 +0000161 } catch (e) {
162 closed = true;
163 continue;
164 }
TheCodedProf267563a2023-01-21 17:00:57 -0500165 await i.deferUpdate();
PineaFan0d06edc2023-01-17 22:10:31 +0000166 if (i.customId === "back") page--;
167 else if (i.customId === "right") page++;
168 else if (i.customId === "category" && i.isStringSelectMenu()) page = parseInt(i.values[0]!);
169 }
pineafan63fc5e22022-08-04 22:04:10 +0100170};
pineafan4f164f32022-02-26 22:07:12 +0000171
TheCodedProff86ba092023-01-27 17:10:07 -0500172const check = (interaction: CommandInteraction, _partial: boolean = false) => {
Skyler Grey75ea9172022-08-06 10:22:23 +0100173 const member = interaction.member as GuildMember;
PineaFan0d06edc2023-01-17 22:10:31 +0000174 if (!member.permissions.has("ManageRoles")) return "You do not have the *Manage Roles* permission";
pineafan63fc5e22022-08-04 22:04:10 +0100175 return true;
176};
pineafan4f164f32022-02-26 22:07:12 +0000177
Skyler Grey75ea9172022-08-06 10:22:23 +0100178export { command, callback, check };