Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor: improve member logs and add flag checking #2583

Merged
merged 2 commits into from
Feb 14, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions src/languages/en-US/events/guilds-members.json
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
{
"guildMemberAdd": "User Joined",
"guildMemberAddDescription": "{{mention}} | **Joined Discord**: {{time, duration}} ago.",
"guildMemberAddDescription": "{{user}} joined Discord {{relativeTime}}",
"guildMemberAddedRoles_other": "**Added roles**: {{addedRoles, list(conjunction)}}",
"guildMemberAddedRoles_one": "**Added role**: {{addedRoles}}",
"guildMemberAddMute": "Muted User joined",
"guildMemberBanned": "User Banned",
"guildMemberKicked": "User Kicked",
"guildMemberNoUpdate": "No update detected",
"guildMemberRemove": "User Left",
"guildMemberRemoveDescription": "{{mention}} | **Joined Server**: Unknown.",
"guildMemberRemoveDescriptionWithJoinedAt": "{{mention}} | **Joined Server**: {{time, duration}} ago.",
"guildMemberRemoveDescription": "{{user}} joined this server *an unknown time ago*",
"guildMemberRemoveDescriptionWithJoinedAt": "{{user}} joined this server {{relativeTime}}",
"guildMemberRemovedRoles_other": "**Removed roles**: {{removedRoles, list(conjunction)}}",
"guildMemberRemovedRoles_one": "**Removed role**: {{removedRoles}}",
"guildMemberSoftBanned": "User Softbanned",
Expand Down
38 changes: 19 additions & 19 deletions src/lib/i18n/languageKeys/keys/events/guilds/Members.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,23 @@
import { FT, T } from '#lib/types';

export const GuildMemberAdd = T<string>('events/guilds-members:guildMemberAdd');
export const GuildMemberAddDescription = FT<{ mention: string; time: number }, string>('events/guilds-members:guildMemberAddDescription');
export const GuildMemberAddedRoles = FT<{ addedRoles: string; count: number }, string>('events/guilds-members:guildMemberAddedRoles');
export const GuildMemberAddMute = T<string>('events/guilds-members:guildMemberAddMute');
export const GuildMemberBanned = T<string>('events/guilds-members:guildMemberBanned');
export const GuildMemberKicked = T<string>('events/guilds-members:guildMemberKicked');
export const GuildMemberNoUpdate = T<string>('events/guilds-members:guildMemberNoUpdate');
export const GuildMemberRemove = T<string>('events/guilds-members:guildMemberRemove');
export const GuildMemberRemoveDescription = FT<{ mention: string; time: number }, string>('events/guilds-members:guildMemberRemoveDescription');
export const GuildMemberRemoveDescriptionWithJoinedAt = FT<{ mention: string; time: number }, string>(
export const GuildMemberAdd = T('events/guilds-members:guildMemberAdd');
export const GuildMemberAddDescription = FT<{ user: string; relativeTime: string }>('events/guilds-members:guildMemberAddDescription');
export const GuildMemberAddedRoles = FT<{ addedRoles: string; count: number }>('events/guilds-members:guildMemberAddedRoles');
export const GuildMemberAddMute = T('events/guilds-members:guildMemberAddMute');
export const GuildMemberBanned = T('events/guilds-members:guildMemberBanned');
export const GuildMemberKicked = T('events/guilds-members:guildMemberKicked');
export const GuildMemberNoUpdate = T('events/guilds-members:guildMemberNoUpdate');
export const GuildMemberRemove = T('events/guilds-members:guildMemberRemove');
export const GuildMemberRemoveDescription = FT<{ user: string; relativeTime: string }>('events/guilds-members:guildMemberRemoveDescription');
export const GuildMemberRemoveDescriptionWithJoinedAt = FT<{ user: string; relativeTime: string }>(
'events/guilds-members:guildMemberRemoveDescriptionWithJoinedAt'
);
export const GuildMemberRemovedRoles = FT<{ removedRoles: string; count: number }, string>('events/guilds-members:guildMemberRemovedRoles');
export const GuildMemberSoftBanned = T<string>('events/guilds-members:guildMemberSoftBanned');
export const NameUpdateNextWasNotSet = FT<{ nextName: string | null }, string>('events/guilds-members:nameUpdateNextWasNotSet');
export const NameUpdateNextWasSet = FT<{ nextName: string | null }, string>('events/guilds-members:nameUpdateNextWasSet');
export const NameUpdatePreviousWasNotSet = FT<{ previousName: string | null }, string>('events/guilds-members:nameUpdatePreviousWasNotSet');
export const NameUpdatePreviousWasSet = FT<{ previousName: string | null }, string>('events/guilds-members:nameUpdatePreviousWasSet');
export const NicknameUpdate = T<string>('events/guilds-members:nicknameUpdate');
export const RoleUpdate = T<string>('events/guilds-members:roleUpdate');
export const UsernameUpdate = T<string>('events/guilds-members:usernameUpdate');
export const GuildMemberRemovedRoles = FT<{ removedRoles: string; count: number }>('events/guilds-members:guildMemberRemovedRoles');
export const GuildMemberSoftBanned = T('events/guilds-members:guildMemberSoftBanned');
export const NameUpdateNextWasNotSet = FT<{ nextName: string | null }>('events/guilds-members:nameUpdateNextWasNotSet');
export const NameUpdateNextWasSet = FT<{ nextName: string | null }>('events/guilds-members:nameUpdateNextWasSet');
export const NameUpdatePreviousWasNotSet = FT<{ previousName: string | null }>('events/guilds-members:nameUpdatePreviousWasNotSet');
export const NameUpdatePreviousWasSet = FT<{ previousName: string | null }>('events/guilds-members:nameUpdatePreviousWasSet');
export const NicknameUpdate = T('events/guilds-members:nicknameUpdate');
export const RoleUpdate = T('events/guilds-members:roleUpdate');
export const UsernameUpdate = T('events/guilds-members:usernameUpdate');
9 changes: 2 additions & 7 deletions src/lib/util/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,13 +38,8 @@ export const enum Emojis {
GreenTickSerialized = 's637706251253317669',
Loading = '<a:sloading:656988867403972629>',
RedCross = '<:redCross:637706251257511973>',
Star = '<:Star:736337719982030910>',
StarEmpty = '<:StarEmpty:736337232738254849>',
StarHalf = '<:StarHalf:736337529900499034>',
/** This is the default Twemoji, uploaded as a custom emoji because iOS and Android do not render the emoji properly */
MaleSignEmoji = '<:2642:845772713770614874>',
/** This is the default Twemoji, uploaded as a custom emoji because iOS and Android do not render the emoji properly */
FemaleSignEmoji = '<:2640:845772713729720320>'
SpammerIcon = '<:spammer:1206893298292232245>',
QuarantinedIcon = '<:quarantined:1206899526447923210>'
}

export const enum BrandingColors {
Expand Down
1 change: 1 addition & 0 deletions src/lib/util/functions/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@ export * from '#lib/util/functions/guild';
export * from '#lib/util/functions/messages';
export * from '#lib/util/functions/permissions';
export * from '#lib/util/functions/pieces';
export * from '#lib/util/functions/users';
35 changes: 35 additions & 0 deletions src/lib/util/functions/users.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { Emojis } from '#utils/constants';
import { userMention } from '@discordjs/builders';
import { BitField } from '@sapphire/bitfield';
import { UserFlags, type Snowflake } from 'discord.js';

const ExtendedUserFlagBits = new BitField({
Quarantined: getExtendedBits(UserFlags.Quarantined),
Collaborator: getExtendedBits(UserFlags.Collaborator),
RestrictedCollaborator: getExtendedBits(UserFlags.RestrictedCollaborator)
});

export function getModerationFlags(bitfield: number) {
return {
spammer: (bitfield & UserFlags.Spammer) === UserFlags.Spammer,
quarantined: ExtendedUserFlagBits.has(getExtendedBits(bitfield), ExtendedUserFlagBits.flags.Quarantined)
};
}

export function getModerationFlagsString(bitfield: number) {
const { spammer, quarantined } = getModerationFlags(bitfield);
if (spammer && quarantined) return Emojis.SpammerIcon + Emojis.QuarantinedIcon;
if (spammer) return Emojis.SpammerIcon;
if (quarantined) return Emojis.QuarantinedIcon;
return '';
}

export function getUserMentionWithFlagsString(bitfield: number, userId: Snowflake) {
const flags = getModerationFlagsString(bitfield);
const mention = userMention(userId);
return flags ? `${mention} ${flags}` : mention;
}

function getExtendedBits(bitfield: number) {
return (bitfield / (1 << 30)) | 0;
}
108 changes: 78 additions & 30 deletions src/listeners/guilds/members/guildMemberAdd.ts
Original file line number Diff line number Diff line change
@@ -1,58 +1,106 @@
import { GuildSettings, readSettings, writeSettings } from '#lib/database';
import { LanguageKeys } from '#lib/i18n/languageKeys';
import { Events } from '#lib/types';
import { floatPromise } from '#utils/common';
import { seconds, toErrorCodeResult } from '#utils/common';
import { Colors } from '#utils/constants';
import { getStickyRoles } from '#utils/functions';
import { getLogPrefix, getLogger, getStickyRoles, getUserMentionWithFlagsString } from '#utils/functions';
import { getFullEmbedAuthor } from '#utils/util';
import { EmbedBuilder } from '@discordjs/builders';
import { Listener } from '@sapphire/framework';
import { PermissionFlagsBits, type GuildMember } from 'discord.js';
import type { TFunction } from '@sapphire/plugin-i18next';
import { isNullish, type Nullish } from '@sapphire/utilities';
import { Guild, PermissionFlagsBits, RESTJSONErrorCodes, TimestampStyles, time, type GuildMember, type Snowflake } from 'discord.js';

const Root = LanguageKeys.Events.Guilds.Members;
const ChannelSettingsKey = GuildSettings.Channels.Logs.MemberAdd;

export class UserListener extends Listener {
public async run(member: GuildMember) {
if (await this.handleStickyRoles(member)) return;
if (await this.#handleStickyRoles(member)) return;
this.container.client.emit(Events.NotMutedMemberAdd, member);
}

private async handleStickyRoles(member: GuildMember) {
async #handleStickyRoles(member: GuildMember) {
if (!member.guild.members.me!.permissions.has(PermissionFlagsBits.ManageRoles)) return false;

const stickyRoles = await getStickyRoles(member).fetch(member.id);
if (stickyRoles.length === 0) return false;

// Handle the case the user is muted
const key = GuildSettings.Channels.Logs.MemberAdd;
const [logChannelId, roleId, t] = await readSettings(member, (settings) => [
settings[key],
settings[GuildSettings.Roles.Muted],
settings.getLanguage()
const [t, targetChannelId, mutedRoleId] = await readSettings(member, (settings) => [
settings.getLanguage(),
settings[ChannelSettingsKey],
settings[GuildSettings.Roles.Muted]
]);
if (roleId && stickyRoles.includes(roleId)) {
// Handle mute
const role = member.guild.roles.cache.get(roleId);
floatPromise(role ? member.roles.add(role) : writeSettings(member, [[GuildSettings.Roles.Muted, null]]));

// Handle log
this.container.client.emit(Events.GuildMessageLog, member.guild, logChannelId, key, () =>
new EmbedBuilder()
.setColor(Colors.Amber)
.setAuthor(getFullEmbedAuthor(member.user))
.setDescription(
t(LanguageKeys.Events.Guilds.Members.GuildMemberAddDescription, {
mention: member.toString(),
time: Date.now() - member.user.createdTimestamp
})
)
.setFooter({ text: t(LanguageKeys.Events.Guilds.Members.GuildMemberAddMute) })
.setTimestamp()
);
if (mutedRoleId && stickyRoles.includes(mutedRoleId)) {
void this.#handleMutedMemberAddRole(member, mutedRoleId);
void this.#handleMutedMemberNotify(t, member, targetChannelId);

return true;
}

floatPromise(member.roles.add(stickyRoles));
void this.#handleStickyRolesAddRoles(member, stickyRoles);

return false;
}

async #handleMutedMemberAddRole(member: GuildMember, mutedRoleId: Snowflake) {
const { guild } = member;
const role = guild.roles.cache.get(mutedRoleId);
if (isNullish(role)) {
await writeSettings(member, [[GuildSettings.Roles.Muted, null]]);
} else {
const result = await toErrorCodeResult(member.roles.add(role));
await result.inspectErrAsync((code) => this.#handleMutedMemberAddRoleErr(guild, code));
}
}

async #handleMutedMemberAddRoleErr(guild: Guild, code: RESTJSONErrorCodes) {
// The member left the guild before we could add the role, ignore:
if (code === RESTJSONErrorCodes.UnknownMember) return;

// The role was deleted, remove it from the settings:
if (code === RESTJSONErrorCodes.UnknownRole) {
await writeSettings(guild, [[GuildSettings.Roles.Muted, null]]);
return;
}

// Otherwise, log the error:
this.container.logger.error(`${getLogPrefix(this)} Failed to add the muted role to a member.`);
}

async #handleMutedMemberNotify(t: TFunction, member: GuildMember, targetChannelId: Snowflake | Nullish) {
await getLogger(member.guild).send({
key: ChannelSettingsKey,
channelId: targetChannelId,
makeMessage: () => {
const { user } = member;
const description = t(Root.GuildMemberAddDescription, {
user: getUserMentionWithFlagsString(user.flags?.bitfield ?? 0, user.id),
relativeTime: time(seconds.fromMilliseconds(user.createdTimestamp), TimestampStyles.RelativeTime)
});
return new EmbedBuilder()
.setColor(Colors.Amber)
.setAuthor(getFullEmbedAuthor(member.user))
.setDescription(description)
.setFooter({ text: t(Root.GuildMemberAddMute) })
.setTimestamp();
}
});
}

async #handleStickyRolesAddRoles(member: GuildMember, stickyRoles: readonly Snowflake[]) {
const guildRoles = member.guild.roles;
const roles = stickyRoles.filter((role) => guildRoles.cache.has(role));
const result = await toErrorCodeResult(member.roles.add(roles));
await result.inspectErrAsync((code) => this.#handleStickyRolesAddRolesErr(code));
}

#handleStickyRolesAddRolesErr(code: RESTJSONErrorCodes) {
// The member left the guild before we could add the roles, ignore:
if (code === RESTJSONErrorCodes.UnknownMember) return;

// Otherwise, log the error:
this.container.logger.error(`${getLogPrefix(this)} Failed to add the muted role to a member.`);
}
}
43 changes: 24 additions & 19 deletions src/listeners/guilds/members/notMutedMemberAddNotify.ts
Original file line number Diff line number Diff line change
@@ -1,33 +1,38 @@
import { GuildSettings, readSettings } from '#lib/database';
import { LanguageKeys } from '#lib/i18n/languageKeys';
import { Events } from '#lib/types';
import { seconds } from '#utils/common';
import { Colors } from '#utils/constants';
import { getLogger, getUserMentionWithFlagsString } from '#utils/functions';
import { getFullEmbedAuthor } from '#utils/util';
import { EmbedBuilder } from '@discordjs/builders';
import { EmbedBuilder, TimestampStyles, time } from '@discordjs/builders';
import { ApplyOptions } from '@sapphire/decorators';
import { Listener } from '@sapphire/framework';
import { isNullish } from '@sapphire/utilities';
import type { GuildMember } from 'discord.js';

const Root = LanguageKeys.Events.Guilds.Members;
const ChannelSettingsKey = GuildSettings.Channels.Logs.MemberAdd;

@ApplyOptions<Listener.Options>({ event: Events.NotMutedMemberAdd })
export class UserListener extends Listener {
public async run(member: GuildMember) {
const key = GuildSettings.Channels.Logs.MemberAdd;
const [logChannelId, t] = await readSettings(member, (settings) => [settings[key], settings.getLanguage()]);
if (isNullish(logChannelId)) return;

this.container.client.emit(Events.GuildMessageLog, member.guild, logChannelId, key, () =>
new EmbedBuilder()
.setColor(Colors.Green)
.setAuthor(getFullEmbedAuthor(member.user))
.setDescription(
t(LanguageKeys.Events.Guilds.Members.GuildMemberAddDescription, {
mention: member.toString(),
time: Date.now() - member.user.createdTimestamp
})
)
.setFooter({ text: t(LanguageKeys.Events.Guilds.Members.GuildMemberAdd) })
.setTimestamp()
);
const [t, logChannelId] = await readSettings(member, (settings) => [settings.getLanguage(), settings[ChannelSettingsKey]]);
await getLogger(member.guild).send({
key: ChannelSettingsKey,
channelId: logChannelId,
makeMessage: () => {
const { user } = member;
const description = t(Root.GuildMemberAddDescription, {
user: getUserMentionWithFlagsString(user.flags?.bitfield ?? 0, user.id),
relativeTime: time(seconds.fromMilliseconds(user.createdTimestamp), TimestampStyles.RelativeTime)
});
return new EmbedBuilder()
.setColor(Colors.Green)
.setAuthor(getFullEmbedAuthor(member.user))
.setDescription(description)
.setFooter({ text: t(Root.GuildMemberAdd) })
.setTimestamp();
}
});
}
}
Loading