1 |
/** |
2 |
* This file is part of SudoBot. |
3 |
* |
4 |
* Copyright (C) 2021-2023 OSN Developers. |
5 |
* |
6 |
* SudoBot is free software; you can redistribute it and/or modify it |
7 |
* under the terms of the GNU Affero General Public License as published by |
8 |
* the Free Software Foundation, either version 3 of the License, or |
9 |
* (at your option) any later version. |
10 |
* |
11 |
* SudoBot is distributed in the hope that it will be useful, but |
12 |
* WITHOUT ANY WARRANTY; without even the implied warranty of |
13 |
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
14 |
* GNU Affero General Public License for more details. |
15 |
* |
16 |
* You should have received a copy of the GNU Affero General Public License |
17 |
* along with SudoBot. If not, see <https://www.gnu.org/licenses/>. |
18 |
*/ |
19 |
|
20 |
import { formatDistanceToNowStrict } from "date-fns"; |
21 |
import { |
22 |
APIEmbedField, |
23 |
ActivityType, |
24 |
EmbedBuilder, |
25 |
Emoji, |
26 |
Guild, |
27 |
GuildMember, |
28 |
PermissionFlagsBits, |
29 |
Role, |
30 |
SlashCommandBuilder, |
31 |
User, |
32 |
roleMention |
33 |
} from "discord.js"; |
34 |
import Client from "../../core/Client"; |
35 |
import Command, { AnyCommandContext, ArgumentType, CommandMessage, CommandReturn, ValidationRule } from "../../core/Command"; |
36 |
import { log, logError } from "../../utils/logger"; |
37 |
import { getUserBadges } from "../../utils/user"; |
38 |
import { getEmoji } from "../../utils/utils"; |
39 |
|
40 |
const status = (s: "idle" | "online" | "dnd" | "invisible" | null | undefined): string => { |
41 |
if (s === "idle") return "Idle"; |
42 |
else if (s === "dnd") return "Do not disturb"; |
43 |
else if (s === "online") return "Online"; |
44 |
else if (s === undefined || s === null || s === "invisible") return "Offline/Invisible"; |
45 |
|
46 |
return s; |
47 |
}; |
48 |
|
49 |
const statusEmoji = (client: Client, s: "idle" | "online" | "dnd" | "invisible" | null | undefined): string => { |
50 |
if (s === "idle") return getEmoji(client, "idle"); |
51 |
else if (s === "dnd") return getEmoji(client, "dnd"); |
52 |
else if (s === "online") return getEmoji(client, "online"); |
53 |
else if (s === undefined || s === null || s === "invisible") return getEmoji(client, "invisible"); |
54 |
|
55 |
return s; |
56 |
}; |
57 |
|
58 |
const getStatusText = (client: Client, member: GuildMember) => |
59 |
"" + |
60 |
((member?.presence?.clientStatus?.desktop |
61 |
? statusEmoji(client, member?.presence?.clientStatus?.desktop) + |
62 |
" Desktop (" + |
63 |
status(member?.presence?.clientStatus?.desktop) + |
64 |
")\n" |
65 |
: "") + |
66 |
(member?.presence?.clientStatus?.web |
67 |
? statusEmoji(client, member?.presence?.clientStatus?.web) + |
68 |
" Web (" + |
69 |
status(member?.presence?.clientStatus?.web) + |
70 |
")\n" |
71 |
: "") + |
72 |
(member?.presence?.clientStatus?.mobile |
73 |
? statusEmoji(client, member?.presence?.clientStatus?.mobile) + |
74 |
" Mobile (" + |
75 |
status(member?.presence?.clientStatus?.mobile) + |
76 |
")" |
77 |
: "")); |
78 |
|
79 |
export function getPermissionLevel( |
80 |
{ permissions, guild, id }: { id: string; permissions: GuildMember["permissions"]; guild: Guild }, |
81 |
string: boolean = false |
82 |
) { |
83 |
if (guild.ownerId === id) { |
84 |
return string ? "100" : 100; |
85 |
} |
86 |
|
87 |
const allBits = Object.keys(PermissionFlagsBits).length; |
88 |
const array = permissions.toArray(); |
89 |
|
90 |
if (array.includes("Administrator")) { |
91 |
return string ? "100" : 100; |
92 |
} |
93 |
|
94 |
const percentage = Math.round((array.length / allBits) * 100); |
95 |
return string ? percentage.toString() : percentage; |
96 |
} |
97 |
|
98 |
export default class ProfileCommand extends Command { |
99 |
public readonly name = "profile"; |
100 |
public readonly validationRules: ValidationRule[] = [ |
101 |
{ |
102 |
types: [ArgumentType.Member, ArgumentType.User], |
103 |
name: "member", |
104 |
optional: true, |
105 |
errors: { |
106 |
"type:invalid": "Invalid user given!", |
107 |
"entity:null": "That user could not be found!", |
108 |
}, |
109 |
entity: { |
110 |
notNull: true |
111 |
}, |
112 |
} |
113 |
]; |
114 |
public readonly aliases = ["userprofile", "userinfo"]; |
115 |
public readonly permissions = []; |
116 |
|
117 |
public readonly description = "Shows your or someone else's profile."; |
118 |
public readonly slashCommandBuilder = new SlashCommandBuilder().addUserOption(option => |
119 |
option.setName("member").setDescription("The target member or user") |
120 |
); |
121 |
|
122 |
private isAvailableEmoji({ id, identifier }: Emoji) { |
123 |
for (const [, guild] of this.client.guilds.cache) { |
124 |
const emoji = guild.emojis.cache.find(e => e.id === id || e.identifier === identifier); |
125 |
|
126 |
if (emoji) { |
127 |
return true; |
128 |
} |
129 |
} |
130 |
|
131 |
return false; |
132 |
} |
133 |
|
134 |
private isGuildMember(member: GuildMember | User | null): member is GuildMember { |
135 |
return member instanceof GuildMember; |
136 |
} |
137 |
|
138 |
async execute(message: CommandMessage, context: AnyCommandContext): Promise<CommandReturn> { |
139 |
await this.deferIfInteraction(message); |
140 |
|
141 |
const member: GuildMember | User | null = |
142 |
(context.isLegacy |
143 |
? context.parsedNamedArgs.member |
144 |
: context.options.getMember("member") ?? context.options.getUser("member", true)) ?? message.member; |
145 |
|
146 |
if (!member) { |
147 |
await this.error(message, "Could not resolve that member!"); |
148 |
return; |
149 |
} |
150 |
|
151 |
const isMember = this.isGuildMember(member); |
152 |
const user = isMember ? (member.user! as User) : member; |
153 |
const activities: string[] = []; |
154 |
|
155 |
if (isMember && member?.presence) { |
156 |
for (const a of member?.presence?.activities.values()!) { |
157 |
log(a); |
158 |
|
159 |
if (a.type === ActivityType.Custom) { |
160 |
activities.push( |
161 |
`${a.emoji && this.isAvailableEmoji(a.emoji) ? `${a.emoji.toString()}` : ":small_blue_diamond:"} ${ |
162 |
a.state |
163 |
}` |
164 |
); |
165 |
} else if (a.type === ActivityType.Listening) { |
166 |
if (a.name === "Spotify") { |
167 |
const url = a.url ? `${a.url}` : null; |
168 |
activities.push( |
169 |
`${this.emoji("spotify")} Listening to **Spotify**: ${url ? "[" : "__"}${a.state?.split(/\;/)[0]} - ${ |
170 |
a.details |
171 |
}${url ? "](" + url + ")" : "__"}` |
172 |
); |
173 |
continue; |
174 |
} |
175 |
|
176 |
activities.push(`:musical_note: Listening to **${a.name}**`); |
177 |
} else if (a.type === ActivityType.Competing) { |
178 |
activities.push(`:fire: Competing **${a.name}**`); |
179 |
} else if (a.type === ActivityType.Playing) { |
180 |
activities.push(`:video_game: Playing **${a.name}**`); |
181 |
} else if (a.type === ActivityType.Streaming) { |
182 |
activities.push(`:video_camera: Streaming **${a.name}**`); |
183 |
} else if (a.type === ActivityType.Watching) { |
184 |
activities.push(`:tv: Watching **${a.name}**`); |
185 |
} |
186 |
} |
187 |
} |
188 |
|
189 |
const allRoles = isMember |
190 |
? [...member!.roles.cache.values()] |
191 |
.filter(role => role.id !== message.guildId!) |
192 |
.sort((role1, role2) => { |
193 |
return role2.position - role1.position; |
194 |
}) |
195 |
: null; |
196 |
const limit = 10; |
197 |
const roles = (isMember ? (allRoles!.length > limit ? allRoles!.slice(0, limit) : allRoles) : ([] as Role[]))! |
198 |
.reduce((acc, value) => `${acc} ${roleMention(value.id)}`, "")! |
199 |
.trim()!; |
200 |
const statusText = isMember ? getStatusText(this.client, member!) : null; |
201 |
|
202 |
const fields: APIEmbedField[] = [ |
203 |
...(isMember |
204 |
? [ |
205 |
{ |
206 |
name: "Nickname", |
207 |
value: `${member!.nickname?.replace(/\*\<\>\@\_\~\|/g, "") ?? "*Nickname is not set*"}` |
208 |
} |
209 |
] |
210 |
: []), |
211 |
{ |
212 |
name: "Display Name", |
213 |
value: `${user!.displayName?.replace(/\*\<\>\@\_\~\|/g, "") ?? "*Display name is not set*"}` |
214 |
}, |
215 |
{ |
216 |
name: "Account Created", |
217 |
value: `${user.createdAt.toLocaleDateString("en-US")} (${formatDistanceToNowStrict(user.createdTimestamp, { |
218 |
addSuffix: true |
219 |
})})`, |
220 |
inline: true |
221 |
}, |
222 |
...(isMember |
223 |
? [ |
224 |
{ |
225 |
name: "Joined at", |
226 |
value: `${member!.joinedAt!.toLocaleDateString("en-US")} (${formatDistanceToNowStrict( |
227 |
member!.joinedTimestamp!, |
228 |
{ |
229 |
addSuffix: true |
230 |
} |
231 |
)})`, |
232 |
inline: true |
233 |
}, |
234 |
{ |
235 |
name: "Active Devices", |
236 |
value: `${statusText === "" ? `${this.emoji("invisible")} Offline/Invisible` : statusText}` |
237 |
}, |
238 |
{ |
239 |
name: "Status", |
240 |
value: `${activities.length === 0 ? "*No status set*" : activities.join("\n")}` |
241 |
}, |
242 |
{ |
243 |
name: "Roles", |
244 |
value: |
245 |
roles === "" |
246 |
? "*No roles assigned*" |
247 |
: `${roles} ${allRoles!.length > limit ? `**+ ${allRoles!.length - limit} More**` : ""}` |
248 |
} |
249 |
] |
250 |
: []) |
251 |
]; |
252 |
|
253 |
const badges = getUserBadges(this.client, user); |
254 |
|
255 |
if (badges.length > 0) { |
256 |
fields.push({ |
257 |
name: "Badges", |
258 |
value: badges.join("\n") |
259 |
}); |
260 |
} |
261 |
|
262 |
let banner: string | undefined; |
263 |
|
264 |
try { |
265 |
await user.fetch(true); |
266 |
banner = user!.bannerURL({ size: 4096, forceStatic: false }) ?? undefined; |
267 |
} catch (e) { |
268 |
logError(e); |
269 |
} |
270 |
|
271 |
log("Banner", banner, user!.banner); |
272 |
|
273 |
let permissionPercentage = 0; |
274 |
|
275 |
if (isMember) { |
276 |
if (this.client.permissionManager.usesLevelBasedMode(member.guild.id)) { |
277 |
permissionPercentage = (await this.client.permissionManager.getManager(member.guild.id)).getPermissionLevel( |
278 |
member |
279 |
); |
280 |
} else { |
281 |
permissionPercentage = |
282 |
message.guild!.ownerId === user.id || (isMember && member.permissions.has("Administrator")) |
283 |
? 100 |
284 |
: (getPermissionLevel(member, false) as number); |
285 |
} |
286 |
} |
287 |
|
288 |
await this.deferredReply(message, { |
289 |
embeds: [ |
290 |
new EmbedBuilder({ |
291 |
image: banner |
292 |
? { |
293 |
url: banner |
294 |
} |
295 |
: undefined |
296 |
}) |
297 |
.setColor(user!.hexAccentColor ? user!.hexAccentColor! : "#007bff") |
298 |
.setAuthor({ |
299 |
name: user.tag!, |
300 |
iconURL: user.displayAvatarURL() |
301 |
}) |
302 |
.setThumbnail( |
303 |
(isMember ? member : user).displayAvatarURL({ |
304 |
size: 4096, |
305 |
forceStatic: false |
306 |
}) |
307 |
) |
308 |
.setFields(fields) |
309 |
.setFooter({ |
310 |
text: |
311 |
`${user.bot ? "Bot" : "User"} • ${member!.id}` + |
312 |
(isMember ? ` • Has ${permissionPercentage}% permissions` : "") |
313 |
}) |
314 |
] |
315 |
}); |
316 |
} |
317 |
} |