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

v3.0.0 #72

Merged
merged 14 commits into from
Mar 3, 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
9 changes: 9 additions & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
root = true

[*.{js,ts,json}]
end_of_line = lf
insert_final_newline = true
charset = utf-8
indent_style = tab
tab_width = 2
trim_trailing_whitespace = true
23 changes: 0 additions & 23 deletions .eslintrc.yml

This file was deleted.

10 changes: 9 additions & 1 deletion CHANGELOG.MD
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
# Changelog

## Version 3.0.0
- BREAKING: Require the Manage Server permission to prune (https://github.com/discord/discord-api-docs/pull/6688).
- Warn users when there are permissions the bot needs in the server that it does not have. The warning message is displayed in `/settings`.
- Correctly determine the number of CRON jobs loaded.
- Add a `.editorconfig` file.
- Bump packages.
- Other miscellaneous changes.

## Version 2.1.0
- Bump dependencies
- Show guild count and memory usage in /about
Expand All @@ -17,4 +25,4 @@
- Support any number of days

## Versions <1.0.0
Changelogs not available.
Changelog not available.
Binary file modified bun.lockb
Binary file not shown.
11 changes: 5 additions & 6 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"author": "net-tech-",
"description": "AutoPruner is a simple Discord bot that allows you to prune members on a customizable interval.",
"license": "MIT",
"version": "2.1.0",
"version": "3.0.0",
"private": true,
"type": "module",
"scripts": {
Expand All @@ -12,29 +12,28 @@
"db:generate": "prisma generate",
"deploy": "bun run ./src/util/deploy.ts",
"format": "biome format . --write",
"lint": "biome check . --apply",
"lint": "biome lint . --apply",
"lint:unsafe": "biome check . --apply-unsafe",
"pretty": "biome format . --write && biome check . --apply"
"pretty": "biome check . --apply"
},
"dependencies": {
"@biomejs/biome": "^1.3.3",
"@discordjs/core": "^1.1.1",
"@discordjs/rest": "^2.2.0",
"@prisma/client": "^5.5.2",
"croner": "^8.0.0",
"discord.js": "^14.13.0",
"human-interval": "^2.0.1",
"ms": "^2.1.3",
"nanoid": "^5.0.1",
"pino": "^8.17.2",
"pino-pretty": "^10.2.3",
"zlib-sync": "^0.1.8"
"pino-pretty": "^10.2.3"
},
"devDependencies": {
"@types/ms": "^0.7.32",
"@types/node": "^20.10.7",
"bun-types": "latest",
"prisma": "^5.7.1",
"ts-node": "^10.9.1",
"typescript": "^5.3.3"
}
}
4 changes: 2 additions & 2 deletions src/commands/about.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import {
EmbedBuilder,
OAuth2Scopes
} from "discord.js"
import { supportServerInviteLink } from "../util/misc.js"
import { SUPPORT_SERVER_INVITE_LINK } from "../util/misc.js"
import type { Command } from "./index.js"

