Skip to content

Commit

Permalink
Merge pull request #72 from net-tech/v3.0.0
Browse files Browse the repository at this point in the history
  • Loading branch information
net-tech authored Mar 3, 2024
2 parents 93e78cf + 30995d7 commit 4fb2333
Show file tree
Hide file tree
Showing 14 changed files with 141 additions and 80 deletions.
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

0 comments on commit 4fb2333

Please sign in to comment.