Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
c1f4956
Merge: Moderation!!
doodles172 Apr 19, 2026
b19650a
Merge pull request #20 from quest-organization/dev
doodles172 Apr 19, 2026
4a4c342
Merge pull request #21 from quest-organization/dev
doodles172 Apr 19, 2026
9281700
prisma was in dev deps, fix
doodles172 Apr 19, 2026
fc48650
lockfile...
doodles172 Apr 19, 2026
3ad30af
accidentally changed from process env to env: revert
doodles172 Apr 19, 2026
df00d9f
FUC-
doodles172 Apr 19, 2026
fcf4a0a
patch: quick fix for mute/warn durations
doodles172 Apr 19, 2026
d5ba599
catch errors
doodles172 Apr 19, 2026
2d373ce
consistant schema + warn fix
doodles172 Apr 19, 2026
98f66bf
improve mute
doodles172 Apr 19, 2026
0617eec
General Improvements in formatting
doodles172 Apr 19, 2026
b59c152
fix: positioning in functions inverted
doodles172 Apr 19, 2026
0e1942c
*also for mute
doodles172 Apr 19, 2026
819a3e9
Cleanup messages!
doodles172 Apr 19, 2026
1ea4bda
fix: warn must have logical pattern
doodles172 Apr 19, 2026
0094f1e
Heartbeat + improve warn list
doodles172 Apr 19, 2026
98d87f5
ready: run on start + delete expired warns
doodles172 Apr 19, 2026
32f5d73
chore: use github to build and send to caprover instead of caprover b…
doodles172 Apr 19, 2026
0b8c48d
chore: update versions
doodles172 Apr 19, 2026
4be5d3b
Github: Add issue templates
doodles172 Apr 19, 2026
99aafca
Github: Update issue templates
doodles172 Apr 19, 2026
1654d30
feat: reminders!
doodles172 Apr 20, 2026
6aca30d
upd: improvements on reminders and auto-deletion
doodles172 Apr 20, 2026
2fab3ab
upd: presence
doodles172 Apr 20, 2026
62d4b3b
upd: increase reminder duration
doodles172 Apr 20, 2026
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
4 changes: 2 additions & 2 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ Contributing to Quest Bot requires basic knowledge of Typescript, Discord.JS and

## AI Rules

Please refrain from using AI to create new features. Using AI to help you understand something is fine but you can't have it vibecode or make features for you. This is especially important for HackClub events.
Please refrain from using AI to create new features. Using AI to help you *understand* how to make something is fine but you can't have it vibecode or make features for you. This is especially important for HackClub events.

## Pull requests

Expand All @@ -18,6 +18,6 @@ Please write in the commit message what you have changed.

## Team

You will be invited to the Quest team if you have contributed a lot towards on or more Quest projects.
You will be invited to the Quest team if you have contributed a lot towards one or more Quest projects.

## Thanks!
30 changes: 30 additions & 0 deletions .github/ISSUE_TEMPLATE/bug_report.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
---
name: Bug report
about: Create a report to help us improve
title: "[BUG] ..."
labels: bug
assignees: ''
type: Bug

---

**Describe the bug**
A clear and concise description of what the bug is.

---

**To Reproduce**
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error

**Expected behavior**
A clear and concise description of what you expected to happen.

**Screenshots**
If applicable, add screenshots to help explain your problem.

**Additional context**
Add any other context about the problem here.
21 changes: 21 additions & 0 deletions .github/ISSUE_TEMPLATE/feature_request.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
---
name: Feature request
about: Suggest an idea for this project
title: "[Feature] Add..."
labels: enhancement
assignees: ''
type: Feature

---

**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]

**Describe the solution you'd like**
A clear and concise description of what you want to happen.

**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.

**Additional context**
Add any other context or screenshots about the feature request here.
56 changes: 35 additions & 21 deletions .github/workflows/deploy.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ on:

permissions:
contents: read
packages: write