export default {
Expand Down Expand Up @@ -47,7 +47,7 @@ export default {
.setStyle(ButtonStyle.Link),
new ButtonBuilder()
.setLabel("Support Server")
.setURL(supportServerInviteLink)
.setURL(SUPPORT_SERVER_INVITE_LINK)
.setStyle(ButtonStyle.Link),
new ButtonBuilder()
.setLabel("Source Code")
Expand Down
59 changes: 41 additions & 18 deletions src/commands/settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,11 @@ import {
} from "discord.js"
import { getGuildData, updateGuildSettings } from "../util/database.js"
import {
RolesStringParserReturn,
colors,
guildSettings,
logChannelRequiredPermissions
COLORS,
GUILD_REQUIRED_PERMISSIONS,
GUILD_SETTINGS,
LOG_CHANNEL_REQUIRED_PERMISSIONS,
RolesStringParserReturn
} from "../util/misc.js"
import { parseInterval } from "../util/parseInterval.js"
import { getSettingDescription, parseRoles } from "../util/settings.js"
Expand Down Expand Up @@ -74,6 +75,8 @@ export default {

await interaction.deferReply()

const me = await interaction.guild.members.fetchMe()

if (roles) {
roles = parseRoles(roles)
if (!roles.reset && roles.roles.length === 0) {
Expand Down Expand Up @@ -105,16 +108,15 @@ export default {
}

if (channel) {
const me = await interaction.guild.members.fetchMe()
const permissions = channel.permissionsFor(me)
if (!permissions.has(logChannelRequiredPermissions)) {
const missing = logChannelRequiredPermissions.filter(
(permission) => !permissions.has(permission)
const channelPermissions = channel.permissionsFor(me)
if (!channelPermissions.has(LOG_CHANNEL_REQUIRED_PERMISSIONS)) {
const missing = LOG_CHANNEL_REQUIRED_PERMISSIONS.filter(
(permission) => !channelPermissions.has(permission)
)
await interaction.editReply({
content: `I am missing the following permission(s) in that channel: ${new PermissionsBitField(
missing
)
content: `I am missing the following permission${
missing.length === 1 ? "" : "s"
} in that channel: ${new PermissionsBitField(missing)
.toArray()
.join(", ")}.`
})
Expand All @@ -136,14 +138,18 @@ export default {
"The interval must be a valid time interval. E.g. `every 3 days`."
})
return
// < 1 day
} else if (interval.getTime() < 86_400_000) {
}

// < 1 day
if (interval.getTime() < 86_400_000) {
await interaction.editReply({
content: "The interval must be at least 1 day."
})
return
// >= 10 years
} else if (interval.getTime() >= 365 * 10 * 86_400_000) {
}

// >= 10 years
if (interval.getTime() >= 365 * 10 * 86_400_000) {
await interaction.editReply({
content:
"Really? You want to prune every 10+ years? The interval must be less than 10 years."
Expand Down Expand Up @@ -177,10 +183,10 @@ export default {
name: interaction.guild.name,
iconURL: interaction.guild.iconURL() ?? ""
})
.setColor(colors.embed)
.setColor(COLORS.embed)

// Process the settings and generate description
for (const setting of guildSettings) {
for (const setting of GUILD_SETTINGS) {
if (!settingsEmbed.data.description) settingsEmbed.data.description = ""
settingsEmbed.data.description += `**${
setting.name
Expand All @@ -189,6 +195,23 @@ export default {
}`
}

const guildPermissions = me.permissions
if (!guildPermissions.has(GUILD_REQUIRED_PERMISSIONS)) {
const missing = GUILD_REQUIRED_PERMISSIONS.filter(
(permission) => !guildPermissions.has(permission)
)

const descriptionSuffix = `:warning: I am missing the following permission${
missing.length === 1 ? "" : "s"
} in this server: ${new PermissionsBitField(missing)
.toArray()
.join(", ")}.`

settingsEmbed.setDescription(
`${settingsEmbed.data.description}\n${descriptionSuffix}`
)
}

settingsEmbed.data.description = settingsEmbed.data.description
?.replaceAll(/undefined|null/gim, "Not set")
.replaceAll("true", "✅")
Expand Down
2 changes: 1 addition & 1 deletion src/events/guildDelete.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Events } from "discord.js"
import { prisma } from "../util/database.ts"
import { logger } from "../util/logger.js"
import type { Event } from "./index.js"
import { prisma } from "../util/database.ts";

export default {
name: Events.GuildDelete,
Expand Down
4 changes: 3 additions & 1 deletion src/events/ready.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,9 @@ export default {
startCron(client)
logger.info(`[CRON] Started CRON "${job}"`)
}
logger.info(`[CRON] Started ${jobs.length} CRONs.`)
logger.info(
`[CRON] Started ${jobs.filter((j) => j.endsWith(".ts")).length} CRONs.`
)
} catch (error) {
logger.warn(error, "[CRON] Failed to load CRONs.")
}
Expand Down
28 changes: 19 additions & 9 deletions src/jobs/prune.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { Cron } from "croner"
import { type Client, DiscordAPIError } from "discord.js"
import ms from "ms"
import { prisma, updateGuildLastPrune } from "../util/database.js"
import { logger } from "../util/logger.js"
import {
Expand All @@ -9,18 +10,19 @@ import {

const pruneJob = async (client: Client) => {
logger.info("[CRON] Starting prune job...")
const startDate = new Date()

const guilds = await prisma.guild.findMany({
where: {
enabled: true,
days: {
gte: 1,
lte: 30
gte: 1, // Greater than 1 day
lte: 30 // Less than 30 days
},
interval: {
not: null,
gte: new Date(86_400_000),
lt: new Date(365 * 10 * 86_400_000)
gte: new Date(86_400_000), // Greater than 1 day
lt: new Date(365 * 10 * 86_400_000) // Less than 10 years
}
},
include: {
Expand All @@ -36,7 +38,7 @@ const pruneJob = async (client: Client) => {
lastPrune.getTime() + guildSetting.interval?.getTime() >
Date.now() - 5000
) {
logger.info(
logger.debug(
`Skipping prune for guild ${guildSetting.id} because it was pruned recently.`
)
continue
Expand All @@ -58,7 +60,7 @@ const pruneJob = async (client: Client) => {
roles: guildSetting.roles.map((role) => role.id),
reason: "Scheduled guild prune"
})
.then((pruned: number | undefined | null) => {
.then((pruned?: number | null) => {
updateGuildLastPrune(guildSetting.id, new Date())

if (guildSetting.logChannelId) {
Expand Down Expand Up @@ -86,7 +88,8 @@ const pruneJob = async (client: Client) => {
postPruneLogErrorMessage(
clientGuild,
guildSetting.logChannelId,
"I do not have permission to prune members in this guild. Please check that I have the 'Kick Members' permission."
"I do not have permission to prune members in this server. Please check that I have the 'Kick Members' and 'Manage Server' permissions. Discord added the 'Manage Server' permission as a prune requirement on <t:1710529200:D>.",
false
)
return
}
Expand All @@ -106,12 +109,19 @@ const pruneJob = async (client: Client) => {
}
})
}
logger.info("[CRON] Prune job finished.")
logger.info(
`[CRON] Prune job finished. Took ${ms(
new Date().getTime() - startDate.getTime(),
{
long: true
}
)}.`
)
}

const startCron = (client: Client) => {
// Every 30 minutes
Cron("*/30 * * * *", async () => {
// Every 30 minutes
await pruneJob(client).catch((error) => {
logger.error(error)
})
Expand Down
38 changes: 32 additions & 6 deletions src/util/misc.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,37 @@
import { PermissionsBitField, type Snowflake } from "discord.js"

export const logChannelRequiredPermissions: readonly bigint[] = [
export const LOG_CHANNEL_REQUIRED_PERMISSIONS: readonly bigint[] = [
// To be able to see the logging channel.
PermissionsBitField.Flags.ViewChannel,
// To be able to send log messages in the logging channel.
PermissionsBitField.Flags.SendMessages,
// To be able to send log messages in the logging channel if it is a thread.
PermissionsBitField.Flags.SendMessagesInThreads,
PermissionsBitField.Flags.EmbedLinks
// To be able to send log embeds in the logging channel.
PermissionsBitField.Flags.EmbedLinks,
// To be able to attach files to the log messages (future feature).
PermissionsBitField.Flags.AttachFiles
] as const

export const GUILD_REQUIRED_PERMISSIONS: readonly bigint[] = [
// To be able to see the logging channel.
PermissionsBitField.Flags.ViewChannel,
// To be able to send log messages in the logging channel.
PermissionsBitField.Flags.SendMessages,
// To be able to send log messages in the logging channel if it is a thread.
PermissionsBitField.Flags.SendMessagesInThreads,
// To be able to send log embeds in the logging channel.
PermissionsBitField.Flags.EmbedLinks,
// To be able to attach files to the log messages (future feature).
PermissionsBitField.Flags.AttachFiles,
// To be able to see if the guild was manually pruned (future feature).
PermissionsBitField.Flags.ViewAuditLog,
// To be able to prune members.
PermissionsBitField.Flags.ManageGuild,
// To be able to prune members.
PermissionsBitField.Flags.KickMembers
]

export interface Setting {
name: string
value: string
Expand Down Expand Up @@ -34,12 +59,12 @@ export interface ScheduledPruneInfo {
date: Date
}

export const colors = {
export const COLORS = {
red: 0xff3b30,
embed: 0x2c2d31
}
} as const

export const guildSettings: readonly Setting[] = [
export const GUILD_SETTINGS: readonly Setting[] = [
{
name: "Auto-prune enabled",
value: "enabled",
Expand Down Expand Up @@ -75,4 +100,5 @@ export const guildSettings: readonly Setting[] = [
}
] as const

export const supportServerInviteLink = "https://discord.com/invite/wAhhesqCAH"
export const SUPPORT_SERVER_INVITE_LINK =
"https://discord.com/invite/wAhhesqCAH"
3 changes: 1 addition & 2 deletions src/util/parseInterval.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ export const parseInterval = (interval: string) => {
interval = interval.replace("every ", "")
if (Number.isNaN(humanInterval(interval))) {
return ms(interval)
} else {
return humanInterval(interval) as number
}
return humanInterval(interval) as number
}
Loading