blob: 6216a3763ba86140e7efcf5e54d1ae9e50967ba8 [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,
10 StringSelectMenuBuilder
Skyler Grey75ea9172022-08-06 10:22:23 +010011} from "discord.js";
PineaFan64486c42022-12-28 09:21:04 +000012import type { SlashCommandSubcommandBuilder } from "@discordjs/builders";
PineaFan1dee28f2023-01-16 22:09:07 +000013import type { GuildBasedChannel } from "discord.js";
pineafan4edb7762022-06-26 19:21:04 +010014import EmojiEmbed from "../../utils/generateEmojiEmbed.js";
pineafanc6158ab2022-06-17 16:34:07 +010015import getEmojiByName from "../../utils/getEmojiByName.js";
pineafan4edb7762022-06-26 19:21:04 +010016import pageIndicator from "../../utils/createPageIndicator.js";
pineafan4f164f32022-02-26 22:07:12 +000017
18const command = (builder: SlashCommandSubcommandBuilder) =>
19 builder
pineafan63fc5e22022-08-04 22:04:10 +010020 .setName("viewas")
21 .setDescription("View the server as a specific member")
Skyler Grey11236ba2022-08-08 21:13:33 +010022 .addUserOption((option) => option.setName("member").setDescription("The member to view as").setRequired(true));
pineafan4f164f32022-02-26 22:07:12 +000023
pineafanbd02b4a2022-08-05 22:01:38 +010024const callback = async (interaction: CommandInteraction): Promise<void> => {
PineaFan1dee28f2023-01-16 22:09:07 +000025 /*
26 * {
27 categoryObject: channel[],
28 categoryObject: channel[],
29 "null": channel[]
30 }
31 */
PineaFan0d06edc2023-01-17 22:10:31 +000032 const m = await interaction.reply({embeds: LoadingEmbed, ephemeral: true, fetchReply: true})
PineaFan1dee28f2023-01-16 22:09:07 +000033
PineaFan0d06edc2023-01-17 22:10:31 +000034 let channels: Record<string, GuildBasedChannel[]> = {"": []};
PineaFan1dee28f2023-01-16 22:09:07 +000035
PineaFan0d06edc2023-01-17 22:10:31 +000036 const channelCollection = await interaction.guild!.channels.fetch();
37
38 channelCollection.forEach(channel => {
39 if (!channel) return; // if no channel
40 if (channel.type === Discord.ChannelType.GuildCategory) {
41 if(!channels[channel!.id]) channels[channel!.id] = [];
42 } else if (channel.parent) {
43 if (!channels[channel.parent.id]) channels[channel.parent.id] = [channel];
44 else (channels[channel.parent.id as string])!.push(channel);
45 } else {
46 channels[""]!.push(channel);
47 }
pineafan63fc5e22022-08-04 22:04:10 +010048 });
PineaFan1dee28f2023-01-16 22:09:07 +000049
50 const member = interaction.options.getMember("member") as Discord.GuildMember;
51 const autoSortBelow = [Discord.ChannelType.GuildVoice, Discord.ChannelType.GuildStageVoice];
PineaFan0d06edc2023-01-17 22:10:31 +000052
53 for (const category in channels) {
54 channels[category] = channels[category]!.sort((a: GuildBasedChannel, b: GuildBasedChannel) => {
55 const disallowedTypes = [Discord.ChannelType.PublicThread, Discord.ChannelType.PrivateThread, Discord.ChannelType.AnnouncementThread];
56 if (disallowedTypes.includes(a.type) || disallowedTypes.includes(b.type)) return 0;
57 a = a as NonThreadGuildBasedChannel;
58 b = b as NonThreadGuildBasedChannel;
59 if (autoSortBelow.includes(a.type) && autoSortBelow.includes(b.type)) return a.position - b.position;
Skyler Grey75ea9172022-08-06 10:22:23 +010060 if (autoSortBelow.includes(a.type)) return 1;
61 if (autoSortBelow.includes(b.type)) return -1;
PineaFan0d06edc2023-01-17 22:10:31 +000062 return a.position - b.position;
Skyler Grey75ea9172022-08-06 10:22:23 +010063 });
pineafanc6158ab2022-06-17 16:34:07 +010064 }
PineaFan0d06edc2023-01-17 22:10:31 +000065 for (const category in channels) {
66 channels[category] = channels[category]!.filter((c) => {
67 return c.permissionsFor(member).has("ViewChannel");
68 });
69 }
70 for (const category in channels) {
71 channels[category] = channels[category]!.filter((c) => {
72 return !(c.type === Discord.ChannelType.PublicThread || c.type === Discord.ChannelType.PrivateThread || c.type === Discord.ChannelType.AnnouncementThread)
73 });
74 }
75 channels = Object.fromEntries(Object.entries(channels).filter(([_, v]) => v.length > 0));
76 let page = 0;
77 let closed = false;
78 const categoryIDs = Object.keys(channels);
79 const categoryNames = Object.values(channels).map((c) => {
80 return c[0]!.parent?.name ?? "Uncategorised";
81 });
82 // Split the category names into the first and last 25, ignoring the last 25 if there are 25 or less
83 const first25 = categoryNames.slice(0, 25);
84 const last25 = categoryNames.slice(25);
85 const categoryNames25: string[][] = [first25];
86 if (last25.length > 0) categoryNames25.push(last25);
PineaFan1dee28f2023-01-16 22:09:07 +000087
PineaFan0d06edc2023-01-17 22:10:31 +000088 const channelTypeEmoji: Record<number, string> = {
89 0: "GUILD_TEXT", // Text channel
90 2: "GUILD_VOICE", // Voice channel
91 5: "GUILD_NEWS", // Announcement channel
92 13: "GUILD_STAGE_VOICE", // Stage channel
93 15: "FORUM", // Forum channel
94 99: "RULES" // Rules channel
95 };
96 const NSFWAvailable: number[] = [0, 2, 5, 13];
97 const rulesChannel = interaction.guild!.rulesChannel?.id;
98
99 async function nameFromChannel(channel: GuildBasedChannel): Promise<string> {
100 let channelType = channel.type;
101 if (channelType === Discord.ChannelType.GuildCategory) return "";
102 if (channel.id === rulesChannel) channelType = 99
103 let threads: Discord.ThreadChannel[] = [];
104 if ("threads" in channel) {
105 threads = channel.threads.cache.toJSON().map((t) => t as Discord.ThreadChannel);
106 }
107 const nsfw = ("nsfw" in channel ? channel.nsfw : false) && NSFWAvailable.includes(channelType)
108 const emojiName = channelTypeEmoji[channelType] + (nsfw ? "_NSFW" : "");
109 const emoji = getEmojiByName("ICONS.CHANNEL." + (threads.length ? "THREAD_CHANNEL" : emojiName));
110 let current = `${emoji} ${channel.name}`;
111 if (threads.length) {
112 for (const thread of threads) {
113 current += `\n${getEmojiByName("ICONS.CHANNEL.THREAD_PIPE")} ${thread.name}`;
114 }
115 }
116 return current;
117 }
118
119 while (!closed) {
120 const category = categoryIDs[page]!;
121 let description = "";
122 for (const channel of channels[category]!) {
123 description += `${await nameFromChannel(channel)}\n`;
124 }
125
126 const parsedCategorySelectMenu: ActionRowBuilder<StringSelectMenuBuilder | ButtonBuilder>[] = categoryNames25.map(
127 (categories, set) => { return new ActionRowBuilder<StringSelectMenuBuilder>().addComponents(new StringSelectMenuBuilder()
128 .setCustomId("category")
129 .setMinValues(1)
130 .setMaxValues(1)
131 .setOptions(categories.map((c, i) => {
132 return new StringSelectMenuOptionBuilder()
133 .setLabel(c)
134 .setValue((set * 25 + i).toString())
135 // @ts-expect-error
136 .setEmoji(getEmojiByName("ICONS.CHANNEL.CATEGORY", "id")) // Again, this is valid but TS doesn't think so
137 .setDefault((set * 25 + i) === page)
138 }))
139 )}
140 );
141
142 const components: ActionRowBuilder<ButtonBuilder | StringSelectMenuBuilder>[] = parsedCategorySelectMenu
143 components.push(new ActionRowBuilder<ButtonBuilder>().addComponents(
144 new ButtonBuilder()
145 .setCustomId("back")
146 .setStyle(ButtonStyle.Secondary)
147 .setDisabled(page === 0)
148 .setEmoji(getEmojiByName("CONTROL.LEFT", "id")),
149 new ButtonBuilder()
150 .setCustomId("right")
151 .setStyle(ButtonStyle.Secondary)
152 .setDisabled(page === categoryIDs.length - 1)
153 .setEmoji(getEmojiByName("CONTROL.RIGHT", "id"))
154 ));
155
156 await interaction.editReply({
157 embeds: [new EmojiEmbed()
158 .setEmoji("MEMBER.JOIN")
159 .setTitle("Viewing as " + member.displayName)
160 .setStatus("Success")
161 .setDescription(description + "\n" + pageIndicator(categoryIDs.length, page))
162 ], components: components
163 });
164 let i;
165 try {
166 i = await m.awaitMessageComponent({filter: (i) => i.user.id === interaction.user.id, time: 30000});
167 } catch (e) {
168 closed = true;
169 continue;
170 }
171 i.deferUpdate();
172 if (i.customId === "back") page--;
173 else if (i.customId === "right") page++;
174 else if (i.customId === "category" && i.isStringSelectMenu()) page = parseInt(i.values[0]!);
175 }
pineafan63fc5e22022-08-04 22:04:10 +0100176};
pineafan4f164f32022-02-26 22:07:12 +0000177
pineafanbd02b4a2022-08-05 22:01:38 +0100178const check = (interaction: CommandInteraction) => {
Skyler Grey75ea9172022-08-06 10:22:23 +0100179 const member = interaction.member as GuildMember;
PineaFan0d06edc2023-01-17 22:10:31 +0000180 if (!member.permissions.has("ManageRoles")) return "You do not have the *Manage Roles* permission";
pineafan63fc5e22022-08-04 22:04:10 +0100181 return true;
182};
pineafan4f164f32022-02-26 22:07:12 +0000183
Skyler Grey75ea9172022-08-06 10:22:23 +0100184export { command, callback, check };