jobs:
deploy:
Expand All @@ -18,28 +19,41 @@ jobs:
(github.event.workflow_run.conclusion == 'success' &&
github.event.workflow_run.head_branch == 'main')
steps:
- uses: actions/checkout@v6
- name: Check out repository
uses: actions/checkout@v6
with:
ref: ${{ github.event.workflow_run.head_sha || github.sha }}

- uses: actions/setup-node@v6
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v4

- name: Login to Container Registry
uses: docker/login-action@v4
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}

- name: Preset Image Name
run: |
SHA="${{ github.event.workflow_run.head_sha || github.sha }}"
IMAGE=$(echo "ghcr.io/${{ github.repository_owner }}/${{ github.event.repository.name }}:$(echo ${SHA} | cut -c1-7)" | tr '[:upper:]' '[:lower:]')
echo "IMAGE_URL=${IMAGE}" >> $GITHUB_ENV

- name: Build and push Docker Image
uses: docker/build-push-action@v7
with:
context: .
file: ./Dockerfile
push: true
tags: ${{ env.IMAGE_URL }}
cache-from: type=gha
cache-to: type=gha,mode=max

- name: Deploy Image to CapRover
uses: caprover/deploy-from-github@v1.1.2
with:
node-version: '24'

- name: Install CapRover CLI
run: npm install -g caprover

- name: Create deploy tarball
run: tar -cvf deploy.tar --exclude='./.git' --exclude='./node_modules' --exclude='./dist' --exclude='./deploy.tar' .

- name: Deploy
env:
CAPROVER_URL: ${{ secrets.CAPROVER_URL }}
CAPROVER_APP_TOKEN: ${{ secrets.CAPROVER_APP_TOKEN }}
CAPROVER_APP: ${{ secrets.CAPROVER_APP }}
run: >
caprover deploy
--caproverUrl "$CAPROVER_URL"
--appToken "$CAPROVER_APP_TOKEN"
--caproverApp "$CAPROVER_APP"
--tarFile ./deploy.tar
server: "${{ secrets.CAPROVER_URL }}"
app: "${{ secrets.CAPROVER_APP }}"
token: "${{ secrets.CAPROVER_APP_TOKEN }}"
image: ${{ env.IMAGE_URL }}
3 changes: 2 additions & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,10 @@ WORKDIR /app
RUN corepack enable

COPY package.json pnpm-lock.yaml ./
COPY prisma.config.ts ./
COPY prisma ./prisma/
RUN pnpm install --prod --frozen-lockfile

COPY --from=builder /app/dist ./dist

CMD ["sh", "-c", "pnpx prisma db push && pnpm run deploy && pnpm start"]
CMD ["sh", "-c", "pnpm prisma db push && pnpm run deploy && pnpm start"]
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
"@types/ms": "^2.1.0",
"discord.js": "^14.24.0",
"dotenv": "^17.4.2",
"prisma": "^7.7.0",
"ms": "^2.1.3",
"zod": "^4.3.6"
},
Expand All @@ -30,7 +31,6 @@
"eslint-formatter-compact": "^8.40.0",
"eslint-formatter-pretty": "^7.0.0",
"prettier": "^3.6.2",
"prisma": "^7.7.0",
"rimraf": "^6.1.3",
"typescript": "~5.9.3"
},
Expand Down
6 changes: 3 additions & 3 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 7 additions & 1 deletion prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ model Server {
updatedAt DateTime @updatedAt

warns Warn[]
mutes Mute[]
reminders Reminder[]

@@map("server")
}
Expand All @@ -42,19 +44,23 @@ model Mute {
reason String?
expiresAt DateTime?
createdAt DateTime @default(now())

server Server @relation(fields: [guildId], references: [id], onDelete: Cascade)

@@unique([guildId, userId])
@@index([expiresAt])
}

