aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.vscode/settings.json6
-rw-r--r--src/commands/info/pronouns.ts62
-rw-r--r--src/commands/info/userInfo.ts5
-rw-r--r--src/commands/leveling/leaderboard.ts2
-rw-r--r--src/commands/moderation/ban.ts51
-rw-r--r--src/commands/moderation/kick.ts5
-rw-r--r--src/commands/moderation/modlog.ts9
-rw-r--r--src/commands/moderation/mute.ts5
-rw-r--r--src/lib/extensions/discord-akairo/BushClientUtil.ts81
-rw-r--r--src/lib/extensions/discord.js/BushBaseGuildTextChannel.ts10
-rw-r--r--src/lib/extensions/discord.js/BushGuild.ts63
-rw-r--r--src/lib/extensions/discord.js/BushGuildMember.ts17
-rw-r--r--src/lib/extensions/discord.js/BushNewsChannel.ts4
-rw-r--r--src/lib/extensions/discord.js/BushTextChannel.ts4
-rw-r--r--src/lib/models/ModLog.ts2
-rw-r--r--src/lib/utils/BushConstants.ts69
-rw-r--r--yarn.lock98
17 files changed, 326 insertions, 167 deletions
diff --git a/.vscode/settings.json b/.vscode/settings.json
index 7a28c7c..3e23797 100644
--- a/.vscode/settings.json
+++ b/.vscode/settings.json
@@ -6,14 +6,16 @@
"**/CVS": true,
"**/.DS_Store": true,
"dist": false,
- ".pnp.js": true
+ ".pnp.js": true,
+ "**/node_modules": true,
},
"javascript.preferences.importModuleSpecifier": "project-relative",
"typescript.preferences.importModuleSpecifier": "project-relative",
"typescript.preferences.importModuleSpecifierEnding": "minimal",
"search.exclude": {
"**/.yarn": true,
- "**/.pnp.*": true
+ "**/.pnp.*": true,
+ "**/node_modules": true,
},
"editor.codeActionsOnSave": {
"source.organizeImports": true,
diff --git a/src/commands/info/pronouns.ts b/src/commands/info/pronouns.ts
index ea20d41..77612da 100644
--- a/src/commands/info/pronouns.ts
+++ b/src/commands/info/pronouns.ts
@@ -1,32 +1,6 @@
import { BushCommand, BushMessage, BushSlashMessage } from '@lib';
import { Snowflake } from 'discord-api-types';
import { MessageEmbed, User } from 'discord.js';
-import got, { HTTPError } from 'got';
-
-export const pronounMapping = {
- unspecified: 'Unspecified',
- hh: 'He/Him',
- hi: 'He/It',
- hs: 'He/She',
- ht: 'He/They',
- ih: 'It/Him',
- ii: 'It/Its',
- is: 'It/She',
- it: 'It/They',
- shh: 'She/He',
- sh: 'She/Her',
- si: 'She/It',
- st: 'She/They',
- th: 'They/He',
- ti: 'They/It',
- ts: 'They/She',
- tt: 'They/Them',
- any: 'Any pronouns',
- other: 'Other pronouns',
- ask: 'Ask me my pronouns',
- avoid: 'Avoid pronouns, use my name'
-};
-export type pronounsType = keyof typeof pronounMapping;
export default class PronounsCommand extends BushCommand {
public constructor() {
@@ -62,43 +36,31 @@ export default class PronounsCommand extends BushCommand {
});
}
override async exec(message: BushMessage | BushSlashMessage, args: { user?: User | Snowflake }): Promise<unknown> {
- const user =
- args?.user === undefined || args?.user === null
- ? message.author
- : typeof args.user === 'object'
- ? args.user
- : await client.users.fetch(`${args.user}`).catch(() => undefined);
+ const user = (await util.resolveNonCachedUser(args.user)) ?? message.author;
- if (user === undefined) return message.util.reply(`${util.emojis.error} Invalid user.`);
+ if (!user) return message.util.reply(`${util.emojis.error} Invalid user.`);
const author = user.id === message.author.id;
- try {
- const apiRes: { pronouns: pronounsType } = await got
- .get(`https://pronoundb.org/api/v1/lookup?platform=discord&id=${user.id}`)
- .json();
+
+ const pronouns = await util.getPronounsOf(user);
+ if (!pronouns) {
+ return await message.util.reply(
+ `${author ? 'You do' : `${user.tag} does`} not appear to have any pronouns set. Please ${
+ author ? '' : 'tell them to'
+ } go to https://pronoundb.org/ and set ${author ? 'your' : 'their'} pronouns.`
+ );
+ } else {
return await message.util.reply({
embeds: [
new MessageEmbed({
title: `${author ? 'Your' : `${user.tag}'s`} pronouns:`,
- description: pronounMapping[apiRes.pronouns],
+ description: pronouns,
footer: {
text: 'Data provided by https://pronoundb.org/'
}
})
]
});
- } catch (e) {
- if (e instanceof HTTPError && e.response.statusCode === 404) {
- if (author) {
- return await message.util.reply(
- 'You do not appear to have any pronouns set. Please go to https://pronoundb.org/ and set your pronouns.'
- );
- } else {
- return await message.util.reply(
- `${user.tag} does not appear to have any pronouns set. Please tell them to go to https://pronoundb.org/ and set their pronouns.`
- );
- }
- } else throw e;
}
}
}
diff --git a/src/commands/info/userInfo.ts b/src/commands/info/userInfo.ts
index ae204f7..9fddd67 100644
--- a/src/commands/info/userInfo.ts
+++ b/src/commands/info/userInfo.ts
@@ -96,6 +96,11 @@ export default class UserInfoCommand extends BushCommand {
`**ID:** ${user.id}`,
`**Created: **${createdAt} (${createdAtDelta} ago)`
];
+ if (user.accentColor !== null) generalInfo.push(`**Accent Color:** ${user.hexAccentColor}`);
+ if (user.banner) generalInfo.push(`**Banner**: [link](${user.bannerURL({ dynamic: true, format: 'png' })})`);
+ const pronouns = await util.getPronounsOf(user);
+ if (pronouns) generalInfo.push(`**Pronouns:** ${pronouns}`);
+
userEmbed.addField('» General Info', generalInfo.join('\n'));
// Server User Info
diff --git a/src/commands/leveling/leaderboard.ts b/src/commands/leveling/leaderboard.ts
index b8838b7..d29c15e 100644
--- a/src/commands/leveling/leaderboard.ts
+++ b/src/commands/leveling/leaderboard.ts
@@ -47,6 +47,6 @@ export default class LeaderboardCommand extends BushCommand {
const embeds = chunked.map((c) =>
new MessageEmbed().setTitle(`${message.guild!.name}'s Leaderboard`).setDescription(c.join('\n'))
);
- return await util.buttonPaginate(message, embeds, null, true, args?.page ?? undefined);
+ return await util.buttonPaginate(message, embeds, undefined, true, args?.page ?? undefined);
}
}
diff --git a/src/commands/moderation/ban.ts b/src/commands/moderation/ban.ts
index c33b39a..2c3e429 100644
--- a/src/commands/moderation/ban.ts
+++ b/src/commands/moderation/ban.ts
@@ -1,5 +1,5 @@
-import { AllowedMentions, BushCommand, BushGuildMember, BushMessage, BushSlashMessage } from '@lib';
-import { User } from 'discord.js';
+import { AllowedMentions, BushCommand, BushMessage, BushSlashMessage } from '@lib';
+import { Snowflake, User } from 'discord.js';
export default class BanCommand extends BushCommand {
public constructor() {
@@ -14,7 +14,7 @@ export default class BanCommand extends BushCommand {
args: [
{
id: 'user',
- type: 'user',
+ customType: util.arg.union('user', 'snowflake'),
prompt: {
start: 'What user would you like to ban?',
retry: '{error} Choose a valid user to ban.'
@@ -83,17 +83,19 @@ export default class BanCommand extends BushCommand {
public override async exec(
message: BushMessage | BushSlashMessage,
{
- user,
+ user: _user,
reason,
days,
force
- }: { user: User; reason?: { duration: number; contentWithoutTime: string }; days?: number; force: boolean }
+ }: { user: User | Snowflake; reason?: { duration: number; contentWithoutTime: string }; days?: number; force: boolean }
): Promise<unknown> {
if (!message.guild) return message.util.reply(`${util.emojis.error} This command cannot be used in dms.`);
- const member = message.guild!.members.cache.get(user.id) as BushGuildMember;
+ const member = message.guild!.members.cache.get((_user as User)?.id);
+ const user = member?.user ?? (await util.resolveNonCachedUser(_user));
+ if (!user) return message.util.reply(`${util.emojis.error} Invalid user.`);
const useForce = force && message.author.isOwner();
if (!message.member) throw new Error(`message.member is null`);
- const canModerateResponse = util.moderationPermissionCheck(message.member, member, 'ban', true, useForce);
+ const canModerateResponse = member ? util.moderationPermissionCheck(message.member, member, 'ban', true, useForce) : true;
if (canModerateResponse !== true) {
return message.util.reply(canModerateResponse);
@@ -112,31 +114,40 @@ export default class BanCommand extends BushCommand {
? await util.arg.cast('duration', client.commandHandler.resolver, message as BushMessage, reason)
: reason.duration;
}
- const parsedReason = reason?.contentWithoutTime ?? '';
+ const parsedReason = reason?.contentWithoutTime ?? null;
- const responseCode = await member.bushBan({
- reason: parsedReason,
- moderator: message.author,
- duration: time! ?? 0,
- deleteDays: days ?? 0
- });
+ const responseCode = member
+ ? await member.bushBan({
+ reason: parsedReason,
+ moderator: message.author,
+ duration: time! ?? 0,
+ deleteDays: days ?? 0
+ })
+ : await message.guild.ban({
+ user,
+ reason: parsedReason,
+ moderator: message.author,
+ duration: time! ?? 0,
+ deleteDays: days ?? 0
+ });
const responseMessage = () => {
switch (responseCode) {
case 'missing permissions':
- return `${util.emojis.error} Could not ban **${member.user.tag}** because I do not have permissions`;
+ return `${util.emojis.error} Could not ban **${user.tag}** because I do not have permissions`;
case 'error banning':
- return `${util.emojis.error} An error occurred while trying to ban **${member.user.tag}**.`;
+ return `${util.emojis.error} An error occurred while trying to ban **${user.tag}**.`;
case 'error creating ban entry':
- return `${util.emojis.error} While banning **${member.user.tag}**, there was an error creating a ban entry, please report this to my developers.`;
+ return `${util.emojis.error} While banning **${user.tag}**, there was an error creating a ban entry, please report this to my developers.`;
case 'error creating modlog entry':
- return `${util.emojis.error} While banning **${member.user.tag}**, there was an error creating a modlog entry, please report this to my developers.`;
+ return `${util.emojis.error} While banning **${user.tag}**, there was an error creating a modlog entry, please report this to my developers.`;
case 'failed to dm':
- return `${util.emojis.warn} Banned **${member.user.tag}** however I could not send them a dm.`;
+ return `${util.emojis.warn} Banned **${user.tag}** however I could not send them a dm.`;
case 'success':
- return `${util.emojis.success} Successfully banned **${member.user.tag}**.`;
+ return `${util.emojis.success} Successfully banned **${user.tag}**.`;
}
};
+ client.console.debug(responseCode);
return await message.util.reply({ content: responseMessage(), allowedMentions: AllowedMentions.none() });
}
}
diff --git a/src/commands/moderation/kick.ts b/src/commands/moderation/kick.ts
index 2315712..341d83c 100644
--- a/src/commands/moderation/kick.ts
+++ b/src/commands/moderation/kick.ts
@@ -61,7 +61,10 @@ export default class KickCommand extends BushCommand {
): Promise<unknown> {
const member = message.guild!.members.cache.get(user.id) as BushGuildMember;
- if (!member) return await message.util.reply(`${util.emojis.error} You cannot kick members that are not in the server.`);
+ if (!member)
+ return await message.util.reply(
+ `${util.emojis.error} The user you selected is not in the server or is not a valid user.`
+ );
if (!message.member) throw new Error(`message.member is null`);
const useForce = force && message.author.isOwner();
const canModerateResponse = util.moderationPermissionCheck(message.member, member, 'kick', true, useForce);
diff --git a/src/commands/moderation/modlog.ts b/src/commands/moderation/modlog.ts
index ef0a56e..fd53ea7 100644
--- a/src/commands/moderation/modlog.ts
+++ b/src/commands/moderation/modlog.ts
@@ -35,6 +35,7 @@ export default class ModlogCommand extends BushCommand {
}
#generateModlogInfo(log: ModLog): string {
+ const trim = (str: string): string => (str.endsWith('\n') ? str.substring(0, str.length - 1).trim() : str.trim());
const modLog = [
`**Case ID**: ${log.id}`,
`**Type**: ${log.type.toLowerCase()}`,
@@ -42,8 +43,8 @@ export default class ModlogCommand extends BushCommand {
`**Moderator**: <@!${log.moderator}> (${log.moderator})`
];
if (log.duration) modLog.push(`**Duration**: ${util.humanizeDuration(log.duration)}`);
- modLog.push(`**Reason**: ${log.reason ?? 'No Reason Specified.'}`);
- if (log.evidence) modLog.push(`**Evidence:** ${log.evidence}`);
+ modLog.push(`**Reason**: ${trim(log.reason ?? 'No Reason Specified.')}`);
+ if (log.evidence) modLog.push(`**Evidence:** ${trim(log.evidence)}`);
return modLog.join(`\n`);
}
@@ -70,11 +71,11 @@ export default class ModlogCommand extends BushCommand {
(chunk) =>
new MessageEmbed({
title: `${foundUser.tag}'s Mod Logs`,
- description: chunk.join('\n**―――――――――――――――――――――――――――**\n'),
+ description: chunk.join('\n━━━━━━━━━━━━━━━\n'),
color: util.colors.default
})
);
- return await util.buttonPaginate(message, embedPages, '', true);
+ return await util.buttonPaginate(message, embedPages, undefined, true);
} else if (search) {
const entry = await ModLog.findByPk(search as string);
if (!entry) return message.util.send(`${util.emojis.error} That modlog does not exist.`);
diff --git a/src/commands/moderation/mute.ts b/src/commands/moderation/mute.ts
index 915302e..ea2ff41 100644
--- a/src/commands/moderation/mute.ts
+++ b/src/commands/moderation/mute.ts
@@ -61,7 +61,10 @@ export default class MuteCommand extends BushCommand {
{ user, reason, force }: { user: BushUser; reason?: { duration: number; contentWithoutTime: string }; force: boolean }
): Promise<unknown> {
const member = message.guild!.members.cache.get(user.id);
- if (!member) return await message.util.reply(`${util.emojis.error} You cannot kick members that are not in the server.`);
+ if (!member)
+ return await message.util.reply(
+ `${util.emojis.error} The user you selected is not in the server or is not a valid user.`
+ );
if (!message.member) throw new Error(`message.member is null`);
const useForce = force && message.author.isOwner();
diff --git a/src/lib/extensions/discord-akairo/BushClientUtil.ts b/src/lib/extensions/discord-akairo/BushClientUtil.ts
index a7dd535..55f525b 100644
--- a/src/lib/extensions/discord-akairo/BushClientUtil.ts
+++ b/src/lib/extensions/discord-akairo/BushClientUtil.ts
@@ -13,7 +13,9 @@ import {
Global,
Guild,
ModLog,
- ModLogType
+ ModLogType,
+ Pronoun,
+ PronounCode
} from '@lib';
import { exec } from 'child_process';
import {
@@ -27,7 +29,6 @@ import {
} from 'discord-akairo';
import { APIMessage } from 'discord-api-types';
import {
- ButtonInteraction,
ColorResolvable,
CommandInteraction,
Constants,
@@ -41,13 +42,16 @@ import {
MessageOptions,
Snowflake,
TextChannel,
+ ThreadMember,
User,
+ UserResolvable,
Util as DiscordUtil
} from 'discord.js';
import got from 'got';
import humanizeDuration from 'humanize-duration';
import _ from 'lodash';
import moment from 'moment';
+import fetch from 'node-fetch';
import { inspect, InspectOptions, promisify } from 'util';
import CommandErrorListener from '../../../listeners/commands/commandError';
import { ActivePunishment, ActivePunishmentType } from '../../models/ActivePunishment';
@@ -690,7 +694,7 @@ export class BushClientUtil extends ClientUtil {
});
const style = Constants.MessageButtonStyles.PRIMARY;
- let curPage = startOn ? startOn - 1 : undefined ?? 0;
+ let curPage = startOn ? startOn - 1 : 0;
if (typeof embeds !== 'object') throw new Error('embeds must be an object');
const msg = (await message.util.reply({
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
@@ -698,11 +702,11 @@ export class BushClientUtil extends ClientUtil {
embeds: [embeds[curPage]],
components: [getPaginationRow()]
})) as Message;
- const filter = (interaction: ButtonInteraction) =>
- interaction.customId.startsWith('paginate_') && interaction.message == msg;
+ const filter = (interaction: MessageComponentInteraction) =>
+ interaction.customId.startsWith('paginate_') && interaction.message.id === msg.id;
const collector = msg.createMessageComponentCollector({ filter, time: 300000 });
collector.on('collect', async (interaction: MessageComponentInteraction) => {
- if (interaction.user.id == message.author.id || client.config.owners.includes(interaction.user.id)) {
+ if (interaction.user.id === message.author.id || client.config.owners.includes(interaction.user.id)) {
switch (interaction.customId) {
case 'paginate_beginning': {
curPage = 0;
@@ -722,7 +726,11 @@ export class BushClientUtil extends ClientUtil {
}
} else {
await interaction
- ?.update({ content: `${text ? text + '\n' : ''}Command closed by user.`, embeds: [], components: [] })
+ ?.update({
+ content: `${text ? text + '\n' : ''}Command closed by user.`,
+ embeds: [],
+ components: []
+ })
.catch(() => undefined);
}
return;
@@ -744,7 +752,13 @@ export class BushClientUtil extends ClientUtil {
});
collector.on('end', async () => {
- await msg.edit({ content: text, embeds: [embeds[curPage]], components: [getPaginationRow(true)] }).catch(() => undefined);
+ await msg
+ .edit({
+ content: text,
+ embeds: [embeds[curPage]],
+ components: [getPaginationRow(true)]
+ })
+ .catch(() => undefined);
});
async function edit(interaction: MessageComponentInteraction): Promise<void> {
@@ -766,7 +780,12 @@ export class BushClientUtil extends ClientUtil {
emoji: paginateEmojis.back,
disabled: disableAll || curPage == 0
}),
- new MessageButton({ style, customId: 'paginate_stop', emoji: paginateEmojis.stop, disabled: disableAll }),
+ new MessageButton({
+ style,
+ customId: 'paginate_stop',
+ emoji: paginateEmojis.stop,
+ disabled: disableAll
+ }),
new MessageButton({
style,
customId: 'paginate_next',
@@ -790,7 +809,8 @@ export class BushClientUtil extends ClientUtil {
const paginateEmojis = this.#paginateEmojis;
updateOptions();
const msg = (await message.util.reply(options as MessageOptions & { split?: false })) as Message;
- const filter = (interaction: ButtonInteraction) => interaction.customId == 'paginate__stop' && interaction.message == msg;
+ const filter = (interaction: MessageComponentInteraction) =>
+ interaction.customId == 'paginate__stop' && interaction.message == msg;
const collector = msg.createMessageComponentCollector({ filter, time: 300000 });
collector.on('collect', async (interaction: MessageComponentInteraction) => {
if (interaction.user.id == message.author.id || client.config.owners.includes(interaction.user.id)) {
@@ -1082,14 +1102,14 @@ export class BushClientUtil extends ClientUtil {
type: ModLogType;
user: BushGuildMemberResolvable;
moderator: BushGuildMemberResolvable;
- reason: string | undefined;
+ reason: string | undefined | null;
duration?: number;
guild: BushGuildResolvable;
},
getCaseNumber = false
): Promise<{ log: ModLog | null; caseNum: number | null }> {
- const user = client.users.resolveId(options.user)!;
- const moderator = client.users.resolveId(options.moderator)!;
+ const user = (await util.resolveNonCachedUser(options.user))!.id;
+ const moderator = (await util.resolveNonCachedUser(options.moderator))!.id;
const guild = client.guilds.resolveId(options.guild)!;
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
const duration = options.duration || undefined;
@@ -1131,10 +1151,10 @@ export class BushClientUtil extends ClientUtil {
modlog?: string;
extraInfo?: Snowflake;
}): Promise<ActivePunishment | null> {
- const expires = options.duration ? new Date(new Date().getTime() + options.duration) : undefined;
- client.console.debug(expires);
+ const expires = options.duration ? new Date(new Date().getTime() + options.duration ?? 0) : undefined;
+ client.console.debug(expires, 1);
client.console.debug(typeof expires);
- const user = client.users.resolveId(options.user)!;
+ const user = (await util.resolveNonCachedUser(options.user))!.id;
const guild = client.guilds.resolveId(options.guild)!;
const type = this.#findTypeEnum(options.type)!;
@@ -1152,7 +1172,7 @@ export class BushClientUtil extends ClientUtil {
user: BushGuildMemberResolvable;
guild: BushGuildResolvable;
}): Promise<boolean> {
- const user = client.users.resolveId(options.user);
+ const user = await util.resolveNonCachedUser(options.user);
const guild = client.guilds.resolveId(options.guild);
const type = this.#findTypeEnum(options.type);
@@ -1366,6 +1386,33 @@ export class BushClientUtil extends ClientUtil {
});
}
+ public async resolveNonCachedUser(user: UserResolvable | undefined | null): Promise<User | undefined> {
+ if (!user) return undefined;
+ const id =
+ user instanceof User || user instanceof GuildMember || user instanceof ThreadMember
+ ? user.id
+ : user instanceof Message
+ ? user.author.id
+ : typeof user === 'string'
+ ? user
+ : undefined;
+ if (!id) return undefined;
+ else return await client.users.fetch(id).catch(() => undefined);
+ }
+
+ public async getPronounsOf(user: User | Snowflake): Promise<Pronoun | undefined> {
+ const _user = await this.resolveNonCachedUser(user);
+ if (!_user) throw new Error(`Cannot find user ${user}`);
+ const apiRes: { pronouns: PronounCode } | undefined = await fetch(
+ `https://pronoundb.org/api/v1/lookup?platform=discord&id=${_user.id}`
+ ).then(async (r) => (r.ok ? ((await r.json()) as { pronouns: PronounCode }) : undefined));
+
+ if (!apiRes) return undefined;
+ if (!apiRes.pronouns) throw new Error('apiRes.pronouns is undefined');
+
+ return client.constants.pronounMapping[apiRes.pronouns];
+ }
+
//~ modified from https://stackoverflow.com/questions/31054910/get-functions-methods-of-a-class
//~ answer by Bruno Grieder
//~ public getMethods(obj: any): string {
diff --git a/src/lib/extensions/discord.js/BushBaseGuildTextChannel.ts b/src/lib/extensions/discord.js/BushBaseGuildTextChannel.ts
index 734642e..78cfada 100644
--- a/src/lib/extensions/discord.js/BushBaseGuildTextChannel.ts
+++ b/src/lib/extensions/discord.js/BushBaseGuildTextChannel.ts
@@ -1,4 +1,10 @@
-import { AllowedThreadTypeForTextChannel, BaseGuildTextChannel, Collection, Snowflake } from 'discord.js';
+import {
+ AllowedThreadTypeForNewsChannel,
+ AllowedThreadTypeForTextChannel,
+ BaseGuildTextChannel,
+ Collection,
+ Snowflake
+} from 'discord.js';
import { RawGuildChannelData } from 'discord.js/typings/rawDataTypes';
import { BushCategoryChannel, BushClient, BushGuildMember } from '../..';
import { BushGuild } from './BushGuild';
@@ -10,7 +16,7 @@ export class BushBaseGuildTextChannel extends BaseGuildTextChannel {
super(guild, data, client, immediatePatch);
}
public declare messages: BushMessageManager;
- public declare threads: BushThreadManager<AllowedThreadTypeForTextChannel>;
+ public declare threads: BushThreadManager<AllowedThreadTypeForTextChannel | AllowedThreadTypeForNewsChannel>;
public declare readonly client: BushClient;
public declare guild: BushGuild;
public declare readonly members: Collection<Snowflake, BushGuildMember>;
diff --git a/src/lib/extensions/discord.js/BushGuild.ts b/src/lib/extensions/discord.js/BushGuild.ts
index 09e355c..12db49a 100644
--- a/src/lib/extensions/discord.js/BushGuild.ts
+++ b/src/lib/extensions/discord.js/BushGuild.ts
@@ -1,4 +1,4 @@
-import { Guild } from 'discord.js';
+import { Guild, UserResolvable } from 'discord.js';
import { RawGuildData } from 'discord.js/typings/rawDataTypes';
import { Guild as GuildDB, GuildFeatures, GuildModel } from '../../models/Guild';
import { ModLogType } from '../../models/ModLog';
@@ -52,9 +52,56 @@ export class BushGuild extends Guild {
return await row.save();
}
+ public async ban(options: {
+ user: BushUserResolvable | UserResolvable;
+ reason?: string | null;
+ moderator?: BushUserResolvable;
+ duration?: number;
+ deleteDays?: number;
+ }): Promise<
+ 'success' | 'missing permissions' | 'error banning' | 'error creating modlog entry' | 'error creating ban entry'
+ > {
+ // checks
+ if (!this.me!.permissions.has('BAN_MEMBERS')) return 'missing permissions';
+
+ const moderator = (await util.resolveNonCachedUser(options.moderator!)) ?? client.user!;
+
+ // ban
+ const banSuccess = await this.bans
+ .create(options.user, {
+ reason: `${moderator.tag} | ${options.reason ?? 'No reason provided.'}`,
+ days: options.deleteDays
+ })
+ .catch(() => false);
+ if (!banSuccess) return 'error banning';
+
+ // add modlog entry
+ const { log: modlog } = await util.createModLogEntry({
+ type: options.duration ? ModLogType.TEMP_BAN : ModLogType.PERM_BAN,
+ user: options.user as BushUserResolvable,
+ moderator: moderator.id,
+ reason: options.reason,
+ duration: options.duration,
+ guild: this
+ });
+ if (!modlog) return 'error creating modlog entry';
+
+ // add punishment entry so they can be unbanned later
+ const punishmentEntrySuccess = await util.createPunishmentEntry({
+ type: 'ban',
+ user: options.user as BushUserResolvable,
+ guild: this,
+ duration: options.duration,
+ modlog: modlog.id
+ });
+ if (!punishmentEntrySuccess) return 'error creating ban entry';
+
+ return 'success';
+ }
+
public async unban(options: {
user: BushUserResolvable | BushUser;
- reason?: string;
+ reason?: string | null;
moderator?: BushUserResolvable;
}): Promise<
| 'success'
@@ -64,13 +111,13 @@ export class BushGuild extends Guild {
| 'error creating modlog entry'
| 'error removing ban entry'
> {
- const user = client.users.resolveId(options.user)!;
- const moderator = client.users.cache.get(client.users.resolveId(options.moderator!)!)!;
+ const user = (await util.resolveNonCachedUser(options.user))!;
+ const moderator = (await util.resolveNonCachedUser(options.moderator ?? this.me))!;
const bans = await this.bans.fetch();
let notBanned = false;
- if (!bans.has(user)) notBanned = true;
+ if (!bans.has(user.id)) notBanned = true;
const unbanSuccess = await this.bans
.remove(user, `${moderator.tag} | ${options.reason ?? 'No reason provided.'}`)
@@ -86,7 +133,7 @@ export class BushGuild extends Guild {
// add modlog entry
const modlog = await util.createModLogEntry({
type: ModLogType.UNBAN,
- user,
+ user: user.id,
moderator: moderator.id,
reason: options.reason,
guild: this
@@ -96,12 +143,12 @@ export class BushGuild extends Guild {
// remove punishment entry
const removePunishmentEntrySuccess = await util.removePunishmentEntry({
type: 'ban',
- user,
+ user: user.id,
guild: this
});
if (!removePunishmentEntrySuccess) return 'error removing ban entry';
- const userObject = client.users.cache.get(user);
+ const userObject = client.users.cache.get(user.id);
userObject?.send(`You have been unbanned from **${this}** for **${options.reason ?? 'No reason provided'}**.`);
diff --git a/src/lib/extensions/discord.js/BushGuildMember.ts b/src/lib/extensions/discord.js/BushGuildMember.ts
index f71a435..67fa2fa 100644
--- a/src/lib/extensions/discord.js/BushGuildMember.ts
+++ b/src/lib/extensions/discord.js/BushGuildMember.ts
@@ -8,7 +8,7 @@ import { BushRole } from './BushRole';
import { BushUser } from './BushUser';
interface BushPunishmentOptions {
- reason?: string;
+ reason?: string | null;
moderator?: BushUserResolvable;
}
@@ -87,7 +87,8 @@ export class BushGuildMember extends GuildMember {
}
public async warn(options: BushPunishmentOptions): Promise<{ result: WarnResponse | null; caseNum: number | null }> {
- const moderator = client.users.cache.get(client.users.resolveId(options.moderator!)!) ?? client.user!;
+ const moderator = (await util.resolveNonCachedUser(options.moderator ?? this.guild.me))!;
+
// add modlog entry
const result = await util.createModLogEntry(
{
@@ -119,7 +120,7 @@ export class BushGuildMember extends GuildMember {
const ifShouldAddRole = this.#checkIfShouldAddRole(options.role);
if (ifShouldAddRole !== true) return ifShouldAddRole;
- const moderator = client.users.cache.get(client.users.resolveId(options.moderator!)!) ?? client.user!;
+ const moderator = (await util.resolveNonCachedUser(options.moderator ?? this.guild.me))!;
if (options.addToModlog || options.duration) {
const { log: modlog } = options.addToModlog
@@ -159,7 +160,7 @@ export class BushGuildMember extends GuildMember {
const ifShouldAddRole = this.#checkIfShouldAddRole(options.role);
if (ifShouldAddRole !== true) return ifShouldAddRole;
- const moderator = client.users.cache.get(client.users.resolveId(options.moderator!)!) ?? client.user!;
+ const moderator = (await util.resolveNonCachedUser(options.moderator ?? this.guild.me))!;
if (options.addToModlog) {
const { log: modlog } = await util.createModLogEntry({
@@ -207,7 +208,7 @@ export class BushGuildMember extends GuildMember {
if (!muteRole) return 'invalid mute role';
if (muteRole.position >= this.guild.me!.roles.highest.position || muteRole.managed) return 'mute role not manageable';
- const moderator = client.users.cache.get(client.users.resolveId(options.moderator!)!) ?? client.user!;
+ const moderator = (await util.resolveNonCachedUser(options.moderator ?? this.guild.me))!;
// add role
const muteSuccess = await this.roles
@@ -264,7 +265,7 @@ export class BushGuildMember extends GuildMember {
if (!muteRole) return 'invalid mute role';
if (muteRole.position >= this.guild.me!.roles.highest.position || muteRole.managed) return 'mute role not manageable';
- const moderator = client.users.cache.get(client.users.resolveId(options.moderator!)!) ?? client.user!;
+ const moderator = (await util.resolveNonCachedUser(options.moderator ?? this.guild.me))!;
//remove role
const muteSuccess = await this.roles
@@ -309,7 +310,7 @@ export class BushGuildMember extends GuildMember {
// checks
if (!this.guild.me?.permissions.has('KICK_MEMBERS') || !this.kickable) return 'missing permissions';
- const moderator = client.users.cache.get(client.users.resolveId(options.moderator!)!) ?? client.user!;
+ const moderator = (await util.resolveNonCachedUser(options.moderator ?? this.guild.me))!;
// dm user
const ending = await this.guild.getSetting('punishmentEnding');
@@ -340,7 +341,7 @@ export class BushGuildMember extends GuildMember {
// checks
if (!this.guild.me!.permissions.has('BAN_MEMBERS') || !this.bannable) return 'missing permissions';
- const moderator = client.users.cache.get(client.users.resolveId(options.moderator!)!) ?? client.user!;
+ const moderator = (await util.resolveNonCachedUser(options.moderator ?? this.guild.me))!;
// dm user
const ending = await this.guild.getSetting('punishmentEnding');
diff --git a/src/lib/extensions/discord.js/BushNewsChannel.ts b/src/lib/extensions/discord.js/BushNewsChannel.ts
index f44ff4a..d770132 100644
--- a/src/lib/extensions/discord.js/BushNewsChannel.ts
+++ b/src/lib/extensions/discord.js/BushNewsChannel.ts
@@ -1,4 +1,4 @@
-import { AllowedThreadTypeForTextChannel, Collection, NewsChannel, Snowflake } from 'discord.js';
+import { AllowedThreadTypeForNewsChannel, Collection, NewsChannel, Snowflake } from 'discord.js';
import { BushClient } from '../discord-akairo/BushClient';
import { BushGuild } from './BushGuild';
import { BushGuildMember } from './BushGuildMember';
@@ -7,8 +7,8 @@ import { BushThreadManager } from './BushThreadManager';
export class BushNewsChannel extends NewsChannel {
public declare readonly client: BushClient;
+ public declare threads: BushThreadManager<AllowedThreadTypeForNewsChannel>;
public declare guild: BushGuild;
public declare messages: BushMessageManager;
public declare members: Collection<Snowflake, BushGuildMember>;
- public declare threads: BushThreadManager<AllowedThreadTypeForTextChannel>;
}
diff --git a/src/lib/extensions/discord.js/BushTextChannel.ts b/src/lib/extensions/discord.js/BushTextChannel.ts
index db3ad8f..1d4d7fe 100644
--- a/src/lib/extensions/discord.js/BushTextChannel.ts
+++ b/src/lib/extensions/discord.js/BushTextChannel.ts
@@ -1,13 +1,15 @@
-import { TextChannel } from 'discord.js';
+import { AllowedThreadTypeForTextChannel, TextChannel } from 'discord.js';
import { RawGuildChannelData } from 'discord.js/typings/rawDataTypes';
import { BushClient } from '../discord-akairo/BushClient';
import { BushGuild } from './BushGuild';
import { BushMessageManager } from './BushMessageManager';
+import { BushThreadManager } from './BushThreadManager';
export class BushTextChannel extends TextChannel {
public declare readonly client: BushClient;
public declare guild: BushGuild;
public declare messages: BushMessageManager;
+ public declare threads: BushThreadManager<AllowedThreadTypeForTextChannel>;
public constructor(guild: BushGuild, data?: RawGuildChannelData) {
super(guild, data);
}
diff --git a/src/lib/models/ModLog.ts b/src/lib/models/ModLog.ts
index 7787375..0be1ea7 100644
--- a/src/lib/models/ModLog.ts
+++ b/src/lib/models/ModLog.ts
@@ -37,7 +37,7 @@ export interface ModLogModelCreationAttributes {
type: ModLogType;
user: Snowflake;
moderator: Snowflake;
- reason?: string;
+ reason?: string | null;
duration?: number;
guild: Snowflake;
evidence?: string;
diff --git a/src/lib/utils/BushConstants.ts b/src/lib/utils/BushConstants.ts
index f2ca327..17880cb 100644
--- a/src/lib/utils/BushConstants.ts
+++ b/src/lib/utils/BushConstants.ts
@@ -22,6 +22,51 @@ interface bushColors {
orange: '#E86100';
}
+export type PronounCode =
+ | 'unspecified'
+ | 'hh'
+ | 'hi'
+ | 'hs'
+ | 'ht'
+ | 'ih'
+ | 'ii'
+ | 'is'
+ | 'it'
+ | 'shh'
+ | 'sh'
+ | 'si'
+ | 'st'
+ | 'th'
+ | 'ti'
+ | 'ts'
+ | 'tt'
+ | 'any'
+ | 'other'
+ | 'ask'
+ | 'avoid';
+export type Pronoun =
+ | 'Unspecified'
+ | 'He/Him'
+ | 'He/It'
+ | 'He/She'
+ | 'He/They'
+ | 'It/Him'
+ | 'It/Its'
+ | 'It/She'
+ | 'It/They'
+ | 'She/He'
+ | 'She/Her'
+ | 'She/It'
+ | 'She/They'
+ | 'They/He'
+ | 'They/It'
+ | 'They/She'
+ | 'They/Them'
+ | 'Any pronouns'
+ | 'Other pronouns'
+ | 'Ask me my pronouns'
+ | 'Avoid pronouns, use my name';
+
export class BushConstants {
public static emojis = {
success: '<:success:837109864101707807>',
@@ -108,6 +153,30 @@ export class BushConstants {
discordEmoji: /<a?:(?<name>[a-zA-Z0-9\_]+):(?<id>\d{15,21})>/im
};
+ public static pronounMapping: { [x in PronounCode]: Pronoun } = {
+ unspecified: 'Unspecified',
+ hh: 'He/Him',
+ hi: 'He/It',
+ hs: 'He/She',
+ ht: 'He/They',
+ ih: 'It/Him',
+ ii: 'It/Its',
+ is: 'It/She',
+ it: 'It/They',
+ shh: 'She/He',
+ sh: 'She/Her',
+ si: 'She/It',
+ st: 'She/They',
+ th: 'They/He',
+ ti: 'They/It',
+ ts: 'They/She',
+ tt: 'They/Them',
+ any: 'Any pronouns',
+ other: 'Other pronouns',
+ ask: 'Ask me my pronouns',
+ avoid: 'Avoid pronouns, use my name'
+ };
+
/** A bunch of mappings */
public static mappings = {
guilds: {
diff --git a/yarn.lock b/yarn.lock
index 59e2c70..061b369 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -359,9 +359,9 @@ __metadata:
linkType: hard
"@types/node@npm:*":
- version: 16.7.6
- resolution: "@types/node@npm:16.7.6"
- checksum: a8533386a1d4ca0ed67885413001af8789c63948df288f3d36e31bd8fccffacf5dffb95e190c8cd57bb40385f010fb9a30f596bad6bb26b2bb88737d54d8ed95
+ version: 16.7.8
+ resolution: "@types/node@npm:16.7.8"
+ checksum: 060ea222ce8f3eb05bd86a7785ca5807503a50602dd805c5be997a5ae684fa6224c9ad8890bcc5551c05d14a8bcd735f94567691342d197f5f7f7f893ed0d46b
languageName: node
linkType: hard
@@ -436,11 +436,11 @@ __metadata:
linkType: hard
"@typescript-eslint/eslint-plugin@npm:^4.14.1":
- version: 4.29.3
- resolution: "@typescript-eslint/eslint-plugin@npm:4.29.3"
+ version: 4.30.0
+ resolution: "@typescript-eslint/eslint-plugin@npm:4.30.0"
dependencies:
- "@typescript-eslint/experimental-utils": 4.29.3
- "@typescript-eslint/scope-manager": 4.29.3
+ "@typescript-eslint/experimental-utils": 4.30.0
+ "@typescript-eslint/scope-manager": 4.30.0
debug: ^4.3.1
functional-red-black-tree: ^1.0.1
regexpp: ^3.1.0
@@ -452,66 +452,66 @@ __metadata:
peerDependenciesMeta:
typescript:
optional: true
- checksum: ded1580fc6348848df3ed92d4365586bf13a05cd698c07aa7727155ca13788b5c33bd326b5435af3e97b702156b1eef811ace20fb5ca44eab6388cecfd8e264a
+ checksum: 131079d73537d960e8bfbf56df9687e0eaa9b0b6791a029d2f4fd367ebe8db56086361e5ebff74be95bef40855f1dd3f9145bd1b02d48e547a9896256e4648f7
languageName: node
linkType: hard
-"@typescript-eslint/experimental-utils@npm:4.29.3":
- version: 4.29.3
- resolution: "@typescript-eslint/experimental-utils@npm:4.29.3"
+"@typescript-eslint/experimental-utils@npm:4.30.0":
+ version: 4.30.0
+ resolution: "@typescript-eslint/experimental-utils@npm:4.30.0"
dependencies:
"@types/json-schema": ^7.0.7
- "@typescript-eslint/scope-manager": 4.29.3
- "@typescript-eslint/types": 4.29.3
- "@typescript-eslint/typescript-estree": 4.29.3
+ "@typescript-eslint/scope-manager": 4.30.0
+ "@typescript-eslint/types": 4.30.0
+ "@typescript-eslint/typescript-estree": 4.30.0
eslint-scope: ^5.1.1
eslint-utils: ^3.0.0
peerDependencies:
eslint: "*"
- checksum: 7cd398bf3fccee1c769006c9d28fc0a353c2978cbc33e21449d186ab413ccf5f731b3ac30f557550c1daac767a5b97dce15ec10fe9ad5a632846d285dafac5b0
+ checksum: 2f63ec6a463edd4e41669febb16160590044a4a065b3d9badebcbe97b1725d1264390135cfe5cc3deb567df7b086f810763b761eddca5d901b9bdf1a462b03b3
languageName: node
linkType: hard
"@typescript-eslint/parser@npm:^4.14.1":
- version: 4.29.3
- resolution: "@typescript-eslint/parser@npm:4.29.3"
+ version: 4.30.0
+ resolution: "@typescript-eslint/parser@npm:4.30.0"
dependencies:
- "@typescript-eslint/scope-manager": 4.29.3
- "@typescript-eslint/types": 4.29.3
- "@typescript-eslint/typescript-estree": 4.29.3
+ "@typescript-eslint/scope-manager": 4.30.0
+ "@typescript-eslint/types": 4.30.0
+ "@typescript-eslint/typescript-estree": 4.30.0
debug: ^4.3.1
peerDependencies:
eslint: ^5.0.0 || ^6.0.0 || ^7.0.0
peerDependenciesMeta:
typescript:
optional: true
- checksum: 3fac6b5219de8b9aea361cc2fa170105661068d5eee5594f2f68526801a66b9525a766fc17427a8d410ada0da2d852f8c021d0b2fac7442a1e913f248ac85d90
+ checksum: aed2048c5eade38a57db50f551245b72cd460c3f8c38f51f3b62ce7715d43b691a91bf277b0a4f5b5e0383fb5d8a93ff0501187e2454eb6d128da34ef802e3ab
languageName: node
linkType: hard
-"@typescript-eslint/scope-manager@npm:4.29.3":
- version: 4.29.3
- resolution: "@typescript-eslint/scope-manager@npm:4.29.3"
+"@typescript-eslint/scope-manager@npm:4.30.0":
+ version: 4.30.0
+ resolution: "@typescript-eslint/scope-manager@npm:4.30.0"
dependencies:
- "@typescript-eslint/types": 4.29.3
- "@typescript-eslint/visitor-keys": 4.29.3
- checksum: 53a4d3cd0844df789ad3548644d9214cf234ce87bbc7843c55949f63e98925b4685b36f0514afbab891b4f8f0da85c249850023be5d5e9b175780aa62d181aac
+ "@typescript-eslint/types": 4.30.0
+ "@typescript-eslint/visitor-keys": 4.30.0
+ checksum: 7756f13cb5fc103e61c765ad8d15249804cec8c13c2051a63c0d543307ab178052f5bcb4a19cb50df281b4097b728f33ba92c83b670a091a51a69795ac8e3b86
languageName: node
linkType: hard
-"@typescript-eslint/types@npm:4.29.3":
- version: 4.29.3
- resolution: "@typescript-eslint/types@npm:4.29.3"
- checksum: 26fd2bd6782b763ff6d5ef3bcc08e1d29b64d15ef6f3604203f6171517935d822c103f803d8755c8e0cb77319143e5d5108dc90e8e897c8e72bab9f178be67ce
+"@typescript-eslint/types@npm:4.30.0":
+ version: 4.30.0
+ resolution: "@typescript-eslint/types@npm:4.30.0"
+ checksum: 1be5c9a30c1e552b5b215fe1ca38f3ac48ea68a06c103eb498d3a987b8cbdbe4770992d4498ab28e216fa9b2d2806ee3c856cc97cbca1a7f01e8032537bd6bef
languageName: node
linkType: hard
-"@typescript-eslint/typescript-estree@npm:4.29.3":
- version: 4.29.3
- resolution: "@typescript-eslint/typescript-estree@npm:4.29.3"
+"@typescript-eslint/typescript-estree@npm:4.30.0":
+ version: 4.30.0
+ resolution: "@typescript-eslint/typescript-estree@npm:4.30.0"
dependencies:
- "@typescript-eslint/types": 4.29.3
- "@typescript-eslint/visitor-keys": 4.29.3
+ "@typescript-eslint/types": 4.30.0
+ "@typescript-eslint/visitor-keys": 4.30.0
debug: ^4.3.1
globby: ^11.0.3
is-glob: ^4.0.1
@@ -520,17 +520,17 @@ __metadata:
peerDependenciesMeta:
typescript:
optional: true
- checksum: b7ea37db1a2f43806bf16090dfb44c7243ad07b7cb75d398fc2a1ce347fa04a59a5c729a41d1e34862cc3ed60275f5565fe3343393df1c42d95395ed42c761f0
+ checksum: e35410e33b5afc010f8bde683f4fd4f9fbe3ad821e916eb5ff1ed23943e6202a817a4cb21eb8e697a55f01182ceecaca1731cb98791d767f7fe67b11f84f3d67
languageName: node
linkType: hard
-"@typescript-eslint/visitor-keys@npm:4.29.3":
- version: 4.29.3
- resolution: "@typescript-eslint/visitor-keys@npm:4.29.3"
+"@typescript-eslint/visitor-keys@npm:4.30.0":
+ version: 4.30.0
+ resolution: "@typescript-eslint/visitor-keys@npm:4.30.0"
dependencies:
- "@typescript-eslint/types": 4.29.3
+ "@typescript-eslint/types": 4.30.0
eslint-visitor-keys: ^2.0.0
- checksum: 76d485cb573cfccb8a6aded5b98fd58266c10f82362685d3d0b870e197cbe5e3d61b485e220a7a973765c4861df9ea52a35757ecb818f125e405925556ee1f90
+ checksum: b825d1340c3209390c39b31f67c995b5a25db716088822c5d056889b700fffb4cd9acb89d75a26db034e52fdbc67d0f7fbfc03ce502f52bcd300ce2379a9aaac
languageName: node
linkType: hard
@@ -1117,12 +1117,12 @@ discord-akairo-message-util@NotEnoughUpdates/discord-akairo-message-util:
discord-akairo@NotEnoughUpdates/discord-akairo:
version: 8.2.2
- resolution: "discord-akairo@https://github.com/NotEnoughUpdates/discord-akairo.git#commit=8c34349a3eb03164e34bcf538787cce259c76aa9"
+ resolution: "discord-akairo@https://github.com/NotEnoughUpdates/discord-akairo.git#commit=8da50867c05265aa01fbc08fd510af4c81f64651"
dependencies:
discord-akairo-message-util: NotEnoughUpdates/discord-akairo-message-util
lodash: ^4.17.21
source-map-support: ^0.5.19
- checksum: 7b71844b4955fed0f383b59e23770e526140459dc55b3a8af4707bd6be7a9295c2ac6812df47282a5b922a50f4569b597151f21f893ecce7ef9b1e8b87f88a64
+ checksum: c96a613e59726a25a65c8a15e24850447d1c43da7ab52e4a88c1eee55b3a981a98a41f2c41e6e3fad7a3865566f4345f92211d6d4b4198059e88fe9e2c247203
languageName: node
linkType: hard
@@ -1142,7 +1142,7 @@ discord-akairo@NotEnoughUpdates/discord-akairo:
discord.js@NotEnoughUpdates/discord.js:
version: 13.2.0-dev
- resolution: "discord.js@https://github.com/NotEnoughUpdates/discord.js.git#commit=a1764c7ece9e6dc83484a1bf2f0ac9ca5a501d0a"
+ resolution: "discord.js@https://github.com/NotEnoughUpdates/discord.js.git#commit=adfd990b6de491c01adb414b231a7a5f3c62042e"
dependencies:
"@discordjs/builders": ^0.5.0
"@discordjs/collection": ^0.2.1
@@ -1152,7 +1152,7 @@ discord.js@NotEnoughUpdates/discord.js:
discord-api-types: ^0.22.0
node-fetch: ^2.6.1
ws: ^7.5.1
- checksum: 54d4a52cba0358317b9c1d2d1d55cdd66987a2489db3a7443dae5220f3a7f532fddf9cd5c8c1d6e8e2f6d1cdcd9b95cf6dc481208724f11d437ddd6c51dc4f4b
+ checksum: 5bb831df1083ed06ce59fd1500158981f6697d15194b46840d133e1ee32659291c497bb7bddcdabe1f0ce9cf5129febd07f2956039f842e21efa7a8c7bba25e4
languageName: node
linkType: hard
@@ -2702,9 +2702,9 @@ discord.js@NotEnoughUpdates/discord.js:
linkType: hard
"resolve-alpn@npm:^1.0.0":
- version: 1.2.0
- resolution: "resolve-alpn@npm:1.2.0"
- checksum: a38b5bf2084d384586fe15b31735396cff4640cbe137c87ffd1dcc94dcdc0c743e6875ec4baa7a9eed460355a46e3f9ebafa4cdbb8122264ed5f4e3260f69d0c
+ version: 1.2.1
+ resolution: "resolve-alpn@npm:1.2.1"
+ checksum: f558071fcb2c60b04054c99aebd572a2af97ef64128d59bef7ab73bd50d896a222a056de40ffc545b633d99b304c259ea9d0c06830d5c867c34f0bfa60b8eae0
languageName: node
linkType: hard