blob: b176dd4a1c051a7ffeed79c297d3ec02ad5fdbc5 [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> => {
TheCodedProf51296102023-01-18 22:35:02 -050025
PineaFan0d06edc2023-01-17 22:10:31 +000026 const m = await interaction.reply({embeds: LoadingEmbed, ephemeral: true, fetchReply: true})
PineaFan1dee28f2023-01-16 22:09:07 +000027
PineaFan0d06edc2023-01-17 22:10:31 +000028 let channels: Record<string, GuildBasedChannel[]> = {"": []};
PineaFan1dee28f2023-01-16 22:09:07 +000029
PineaFan0d06edc2023-01-17 22:10:31 +000030 const channelCollection = await interaction.guild!.channels.fetch();
31
32 channelCollection.forEach(channel => {
33 if (!channel) return; // if no channel
34 if (channel.type === Discord.ChannelType.GuildCategory) {
35 if(!channels[channel!.id]) channels[channel!.id] = [];
36 } else if (channel.parent) {
37 if (!channels[channel.parent.id]) channels[channel.parent.id] = [channel];
38 else (channels[channel.parent.id as string])!.push(channel);
39 } else {
40 channels[""]!.push(channel);
41 }
pineafan63fc5e22022-08-04 22:04:10 +010042 });
PineaFan1dee28f2023-01-16 22:09:07 +000043
44 const member = interaction.options.getMember("member") as Discord.GuildMember;
45 const autoSortBelow = [Discord.ChannelType.GuildVoice, Discord.ChannelType.GuildStageVoice];
PineaFan0d06edc2023-01-17 22:10:31 +000046
47 for (const category in channels) {
48 channels[category] = channels[category]!.sort((a: GuildBasedChannel, b: GuildBasedChannel) => {
49 const disallowedTypes = [Discord.ChannelType.PublicThread, Discord.ChannelType.PrivateThread, Discord.ChannelType.AnnouncementThread];
50 if (disallowedTypes.includes(a.type) || disallowedTypes.includes(b.type)) return 0;
51 a = a as NonThreadGuildBasedChannel;
52 b = b as NonThreadGuildBasedChannel;
53 if (autoSortBelow.includes(a.type) && autoSortBelow.includes(b.type)) return a.position - b.position;
Skyler Grey75ea9172022-08-06 10:22:23 +010054 if (autoSortBelow.includes(a.type)) return 1;
55 if (autoSortBelow.includes(b.type)) return -1;
PineaFan0d06edc2023-01-17 22:10:31 +000056 return a.position - b.position;
Skyler Grey75ea9172022-08-06 10:22:23 +010057 });
pineafanc6158ab2022-06-17 16:34:07 +010058 }
PineaFan0d06edc2023-01-17 22:10:31 +000059 for (const category in channels) {
60 channels[category] = channels[category]!.filter((c) => {
61 return c.permissionsFor(member).has("ViewChannel");
62 });
63 }
64 for (const category in channels) {
65 channels[category] = channels[category]!.filter((c) => {
66 return !(c.type === Discord.ChannelType.PublicThread || c.type === Discord.ChannelType.PrivateThread || c.type === Discord.ChannelType.AnnouncementThread)
67 });
68 }
69 channels = Object.fromEntries(Object.entries(channels).filter(([_, v]) => v.length > 0));
70 let page = 0;
71 let closed = false;
72 const categoryIDs = Object.keys(channels);
73 const categoryNames = Object.values(channels).map((c) => {
74 return c[0]!.parent?.name ?? "Uncategorised";
75 });
76 // Split the category names into the first and last 25, ignoring the last 25 if there are 25 or less
77 const first25 = categoryNames.slice(0, 25);
78 const last25 = categoryNames.slice(25);
79 const categoryNames25: string[][] = [first25];
80 if (last25.length > 0) categoryNames25.push(last25);
PineaFan1dee28f2023-01-16 22:09:07 +000081
PineaFan0d06edc2023-01-17 22:10:31 +000082 const channelTypeEmoji: Record<number, string> = {
83 0: "GUILD_TEXT", // Text channel
84 2: "GUILD_VOICE", // Voice channel
85 5: "GUILD_NEWS", // Announcement channel
86 13: "GUILD_STAGE_VOICE", // Stage channel
87 15: "FORUM", // Forum channel
88 99: "RULES" // Rules channel
89 };
90 const NSFWAvailable: number[] = [0, 2, 5, 13];
91 const rulesChannel = interaction.guild!.rulesChannel?.id;
92
93 async function nameFromChannel(channel: GuildBasedChannel): Promise<string> {
PineaFan638eb132023-01-19 10:41:22 +000094 let channelType: Discord.ChannelType | 99 = channel.type;
PineaFan0d06edc2023-01-17 22:10:31 +000095 if (channelType === Discord.ChannelType.GuildCategory) return "";
96 if (channel.id === rulesChannel) channelType = 99
97 let threads: Discord.ThreadChannel[] = [];
98 if ("threads" in channel) {
99 threads = channel.threads.cache.toJSON().map((t) => t as Discord.ThreadChannel);
100 }
101 const nsfw = ("nsfw" in channel ? channel.nsfw : false) && NSFWAvailable.includes(channelType)
TheCodedProf51296102023-01-18 22:35:02 -0500102 const emojiName = channelTypeEmoji[channelType.valueOf()] + (nsfw ? "_NSFW" : "");
PineaFan0d06edc2023-01-17 22:10:31 +0000103 const emoji = getEmojiByName("ICONS.CHANNEL." + (threads.length ? "THREAD_CHANNEL" : emojiName));
104 let current = `${emoji} ${channel.name}`;
105 if (threads.length) {
106 for (const thread of threads) {
107 current += `\n${getEmojiByName("ICONS.CHANNEL.THREAD_PIPE")} ${thread.name}`;
108 }
109 }
110 return current;
111 }
112
113 while (!closed) {
114 const category = categoryIDs[page]!;
115 let description = "";
116 for (const channel of channels[category]!) {
117 description += `${await nameFromChannel(channel)}\n`;
118 }
119
120 const parsedCategorySelectMenu: ActionRowBuilder<StringSelectMenuBuilder | ButtonBuilder>[] = categoryNames25.map(
121 (categories, set) => { return new ActionRowBuilder<StringSelectMenuBuilder>().addComponents(new StringSelectMenuBuilder()
122 .setCustomId("category")
123 .setMinValues(1)
124 .setMaxValues(1)
125 .setOptions(categories.map((c, i) => {
126 return new StringSelectMenuOptionBuilder()
127 .setLabel(c)
128 .setValue((set * 25 + i).toString())
129 // @ts-expect-error
130 .setEmoji(getEmojiByName("ICONS.CHANNEL.CATEGORY", "id")) // Again, this is valid but TS doesn't think so
131 .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 {
160 i = await m.awaitMessageComponent({filter: (i) => i.user.id === interaction.user.id, time: 30000});
161 } catch (e) {
162 closed = true;
163 continue;
164 }
165 i.deferUpdate();
166 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
pineafanbd02b4a2022-08-05 22:01:38 +0100172const check = (interaction: CommandInteraction) => {
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 };