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

v2.0.1 #56

Merged
merged 7 commits into from
Nov 16, 2023
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
15 changes: 15 additions & 0 deletions CHANGELOG.MD
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# Changelog

## Version 2.0.1
- Fetch guilds which can be pruned from the database and loop over them, instead of fetching all guilds the client is in and then fetching the settings of each guild from the database.
- Add `select` to database functions.
- Disable two more caches.
-

## Version 2.0.0
- Rewrite to use npx create-discord-bot.
- Disable caches
- Support any number of days

## Versions <1.0.0
Changelogs not available.
Binary file modified bun.lockb
Binary file not shown.
76 changes: 38 additions & 38 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,40 +1,40 @@
{
"name": "auto-pruner",
"author": "net-tech-",
"description": "AutoPruner is a simple Discord bot that allows you to prune members on a customizable interval.",
"license": "MIT",
"version": "2.0.0",
"private": true,
"type": "module",
"scripts": {
"typecheck": "tsc --noEmit",
"start": "bun run ./src/index.ts",
"db:generate": "prisma generate",
"deploy": "bun run ./src/util/deploy.ts",
"format": "biome format . --write",
"lint": "biome check . --apply",
"lint:unsafe": "biome check . --apply-unsafe",
"pretty": "biome format . --write && biome check . --apply"
},
"dependencies": {
"@biomejs/biome": "^1.3.3",
"@discordjs/core": "^1.0.1",
"@prisma/client": "^5.5.2",
"croner": "^7.0.2",
"discord.js": "^14.13.0",
"human-interval": "^2.0.1",
"ms": "^2.1.3",
"nanoid": "^5.0.1",
"pino": "^8.15.7",
"pino-pretty": "^10.2.3",
"zlib-sync": "^0.1.8"
},
"devDependencies": {
"@types/ms": "^0.7.32",
"@types/node": "^20.8.10",
"bun-types": "latest",
"prisma": "^5.5.2",
"ts-node": "^10.9.1",
"typescript": "^5.2.2"
}
"name": "auto-pruner",
"author": "net-tech-",
"description": "AutoPruner is a simple Discord bot that allows you to prune members on a customizable interval.",
"license": "MIT",
"version": "2.0.1",
"private": true,
"type": "module",
"scripts": {
"typecheck": "tsc --noEmit",
"start": "bun run ./src/index.ts",
"db:generate": "prisma generate",
"deploy": "bun run ./src/util/deploy.ts",
"format": "biome format . --write",
"lint": "biome check . --apply",
"lint:unsafe": "biome check . --apply-unsafe",
"pretty": "biome format . --write && biome check . --apply"
},
"dependencies": {
"@biomejs/biome": "^1.3.3",
"@discordjs/core": "^1.0.1",
"@prisma/client": "^5.5.2",
"croner": "^7.0.2",
"discord.js": "^14.13.0",
"human-interval": "^2.0.1",
"ms": "^2.1.3",
"nanoid": "^5.0.1",
"pino": "^8.15.7",
"pino-pretty": "^10.2.3",
"zlib-sync": "^0.1.8"
},
"devDependencies": {
"@types/ms": "^0.7.32",
"@types/node": "^20.8.10",
"bun-types": "latest",
"prisma": "^5.5.2",
"ts-node": "^10.9.1",
"typescript": "^5.2.2"
}
}
2 changes: 1 addition & 1 deletion src/commands/settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -169,7 +169,7 @@ export default {
return
})

const guildData = await getGuildData(interaction.guild.id)
const guildData = await getGuildData(interaction.guild.id, true)