model Reminder {
id String @id @default(cuid())
userId String
guildId String?
userId String
channelId String?
message String
remindAt DateTime
createdAt DateTime @default(now())

server Server? @relation(fields: [guildId], references: [id], onDelete: Cascade)

@@index([remindAt])
@@index([userId])
Expand Down
15 changes: 12 additions & 3 deletions src/commands/moderation/ban.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ export default {

const row = new ActionRowBuilder<ButtonBuilder>().addComponents(cancel, confirm);
const response = await interaction.reply({
content: `${emojis.rightArrow1} Are you sure you want to ban ${targetMember.user.username} for reason: ${reason}?`,
content: `${emojis.rightArrow1} Are you sure you want to ban <@${targetMember.user.username}> for reason: ${reason}?`,
components: [row],
withResponse: true,
});
Expand All @@ -79,11 +79,20 @@ export default {

if (confirmation.customId === 'confirm') {
await interaction.guild!.members.ban(targetMember);
await confirmation.update({ content: `${emojis.rightArrow2} ${targetMember.user.username} has been banned with reason: ${reason}`, components: [] });
await confirmation.update({ content: `${emojis.rightArrow2} <@${targetMember.user.username}> has been banned with reason: ${reason}`, components: [] });

setTimeout(() => {
interaction.deleteReply().catch(() => {});
}, 3000);
} else if (confirmation.customId === 'cancel') {
await confirmation.update({ content: `${emojis.rightArrow2} Cancelled.`, components: [] });

setTimeout(() => {
interaction.deleteReply().catch(() => {});
}, 3000);
}
} catch {
} catch (err) {
console.error(err)
await interaction.editReply({ content: `${emojis.rightArrow2} No response within a minute or errored.`, components: [] });
}
},
Expand Down
15 changes: 12 additions & 3 deletions src/commands/moderation/kick.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ export default {

const row = new ActionRowBuilder<ButtonBuilder>().addComponents(cancel, confirm);
const response = await interaction.reply({
content: `${emojis.rightArrow1} Are you sure you want to kick ${targetMember.user.username} with reason: ${reason}?`,
content: `${emojis.rightArrow1} Are you sure you want to kick <@${targetMember.user.id}> with reason: ${reason}?`,
components: [row],
withResponse: true,
});
Expand All @@ -83,11 +83,20 @@ export default {
`You have been kicked from **${interaction.guild.name}**.\nReason: ${reason}`
).catch(() => {});
await interaction.guild!.members.kick(targetMember);
await confirmation.update({ content: `${emojis.rightArrow2} **${targetMember.user.username}** has been kicked for reason: ${reason}\nYou must have had a real ick towards that person.`, components: [] });
await confirmation.update({ content: `${emojis.rightArrow2} <@${targetMember.user.id}> has been kicked for reason: ${reason}\nYou must have had a real ick towards that person.`, components: [] });

setTimeout(() => {
interaction.deleteReply().catch(() => {});
}, 3000);
} else if (confirmation.customId === 'cancel') {
await confirmation.update({ content: `${emojis.rightArrow2} Cancelled.`, components: [] });

setTimeout(() => {
interaction.deleteReply().catch(() => {});
}, 3000);
}
} catch {
} catch (err) {
console.error(err)
await interaction.editReply({ content: `${emojis.rightArrow2} No response within a minute or errored.`, components: [] });
}
},
Expand Down
33 changes: 24 additions & 9 deletions src/commands/moderation/mute.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,13 @@ export default {
},
{
type: ApplicationCommandOptionType.String,
name: "reason",
description: "Provide a reason for their mute"
name: "duration",
description: "Specify a duration for the mute"
},
{
type: ApplicationCommandOptionType.String,
name: "duration",
description: "Specify a duration for the mute"
name: "reason",
description: "Provide a reason for their mute"
}

]
Expand All @@ -44,13 +44,18 @@ export default {
const targetMember = interaction.options.getMember('target') as GuildMember
const reason = interaction.options.getString('reason') ?? 'No reason provided';
const durationStr = interaction.options.getString('duration') as StringValue
const duration = ms(durationStr) ?? 'No duration provided'
const duration = durationStr ? ms(durationStr as StringValue) : null;
const expiresAt = duration ? new Date(Date.now() + duration) : null;

if (!targetMember) {
await interaction.reply({ content: `${emojis.rightArrow2} That user is not in this server.`, flags: MessageFlags.Ephemeral });
return;
}

if (durationStr && (typeof duration !== 'number' || isNaN(duration))) {
await interaction.reply({ content: `${emojis.rightArrow2} Invalid duration format.`, flags: MessageFlags.Ephemeral });
return;
}

if (targetMember.id === interaction.user.id) {
await interaction.reply({ content: `${emojis.rightArrow2} You cannot mute yourself.`, flags: MessageFlags.Ephemeral });
Expand Down Expand Up @@ -79,7 +84,7 @@ export default {

const row = new ActionRowBuilder<ButtonBuilder>().addComponents(cancel, confirm);
const response = await interaction.reply({
content: `${emojis.rightArrow1} Are you sure you want to mute ${targetMember.user.username} for ${durationStr} with reason: ${reason}?`,
content: `${emojis.rightArrow1} Are you sure you want to mute <@${targetMember.user.id}> with reason: ${reason}?`,
components: [row],
withResponse: true,
});
Expand All @@ -89,13 +94,23 @@ export default {
const confirmation = await response.resource!.message!.awaitMessageComponent({ filter: collectorFilter, time: 60_000 });

if (confirmation.customId === 'confirm') {
await createMute(interaction.guild.id, targetMember.id, expiresAt, reason);
await createMute(interaction.guild.id, interaction.guild.name, targetMember.id, expiresAt, reason);
await enforceMute(interaction.guild, targetMember.id);
await confirmation.update({ content: `${emojis.rightArrow2} ${targetMember.user.username} has been muted for ${durationStr} with reason: ${reason}`, components: [] });
await confirmation.update({ content: `${emojis.rightArrow2} <@${targetMember.user.id}> has been muted with reason: ${reason}`, components: [] });

setTimeout(() => {
interaction.deleteReply().catch(() => {});
}, 3000);

} else if (confirmation.customId === 'cancel') {
await confirmation.update({ content: `${emojis.rightArrow2} Cancelled.`, components: [] });

setTimeout(() => {
interaction.deleteReply().catch(() => {});
}, 3000);
}
} catch {
} catch (err) {
console.error(err)
await interaction.editReply({ content: `${emojis.rightArrow2} No response within a minute or errored.`, components: [] });
}
},
Expand Down
12 changes: 10 additions & 2 deletions src/commands/moderation/unmute.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ export default {

const row = new ActionRowBuilder<ButtonBuilder>().addComponents(cancel, confirm);
const response = await interaction.reply({
content: `${emojis.rightArrow1} Are you sure you want to unmute ${targetMember.user.username} with reason: ${reason}?`,
content: `${emojis.rightArrow1} Are you sure you want to unmute <@${targetMember.user.id}> with reason: ${reason}?`,
components: [row],
withResponse: true,
});
Expand All @@ -72,9 +72,17 @@ export default {
if (confirmation.customId === 'confirm') {
await removeMute(interaction.guild.id, targetMember.id);
targetMember.timeout(null, reason)
await confirmation.update({ content: `${emojis.rightArrow2} ${targetMember.user.username} has been unmuted with reason: ${reason}`, components: [] });
await confirmation.update({ content: `${emojis.rightArrow2} <@${targetMember.user.id}> has been unmuted with reason: ${reason}`, components: [] });

setTimeout(() => {
interaction.deleteReply().catch(() => {});
}, 3000);
} else if (confirmation.customId === 'cancel') {
await confirmation.update({ content: `${emojis.rightArrow2} Cancelled.`, components: [] });

setTimeout(() => {
interaction.deleteReply().catch(() => {});
}, 3000);
}
} catch {
await interaction.editReply({ content: `${emojis.rightArrow2} No response within a minute or errored.`, components: [] });
Expand Down
Loading