const settingsEmbed = new EmbedBuilder()
.setTitle("Server Settings")
Expand Down
3 changes: 2 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,8 @@ const client = new Client({
GuildInviteManager: 0,
GuildScheduledEventManager: 0,
GuildStickerManager: 0,
BaseGuildEmojiManager: 0
BaseGuildEmojiManager: 0,
DMMessageManager: 0
}),
allowedMentions: {
parse: ["users"],
Expand Down
79 changes: 48 additions & 31 deletions src/jobs/prune.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Cron } from "croner"
import { type Client, DiscordAPIError } from "discord.js"
import { getGuildData, updateGuildLastPrune } from "../util/database.js"
import { prisma, updateGuildLastPrune } from "../util/database.js"
import { logger } from "../util/logger.js"
import {
postPruneLogErrorMessage,
Expand All @@ -9,52 +9,69 @@ import {

const pruneJob = async (client: Client) => {
logger.info("[CRON] Starting prune job...")
const guilds = client.guilds.cache
for (const guild of guilds.values()) {
const guildSettings = await getGuildData(guild.id)
if (!guildSettings) {
logger.error(`Guild ${guild.id} not found in database.`)
continue

const guilds = await prisma.guild.findMany({
where: {
enabled: true,
days: {
gte: 1,
lte: 30
},
interval: {
not: null,
gte: new Date(86_400_000),
lt: new Date(365 * 10 * 86_400_000)
}
},
include: {
roles: true
}
if (!guildSettings.enabled) continue
if (!guildSettings.interval) continue
if (!guildSettings.days) continue
})

if (guildSettings.lastPrune) {
const lastPrune = new Date(guildSettings.lastPrune)
for (const guildSetting of guilds) {
if (guildSetting.lastPrune && guildSetting.interval) {
const lastPrune = new Date(guildSetting.lastPrune)
// Minus 5 seconds just to not have false positives.
if (
lastPrune.getTime() + guildSettings.interval.getTime() >
lastPrune.getTime() + guildSetting.interval?.getTime() >
Date.now() - 5000
) {
logger.info(
`Skipping prune for guild ${guild.id} because it was pruned recently.`
`Skipping prune for guild ${guildSetting.id} because it was pruned recently.`
)
continue
}
}

await guild.members
const clientGuild = client.guilds.cache.get(guildSetting.id)
if (!clientGuild) {
logger.warn(
`Skipping prune for guild ${guildSetting.id} because I am not in it.`
)
continue
}

await clientGuild.members
.prune({
days: guildSettings.days,
count: guild.memberCount > 10_000 ? false : true,
roles: guildSettings.roles.map((role) => role.id),
days: guildSetting.days,
count: clientGuild.memberCount > 10_000 ? false : true,
roles: guildSetting.roles.map((role) => role.id),
reason: "Scheduled guild prune"
})
.then((pruned: number | undefined | null) => {
updateGuildLastPrune(guild.id, new Date())
updateGuildLastPrune(guildSetting.id, new Date())

if (guildSettings.logChannelId) {
postPruneLogSuccessMessage(guild, guildSettings.logChannelId, {
guildId: guild.id,
if (guildSetting.logChannelId) {
postPruneLogSuccessMessage(clientGuild, guildSetting.logChannelId, {
guildId: guildSetting.id,
pruneCount: pruned ?? undefined,
roles: guildSettings.roles.map((role) => role.id),
days: guildSettings.days,
roles: guildSetting.roles.map((role) => role.id),
days: guildSetting.days,
date: new Date()
}).catch((error) => {
if (error.error && error.code === 50001) {
logger.warn(
`Skipping prune log for guild ${guild.id} because I do not have access to the log channel.`
`Skipping prune log for guild ${guildSetting.id} because I do not have access to the log channel.`
)
}
logger.error(error, "Error sending prune log message")
Expand All @@ -64,23 +81,23 @@ const pruneJob = async (client: Client) => {
.catch((error) => {
logger.error(error, "Error pruning guild")
if (error instanceof DiscordAPIError) {
if (guildSettings.logChannelId) {
if (guildSetting.logChannelId) {
if (error.code === 50013) {
postPruneLogErrorMessage(
guild,
guildSettings.logChannelId,
clientGuild,
guildSetting.logChannelId,
"I do not have permission to prune members in this guild. Please check that I have the 'Kick Members' permission."
)
return
}
postPruneLogErrorMessage(
guild,
guildSettings.logChannelId,
clientGuild,
guildSetting.logChannelId,
`Discord API Error ${error.code}: ${error.message}`
).catch((error) => {
if (error.error && error.code === 50001) {
logger.warn(
`Skipping prune log for guild ${guild.id} because I do not have access to the log channel.`
`Skipping prune log for guild ${guildSetting.id} because I do not have access to the log channel.`
)
}
logger.error(error, "Error sending prune log message")
Expand Down
27 changes: 18 additions & 9 deletions src/util/database.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,19 @@
import { Prisma, PrismaClient } from "@prisma/client"

const prisma = new PrismaClient({
export const prisma = new PrismaClient({
errorFormat: "pretty"
})

/**
* Fetch data for a specific guild. If the guild doesn't exist in the database, a new record is created with the provided guildId.
* @param guildId - The ID of the guild to fetch data for
* @param select - The fields to select from the database. If true, all fields will be selected, including the `roles` field. If false, roles will still be selected.
* @returns The data for the guild
*/
export const getGuildData = async (guildId: string) => {
export const getGuildData = async (
guildId: string,
select: Prisma.GuildSelect | true
) => {
const guildData = await prisma.guild.upsert({
where: {
id: guildId
Expand All @@ -18,9 +22,14 @@ export const getGuildData = async (guildId: string) => {
create: {
id: guildId
},
include: {
roles: true
}
select: select
? {
roles: true
}
: {
roles: true,
...(select as Prisma.GuildSelect)
}
})
return guildData
}
Expand All @@ -40,7 +49,7 @@ export const updateGuildSettings = async (
settings: Prisma.GuildCreateInput & {
roles: { reset: boolean; roles: string[] } | undefined
}
): Promise<void> => {
) => {
if (settings.roles) {
if (settings.roles.reset) {
await resetRolesForGuild(guildId)
Expand All @@ -55,7 +64,7 @@ export const updateGuildSettings = async (
roles: undefined
}

await prisma.guild.upsert({
return await prisma.guild.upsert({
where: { id: guildId },
update: upsertSettings,
create: { ...upsertSettings }
Expand All @@ -67,8 +76,8 @@ export const updateGuildSettings = async (
*
* @param guildId - The ID of the guild for which roles should be reset.
*/
const resetRolesForGuild = async (guildId: string): Promise<void> => {
await prisma.role.deleteMany({
const resetRolesForGuild = async (guildId: string) => {
return await prisma.role.deleteMany({
where: { guild: { id: guildId } }
})
}
Expand Down