21 Commits

Author SHA1 Message Date
Chris Hall
e2787c82fb simplifying credentials 2022-12-29 15:48:36 -08:00
Chris Hall
f9dd7b125f simplifying credentials 2022-12-29 15:44:59 -08:00
Chris Hall
ec22a48000 added checkout step 2022-12-29 15:34:30 -08:00
Chris Hall
4f3d283cae updated registry 2022-12-29 15:32:51 -08:00
Chris Hall
0eacc18d7c updated image build 2022-12-29 15:29:29 -08:00
Chris Hall
b78ec26cc6 working ssh step 2022-12-29 12:50:31 -08:00
Chris Hall
09c13a21bd debugging compose 2022-09-16 11:13:09 -07:00
Chris Hall
ed4b3b42e5 debugging compose 2022-09-16 11:11:56 -07:00
Chris Hall
4291551973 added closing parenthesis 2022-09-16 11:09:22 -07:00
Chris Hall
0375f2cb8e added closing parenthesis 2022-09-16 11:08:00 -07:00
Chris Hall
d7b9297afa updated 2022-09-16 11:06:39 -07:00
Chris Hall
9edb615b56 fixed docker command 2022-09-16 10:26:53 -07:00
Chris Hall
b2d16a0e57 added clone and compose 2022-09-16 09:51:08 -07:00
Chris Hall
dea7e0a560 added clone and compose 2022-09-16 09:46:09 -07:00
Chris Hall
ff5da863df added clone and compose 2022-09-16 09:38:34 -07:00
Chris Hall
f81252cebe ignoring hostkey checking 2022-09-16 09:28:57 -07:00
Chris Hall
aa410164fe adding env variable 2022-09-16 09:24:35 -07:00
Chris Hall
77fce86936 updated with ssh-agent 2022-09-14 08:51:48 -07:00
Chris Hall
f6930d8861 updated with key 2022-09-14 08:49:02 -07:00
Chris Hall
11e9217d63 updated command 2022-09-14 08:00:14 -07:00
Chris Hall
c5edfab2de initial version 2022-09-13 09:48:17 -07:00
197 changed files with 7200 additions and 19441 deletions

View File

@@ -1,6 +1,6 @@
.idea .idea
dist dist
node_modules node_modules
tsconfig.tsbuildinfo
npm-debug.log npm-debug.log
.env .env
*.md

View File

@@ -1,20 +0,0 @@
# Tokens
DISCORD_TOKEN= # Bot token from: https://discord.com/developers/
# Configuration
DEFAULT_PREFIX= # Prefix used to run commands in Discord
DEVELOPMENT= # (true/false) Enables developer mode
# Docker
POSTGRES_USER=USERNAME
POSTGRES_PASSWORD=PASSWORD
POSTGRES_DB=DB
# Redis (if running everything within docker compose, use "redis" for the host and leave the rest empty)
REDIS_HOST= # URL to redis database
REDIS_USER= # redis database user
REDIS_PASSWORD= # redis database password
REDIS_PORT= # redis database port
# Database URL (designed for Postgres, but designed on Prisma)
DATABASE_URL= # "postgresql://USERNAME:PASSWORD@postgres:5432/DB?schema=ara&sslmode=prefer"

View File

@@ -1,26 +1,22 @@
{ {
"env": { "env": {
"browser": true, "browser": true,
"es2021": true "es2021": true
}, },
"extends": [ "extends": [
"eslint:recommended", "airbnb-base",
"plugin:@typescript-eslint/recommended", "airbnb-typescript/base"
"prettier" ],
], "parser": "@typescript-eslint/parser",
"parser": "@typescript-eslint/parser", "parserOptions": {
"parserOptions": { "ecmaVersion": "latest",
"ecmaVersion": "latest", "sourceType": "module",
"sourceType": "module", "project": "tsconfig.json"
"project": "tsconfig.json" },
}, "plugins": [
"plugins": ["@typescript-eslint"], "@typescript-eslint"
"rules": { ],
"class-methods-use-this": "off" "rules": {
}, "class-methods-use-this": "off"
"settings": {
"import/parsers": {
"@typescript-eslint/parser": [".ts", ".tsx"]
} }
}
} }

View File

@@ -4,6 +4,7 @@ about: Create a report to help us improve
title: '' title: ''
labels: bug labels: bug
assignees: '' assignees: ''
--- ---
**Describe the bug** **Describe the bug**
@@ -11,7 +12,6 @@ A clear and concise description of what the bug is.
**To Reproduce** **To Reproduce**
Steps to reproduce the behavior: Steps to reproduce the behavior:
1. Go to '...' 1. Go to '...'
2. Click on '....' 2. Click on '....'
3. Scroll down to '....' 3. Scroll down to '....'
@@ -24,17 +24,15 @@ A clear and concise description of what you expected to happen.
If applicable, add screenshots to help explain your problem. If applicable, add screenshots to help explain your problem.
**Desktop (please complete the following information):** **Desktop (please complete the following information):**
- OS: [e.g. iOS]
- OS: [e.g. iOS] - Browser [e.g. chrome, safari]
- Browser [e.g. chrome, safari] - Version [e.g. 22]
- Version [e.g. 22]
**Smartphone (please complete the following information):** **Smartphone (please complete the following information):**
- Device: [e.g. iPhone6]
- Device: [e.g. iPhone6] - OS: [e.g. iOS8.1]
- OS: [e.g. iOS8.1] - Browser [e.g. stock browser, safari]
- Browser [e.g. stock browser, safari] - Version [e.g. 22]
- Version [e.g. 22]
**Additional context** **Additional context**
Add any other context about the problem here. Add any other context about the problem here.

View File

@@ -4,6 +4,7 @@ about: Suggest an idea for this project
title: '' title: ''
labels: enhancement labels: enhancement
assignees: '' assignees: ''
--- ---
**Is your feature request related to a problem? Please describe.** **Is your feature request related to a problem? Please describe.**

View File

@@ -1,5 +0,0 @@
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
"extends": ["config:base"],
"schedule": ["after 12pm and before 6pm on Saturday"]
}

View File

@@ -9,14 +9,14 @@
# the `language` matrix defined below to confirm you have the correct set of # the `language` matrix defined below to confirm you have the correct set of
# supported CodeQL languages. # supported CodeQL languages.
# #
name: 'CodeQL' name: "CodeQL"
on: on:
push: push:
branches: ['main'] branches: [ "main" ]
pull_request: pull_request:
# The branches below must be a subset of the branches above # The branches below must be a subset of the branches above
branches: ['main'] branches: [ "main" ]
schedule: schedule:
- cron: '37 11 * * 2' - cron: '37 11 * * 2'
@@ -32,40 +32,41 @@ jobs:
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
language: ['javascript'] language: [ 'javascript' ]
# CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ]
# Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v4 uses: actions/checkout@v3
# Initializes the CodeQL tools for scanning. # Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL - name: Initialize CodeQL
uses: github/codeql-action/init@v3 uses: github/codeql-action/init@v2
with: with:
languages: ${{ matrix.language }} languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file. # If you wish to specify custom queries, you can do so here or in a config file.
# By default, queries listed here will override any specified in a config file. # By default, queries listed here will override any specified in a config file.
# Prefix the list here with "+" to use these queries and those in the config file. # Prefix the list here with "+" to use these queries and those in the config file.
# Details on CodeQL's query packs refer to : https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs
# queries: security-extended,security-and-quality
# Details on CodeQL's query packs refer to : https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs
# queries: security-extended,security-and-quality # Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
# If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild
uses: github/codeql-action/autobuild@v2
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java). # Command-line programs to run using the OS shell.
# If this step fails, then you should remove it and run the build manually (see below) # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
- name: Autobuild
uses: github/codeql-action/autobuild@v3
# Command-line programs to run using the OS shell. # If the Autobuild fails above, remove it and uncomment the following three lines.
# 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun # modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance.
# If the Autobuild fails above, remove it and uncomment the following three lines. # - run: |
# modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance. # echo "Run, Build Application using script"
# ./location_of_script_within_repo/buildscript.sh
# - run: | - name: Perform CodeQL Analysis
# echo "Run, Build Application using script" uses: github/codeql-action/analyze@v2
# ./location_of_script_within_repo/buildscript.sh
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v3

40
.github/workflows/deploy.yml vendored Normal file
View File

@@ -0,0 +1,40 @@
# This workflow uses actions that are not certified by GitHub.
# They are provided by a third-party and are governed by
# separate terms of service, privacy policy, and support
# documentation.
# ESLint is a tool for identifying and reporting on patterns
# found in ECMAScript/JavaScript code.
# More details at https://github.com/eslint/eslint
# and https://eslint.org
name: Deploy
on:
push:
branches: [ "main", "github-deploy-action" ]
pull_request:
# The branches below must be a subset of the branches above
branches: [ "main" ]
jobs:
build:
env:
REPO: git@github.com:veganhacktivists/arabot.git
REGISTRY: registry.digitalocean.com/vh-registry
IMAGE: test
TAG: latest
name: docker-build
runs-on: ubuntu-latest
steps:
- name: Check out Code
uses: actions/checkout@v3
- name: Login to Registry
uses: docker/login-action@v2
with:
registry: registry.digitalocean.com
username: ${{ secrets.DIGITAL_OCEAN_IMAGE_REPO_TOKEN }}
password: ${{ secrets.DIGITAL_OCEAN_IMAGE_REPO_TOKEN }}
- name: build
run: docker build . -t ${REGISTRY}/${IMAGE}:${TAG}
- name: push
run: docker push ${REGISTRY}/${IMAGE}:${TAG}

View File

@@ -11,10 +11,10 @@ name: ESLint
on: on:
push: push:
branches: ['main'] branches: [ "main" ]
pull_request: pull_request:
# The branches below must be a subset of the branches above # The branches below must be a subset of the branches above
branches: ['main'] branches: [ "main" ]
schedule: schedule:
- cron: '27 13 * * 1' - cron: '27 13 * * 1'
@@ -27,7 +27,7 @@ jobs:
security-events: write security-events: write
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@v4 uses: actions/checkout@v3
- name: Install ESLint - name: Install ESLint
run: | run: |
@@ -38,12 +38,12 @@ jobs:
run: npx eslint . run: npx eslint .
--config .eslintrc.json --config .eslintrc.json
--ext .js,.jsx,.ts,.tsx --ext .js,.jsx,.ts,.tsx
--format @microsoft/eslint-formatter-sarif --format @microsoft/eslint-formatter-sarif
--output-file eslint-results.sarif --output-file eslint-results.sarif
continue-on-error: true continue-on-error: true
- name: Upload analysis results to GitHub - name: Upload analysis results to GitHub
uses: github/codeql-action/upload-sarif@v3 uses: github/codeql-action/upload-sarif@v2
with: with:
sarif_file: eslint-results.sarif sarif_file: eslint-results.sarif
wait-for-processing: true wait-for-processing: true

View File

@@ -1,28 +0,0 @@
name: Prettier
on:
push:
branches: ['main']
pull_request:
# The branches below must be a subset of the branches above
branches: ['main']
schedule:
- cron: '27 13 * * 1'
jobs:
eslint:
name: Run prettier scanning
runs-on: ubuntu-latest
permissions:
contents: read
security-events: write
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Install Prettier
run: |
npm install prettier@3.1.0
- name: Run Prettier
run: npx prettier . --check

3
.npmrc
View File

@@ -1,3 +0,0 @@
node-linker=hoisted
shamefully-hoist=true
public-hoist-pattern[]=@sapphire/*

View File

@@ -1,4 +0,0 @@
dist
node_modules
tsconfig.json
pnpm-lock.yaml

View File

@@ -1,3 +0,0 @@
{
"singleQuote": true
}

View File

@@ -1,26 +1,20 @@
FROM node:22 AS base FROM node:18-buster
# PNPM
ENV PNPM_HOME="/pnpm"
ENV PATH="$PNPM_HOME:$PATH"
RUN corepack enable
COPY . /app WORKDIR /opt/app
WORKDIR /app
FROM base AS prod-deps COPY --chown=node:node package.json .
RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm install --prod --frozen-lockfile COPY --chown=node:node package-lock.json .
COPY --chown=node:node tsconfig.json .
COPY --chown=node:node prisma ./prisma/
FROM base AS build RUN npm install
RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm install --frozen-lockfile
RUN pnpm exec prisma generate
RUN pnpm run build
FROM base COPY . .
COPY --from=prod-deps /app/node_modules /app/node_modules
COPY --from=build /app/node_modules/.prisma /app/node_modules/.prisma
COPY --from=build /app/dist /app/dist
RUN chown node:node . RUN npm run build
RUN chown node:node /opt/app/
USER node USER node
CMD [ "pnpm", "run", "start:migrate"]
CMD [ "npm", "run", "start:migrate"]

View File

@@ -14,6 +14,7 @@ Make sure to create the .env file, which you can use the [.env.example](.env.exa
There are 2 options for running this bot, one using docker-compose and the other, less desirable npm. There are 2 options for running this bot, one using docker-compose and the other, less desirable npm.
### Docker ### Docker
Running the bot Dockerised makes everything easier. To run the bot, run: Running the bot Dockerised makes everything easier. To run the bot, run:
@@ -27,13 +28,11 @@ docker-compose up -d
Make sure to run `npm install` if you just cloned the repo. Make sure to run `npm install` if you just cloned the repo.
Then make sure to compile the TypeScript files using Then make sure to compile the TypeScript files using
```shell ```shell
npm run build npm run build
``` ```
If you are running the code for the first time with a new database, make sure to run `npm run start:migrate`, otherwise run: If you are running the code for the first time with a new database, make sure to run `npm run start:migrate`, otherwise run:
```shell ```shell
npm start npm start
``` ```
@@ -67,4 +66,4 @@ For support, feel free to send an email to anthony@aramovement.org or reach out
This bot is free and open source. It licensed using [GPL v3](LICENSE). This bot is free and open source. It licensed using [GPL v3](LICENSE).
Well done on making it to the bottom of the README file :) Well done on making it to the bottom of the README file :)

View File

@@ -1,46 +1,38 @@
version: '3.7' version: "3.7"
services: services:
postgres: postgres:
image: postgres:16 image: postgres:14
profiles: ["standalone"]
container_name: postgres container_name: postgres
restart: always restart: always
env_file: env_file:
- .env - .env
volumes: volumes:
- postgres:/var/lib/postgresql/data - postgres:/var/lib/postgresql/data
networks:
- arabot
redis: redis:
image: redis:7 image: redis:7
profiles: ["standalone", "prod"]
container_name: redis container_name: redis
restart: always restart: always
env_file: env_file:
- .env - .env.prod
volumes: volumes:
- redis:/data - redis:/data
networks:
- arabot
arabot: node:
build: build:
context: . context: .
dockerfile: Dockerfile dockerfile: Dockerfile
container_name: arabot profiles: ["standalone", "prod"]
restart: always
depends_on: depends_on:
- postgres - postgres
- redis - redis
env_file: env_file:
- .env - .env.prod
networks:
- arabot
volumes: volumes:
postgres: postgres:
name: arabot-db name: arabot-db
redis: redis:
name: arabot-redis name: arabot-redis
networks:
arabot:

View File

@@ -17,23 +17,23 @@ diverse, inclusive, and healthy community.
Examples of behavior that contributes to a positive environment for our Examples of behavior that contributes to a positive environment for our
community include: community include:
- Demonstrating empathy and kindness toward other people * Demonstrating empathy and kindness toward other people
- Being respectful of differing opinions, viewpoints, and experiences * Being respectful of differing opinions, viewpoints, and experiences
- Giving and gracefully accepting constructive feedback * Giving and gracefully accepting constructive feedback
- Accepting responsibility and apologizing to those affected by our mistakes, * Accepting responsibility and apologizing to those affected by our mistakes,
and learning from the experience and learning from the experience
- Focusing on what is best not just for us as individuals, but for the * Focusing on what is best not just for us as individuals, but for the
overall community overall community
Examples of unacceptable behavior include: Examples of unacceptable behavior include:
- The use of sexualized language or imagery, and sexual attention or * The use of sexualized language or imagery, and sexual attention or
advances of any kind advances of any kind
- Trolling, insulting or derogatory comments, and personal or political attacks * Trolling, insulting or derogatory comments, and personal or political attacks
- Public or private harassment * Public or private harassment
- Publishing others' private information, such as a physical or email * Publishing others' private information, such as a physical or email
address, without their explicit permission address, without their explicit permission
- Other conduct which could reasonably be considered inappropriate in a * Other conduct which could reasonably be considered inappropriate in a
professional setting professional setting
## Enforcement Responsibilities ## Enforcement Responsibilities
@@ -106,7 +106,7 @@ Violating these terms may lead to a permanent ban.
### 4. Permanent Ban ### 4. Permanent Ban
**Community Impact**: Demonstrating a pattern of violation of community **Community Impact**: Demonstrating a pattern of violation of community
standards, including sustained inappropriate behavior, harassment of an standards, including sustained inappropriate behavior, harassment of an
individual, or aggression toward or disparagement of classes of individuals. individual, or aggression toward or disparagement of classes of individuals.
**Consequence**: A permanent ban from any sort of public interaction within **Consequence**: A permanent ban from any sort of public interaction within

View File

@@ -1,12 +0,0 @@
# Commands for the bot
These are all the commands that you can use for the bot. Some of these are for staff only, but everyone can use the
general commands
## Contents
- [General](commands/GENERAL.md)
- [Moderator](commands/MOD.md)
- [Verifier](commands/VERIFIER.md)
- [Mentor](commands/MENTOR.md)
- [Coordinator](commands/COORDINATOR.md)

View File

@@ -1,12 +1,13 @@
# Commit Message Format # Commit Message Format
_This specification is a modified version of the [AngularJS commit message format](https://github.com/angular/angular/blob/main/CONTRIBUTING.md#-commit-message-format)._ *This specification is a modified version of the [AngularJS commit message format](https://github.com/angular/angular/blob/main/CONTRIBUTING.md#-commit-message-format).*
We have very precise rules over how our Git commit messages must be formatted. We have very precise rules over how our Git commit messages must be formatted.
This format leads to **easier to read commit history**. This format leads to **easier to read commit history**.
Each commit message consists of a **header**, a **body**, and a **footer**. Each commit message consists of a **header**, a **body**, and a **footer**.
``` ```
<header> <header>
<BLANK LINE> <BLANK LINE>
@@ -22,6 +23,7 @@ When the body is present it must be at least 20 characters long and must conform
The `footer` is optional. The [Commit Message Footer](#commit-footer) format describes what the footer is used for and the structure it must have. The `footer` is optional. The [Commit Message Footer](#commit-footer) format describes what the footer is used for and the structure it must have.
#### <a name="commit-header"></a>Commit Message Header #### <a name="commit-header"></a>Commit Message Header
``` ```
@@ -36,39 +38,42 @@ The `footer` is optional. The [Commit Message Footer](#commit-footer) format des
The `<type>` and `<summary>` fields are mandatory, the `(<scope>)` field is optional. The `<type>` and `<summary>` fields are mandatory, the `(<scope>)` field is optional.
##### Type ##### Type
Must be one of the following: Must be one of the following:
- **build**: Changes that affect the build system or external dependencies (example scopes: gulp, broccoli, npm) * **build**: Changes that affect the build system or external dependencies (example scopes: gulp, broccoli, npm)
- **ci**: Changes to our CI configuration files and scripts * **ci**: Changes to our CI configuration files and scripts
- **docs**: Documentation only changes * **docs**: Documentation only changes
- **feat**: A new feature * **feat**: A new feature
- **fix**: A bug fix * **fix**: A bug fix
- **perf**: A code change that improves performance * **perf**: A code change that improves performance
- **refactor**: A code change that neither fixes a bug nor adds a feature * **refactor**: A code change that neither fixes a bug nor adds a feature
- **test**: Adding missing tests or correcting existing tests * **test**: Adding missing tests or correcting existing tests
##### Scope ##### Scope
The scope should be the name of the npm package affected (as perceived by the person reading the changelog generated from commit messages). The scope should be the name of the npm package affected (as perceived by the person reading the changelog generated from commit messages).
The following is the list of supported scopes: The following is the list of supported scopes:
- `arabot` * `arabot`
- `config` * `config`
- `compiler` * `compiler`
- `database` * `database`
- `docs` * `docs`
- `upgrade` * `upgrade`
##### Summary ##### Summary
Use the summary field to provide a succinct description of the change: Use the summary field to provide a succinct description of the change:
- use the imperative, present tense: "change" not "changed" nor "changes" * use the imperative, present tense: "change" not "changed" nor "changes"
- don't capitalize the first letter * don't capitalize the first letter
- no dot (.) at the end * no dot (.) at the end
#### <a name="commit-body"></a>Commit Message Body #### <a name="commit-body"></a>Commit Message Body
@@ -77,6 +82,7 @@ Just as in the summary, use the imperative, present tense: "fix" not "fixed" nor
Explain the motivation for the change in the commit message body. This commit message should explain _why_ you are making the change. Explain the motivation for the change in the commit message body. This commit message should explain _why_ you are making the change.
You can include a comparison of the previous behavior with the new behavior in order to illustrate the impact of the change. You can include a comparison of the previous behavior with the new behavior in order to illustrate the impact of the change.
#### <a name="commit-footer"></a>Commit Message Footer #### <a name="commit-footer"></a>Commit Message Footer
The footer can contain information about breaking changes and deprecations and is also the place to reference GitHub issues, Jira tickets, and other PRs that this commit closes or is related to. The footer can contain information about breaking changes and deprecations and is also the place to reference GitHub issues, Jira tickets, and other PRs that this commit closes or is related to.
@@ -106,6 +112,7 @@ Breaking Change section should start with the phrase "BREAKING CHANGE: " followe
Similarly, a Deprecation section should start with "DEPRECATED: " followed by a short description of what is deprecated, a blank line, and a detailed description of the deprecation that also mentions the recommended update path. Similarly, a Deprecation section should start with "DEPRECATED: " followed by a short description of what is deprecated, a blank line, and a detailed description of the deprecation that also mentions the recommended update path.
### Revert commits ### Revert commits
If the commit reverts a previous commit, it should begin with `revert: `, followed by the header of the reverted commit. If the commit reverts a previous commit, it should begin with `revert: `, followed by the header of the reverted commit.

View File

@@ -1,47 +0,0 @@
# Coordinator Commands
## General Coordinator Commands
- `/access <permission> <channel> <user/role>` - Gives/removes specific access to a user/role from a channel in
ModMail/Private/Restricted categories.
- `/anonymous <message> <optional: channel>`/`?anon <channel> <message>` - Sends a message via the bot (optionally if
channel has been defined, to that channel).
- `/clear <messages>`/`?clear <messages>` - Bulk deletes 1-100 messages.
- `/moveall <channel>`/`?mvall <channel>` - Moves everyone in the current voice channel to the specified voice channel.
- `/plus <user>`/`?plus <user>` - Gives/removes the Plus role from the user. This role is for members that are 18+.
## Private Channels
- `/private create <user>` - Creates a private channel for that user for the specific coordinator team you are on.
- `/private delete <optional: user>` - Deletes the private channel, either the channel you are currently in or if
specified the optional channel, the channel you declared. You can only delete your specific coordinator team's private
channels.
## Diversity Team
- `/diversity <user>`/`?div <user>` - Gives/removes the Diversity Team role from the user.
- `/diversity toggleopen` - toggles the diversity chat open or closed.
## Events Team
- `/stagehost <user>`/`?stagehost <user>` - Gives/removes the Stage Host role from the user.
- `/bookclub <user>`/`?bookclub <user>` - Gives/removes the Book Club role from the user.
- `/debatehost <user>`/`?debatehost <user>` - Gives/removes the Debate Host role from the user.
- `/gamenight <user>`/`?gamenight <user>` - Gives/removes the Game Night Host role from the user.
- `/guest <user>`/`?guest <user>` - Gives/removes the Guest role from the user.
## Mentor Team
- `/mentor <user>`/`?mentor <user>` - Gives/removes the Mentor role from the user.
## Verification Team
- `/vegan <user>`/`?v <user>` - Gives/removes the Vegan role from the user.
- `/activist <user>`/`?a <user>` - Gives/removes the Activist role from the user.
- `/verifier <user>`/`?verifier <user>` - Gives/removes the Verifier role from the user.
- `/trialverifier <user>`/`?trialverifier <user>` - Gives/removes the Trial Verifier role from the user.
## Mod Team
- `/mod <user>`/`?mod <user>` - Gives/removes the Mod role from the user.
- `/restrictedaccess <user>`/`?ra <user>` - Gives/removes the Restricted Access role from the user.

View File

@@ -1,32 +0,0 @@
# General commands
## Utilities
- `/ping`/`?ping` - Checks if the bot is alive and the ping of the bot.
- `/apply`/`?apply` - Gives you the link to where you can apply to be a Moderator or Verifier.
- `/count`/`?count` - Tells you how many vegans and non-vegans there are on the server.
- `/info <info> <optional: visible>` - Gives you information based on what you pick in `<info>`. If you make `visible`
true, it will make the information visible to everyone.
- `/help`/`?help` - Gives a link (here) to all the commands.
## Economy
- `/daily`/`?daily` - Gives you a daily reward of ARAs
- `/pay <user> <amount> <reason>`/`?pay <user> <amount> <reason>` - Give a user an amount of ARAs
- `/balance`/`?balance` - Checks how many ARAs you have
## XP
- `/rank <optional: user>`/`?rank <optional: user>` - Shows your rank based on the amount of XP you have. If you provide
a user, it will display that user's rank instead.
## Fun Commands
- `/1984`
- `/happy`
- `/hug`
- `/kill`
- `/poke`
- `/sad`
- `/shrug`
- `/cringe`

View File

@@ -1,4 +0,0 @@
# Mentor Commands
- `/vegcurious <user>`/`?veg <user>` - Gives/removes the Veg Curious role from the user (only usable on non-vegans).
- `/convinced <user>`/`?conv <user>` - Gives/removes the Convinced role from the user.

View File

@@ -1,50 +0,0 @@
# Moderation Commands
## General Moderation
- `/rename <user> <optional: nickname>`/`?ru <user> <optional: nickname>` - Renames that user to the specified nickname,
if nickname is left blank it will reset the nickname to their original username.
- `/slowmode <duration>`/`?slowmode <duration>` - changes the slowmode for a chat. Duration uses a time which can be set by providing a number
followed by s/d/m/y or just writing `off`. These can be combined.
For example to set the slowmode for 1 minute and 30 seconds, you would write: `1m 30s`.
- `/softmute <user>`/`?sm/softmute <user>` - Prevents the user from reacting to messages.
- `/vcmute <user>`/`?vcmute <user>` - Adds a persistent VC mute if the user has left the VC or leaves the server to
circumvent VC mutes.
- `?warn <user> <reason>` - Gives a warning to the user.
## Roles
These are roles you can give/take away from users.
- `/trusted <user>`/`?t <user>` - Gives/removes the Trusted role from the user.
- `/vegcurious <user>`/`?veg <user>` - Gives/removes the Veg Curious role from the user (only usable on non-vegans).
- `/convinced <user>`/`?conv <user>` - Gives/removes the Convinced role from the user.
## Sus
This command stores notes on users that could be information on the user
or anything that is not serious enough for a warning.
- `/sus add <user> <note>`/`?sus <user> <note>` - Add a note to the user
- `/sus view <user>` - View notes made on the user
- `/sus remove <id>` - Remove a specific note from the sus note id, which you can get from using `/sus view`
- `/sus purge <user>` - Remove all sus notes from the user
## Restrictions
These are used for users that have broken rules severe enough that takes away their access to the server.
- `/restrict <user> <reason>`/`?r/restrict <user> <reason>` - Restricts the user to the restricted section
- `/unrestrict <user>`/`?ur <user>` - Unrestricts the user
- `/restrictlogs <optional: user>` - Shows the logs of when the user has been restricted. The need to provide the user
is optional depending on if the command is run in the ModMail category.
- `/restricttools channel delete <optional: user>` - Deletes the vegan restricted channel for the user. Providing a user
is only optional if the command is run in the channel that is to be deleted.
## Bans
- `/tempban <user> <duration> <reason>`/`?tempban <user> <duration <reason>` - Bans the user for a specific amount of
time. Duration uses a time which can be set by providing a number followed by s/d/m/y. These can be combined.
For example to ban someone for 1 week and 3 days, you would write: `1w 3d`.
- `/ban <user> <reason>`/`?ban <user> <reason>` - Permanently bans that user.
- `/unban <user>`/`?unban <user>` - Unbans that user.

View File

@@ -1,32 +0,0 @@
# Verifier Commands
## Verification
- `/verify <user> <roles>`/`?ver <user> <roles>` - This is a manual verification to give roles to a user. This should
not be used if you are verifying the user in the voice channel. Roles available (you can write multiple in one command
such as `v a t`):
- `v` - Vegan
- `a` - Activist
- `t` - Trusted
- `x` - ARA Vegan (went vegan because of ARA)
- `nv` - Not Vegan
- `conv` - Convinced
- `veg` - Veg Curious
- `/verifytimeoutremove <user>` - Removes a verification timeout if the user has been timed out as a verifier was not
available. This cannot be used for users that have been verified.
## Roles
These are roles you can give/take away from users.
- `/trusted <user>`/`?t <user>` - Gives/removes the Trusted role from the user.
- `/vegcurious <user>`/`?veg <user>` - Gives/removes the Veg Curious role from the user (only usable on non-vegans).
- `/convinced <user>`/`?conv <user>` - Gives/removes the Convinced role from the user.
### Roles you can only give
- `/vegan <user>`/`?v <user>` - Give the vegan role
- `/activist <user>`/`?a <user>` - Gives the activist role
- `/aravegan <user`/`?aravegan <user>` - Gives the ARA vegan role

View File

@@ -1,5 +0,0 @@
[phases.build]
cmds = ["pnpm prisma generate", "..."]
[start]
cmd = 'pnpm run start:migrate'

5736
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -1,17 +1,13 @@
{ {
"name": "arabot", "name": "arabot",
"version": "0.4.1", "version": "0.0.1",
"description": "A Discord bot for Animal Rights Advocates", "description": "A Discord bot for Animal Rights Advocates",
"main": "dist/index.js", "main": "dist/index.js",
"scripts": { "scripts": {
"preinstall": "npx only-allow pnpm",
"build": "tsc", "build": "tsc",
"cleanBuild": "rm -rf ./dist && tsc", "cleanBuild": "rm -rf ./dist && tsc",
"start": "node dist/index.js", "start": "node dist/index.js",
"start:migrate": "prisma migrate deploy && pnpm run start" "start:migrate": "prisma migrate deploy && npm run start"
},
"imports": {
"#utils/*": "./dist/utils/*.js"
}, },
"repository": { "repository": {
"type": "git", "type": "git",
@@ -29,35 +25,32 @@
"url": "https://github.com/veganhacktivists/arabot/issues" "url": "https://github.com/veganhacktivists/arabot/issues"
}, },
"homepage": "https://github.com/veganhacktivists/arabot#readme", "homepage": "https://github.com/veganhacktivists/arabot#readme",
"engines": {
"node": ">=20",
"pnpm": ">=9"
},
"dependencies": { "dependencies": {
"@prisma/client": "^5.22.0", "@discordjs/builders": "^1.2.0",
"@sapphire/discord.js-utilities": "^7.3.2", "@prisma/client": "^4.0.0",
"@sapphire/framework": "^5.3.2", "@sapphire/discord.js-utilities": "^5.0.0",
"@sapphire/plugin-logger": "^4.0.2", "@sapphire/framework": "^3.1.1",
"@sapphire/plugin-scheduled-tasks": "^10.0.2", "@sapphire/plugin-scheduled-tasks": "^4.0.0",
"@sapphire/plugin-subcommands": "^6.0.3", "@sapphire/plugin-subcommands": "^3.0.0",
"@sapphire/stopwatch": "^1.5.4", "@sapphire/stopwatch": "^1.4.1",
"@sapphire/time-utilities": "^1.7.14", "@sapphire/ts-config": "^3.3.4",
"@sapphire/ts-config": "^5.0.1", "@sapphire/utilities": "^3.9.2",
"@sapphire/utilities": "^3.18.1", "@types/node": "^18.0.3",
"bullmq": "^5.34.10", "bullmq": "^1.89.1",
"discord.js": "^14.17.3", "discord-api-types": "^0.33.3",
"ioredis": "^5.4.2", "discord.js": "^13.10.3",
"ts-node": "^10.9.2", "dotenv": "^16.0.1",
"typescript": "~5.4.5" "prisma": "^4.2.1",
"ts-node": "^10.8.2",
"typescript": "^4.7.4"
}, },
"devDependencies": { "devDependencies": {
"@types/node": "^20.17.13", "@types/ioredis": "^4.28.10",
"@typescript-eslint/eslint-plugin": "^6.21.0", "@typescript-eslint/eslint-plugin": "^5.30.7",
"@typescript-eslint/parser": "^6.21.0", "@typescript-eslint/parser": "^5.30.7",
"eslint": "8.56.0", "eslint": "8.22.0",
"eslint-config-prettier": "^9.1.0", "eslint-config-airbnb-base": "^15.0.0",
"prettier": "3.2.4", "eslint-config-airbnb-typescript": "^17.0.0",
"prisma": "^5.22.0" "eslint-plugin-import": "^2.26.0"
}, }
"packageManager": "pnpm@9.12.2+sha512.22721b3a11f81661ae1ec68ce1a7b879425a1ca5b991c975b074ac220b187ce56c708fe5db69f4c962c989452eee76c82877f4ee80f474cebd61ee13461b6228"
} }

1899
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,14 +0,0 @@
/*
Warnings:
- You are about to drop the column `balance` on the `User` table. All the data in the column will be lost.
- You are about to drop the column `lastDaily` on the `User` table. All the data in the column will be lost.
- You are about to drop the column `level` on the `User` table. All the data in the column will be lost.
- You are about to drop the column `xp` on the `User` table. All the data in the column will be lost.
*/
-- AlterTable
ALTER TABLE "User" DROP COLUMN "balance",
DROP COLUMN "lastDaily",
DROP COLUMN "level",
DROP COLUMN "xp";

View File

@@ -1,8 +0,0 @@
/*
Warnings:
- Added the required column `channelId` to the `VerifyUnblock` table without a default value. This is not possible if the table is not empty.
*/
-- AlterTable
ALTER TABLE "Verify" ADD COLUMN "channelId" TEXT NOT NULL;

View File

@@ -1,10 +0,0 @@
/*
Warnings:
- You are about to drop the column `time` on the `VerifyUnblock` table. All the data in the column will be lost.
*/
-- AlterTable
ALTER TABLE "Verify" DROP COLUMN "time",
ADD COLUMN "finishTime" TIMESTAMP(3),
ADD COLUMN "startTime" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP;

View File

@@ -1,3 +0,0 @@
-- AlterTable
ALTER TABLE "Verify" ADD COLUMN "joinTime" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
ALTER COLUMN "startTime" DROP NOT NULL;

View File

@@ -1,14 +0,0 @@
/*
Warnings:
- The primary key for the `VerifyUnblock` table will be changed. If it partially fails, the table could be left without primary key constraint.
- You are about to drop the column `channelId` on the `VerifyUnblock` table. All the data in the column will be lost.
*/
-- AlterTable
ALTER TABLE "Verify" DROP CONSTRAINT "Verify_pkey",
DROP COLUMN "channelId",
ALTER COLUMN "id" DROP DEFAULT,
ALTER COLUMN "id" SET DATA TYPE TEXT,
ADD CONSTRAINT "Verify_pkey" PRIMARY KEY ("id");
DROP SEQUENCE "Verify_id_seq";

View File

@@ -1,2 +0,0 @@
-- AlterTable
ALTER TABLE "Verify" ALTER COLUMN "startTime" DROP DEFAULT;

View File

@@ -1,10 +0,0 @@
-- AlterTable
ALTER TABLE "Verify" ADD COLUMN "activist" BOOLEAN NOT NULL DEFAULT false,
ADD COLUMN "food" INTEGER,
ADD COLUMN "length" INTEGER,
ADD COLUMN "life" INTEGER,
ADD COLUMN "reason" INTEGER,
ADD COLUMN "reasoning" INTEGER,
ADD COLUMN "trusted" BOOLEAN NOT NULL DEFAULT false,
ADD COLUMN "vegCurious" BOOLEAN NOT NULL DEFAULT false,
ADD COLUMN "where" INTEGER;

View File

@@ -1,2 +0,0 @@
-- AlterTable
ALTER TABLE "Verify" ADD COLUMN "convinced" BOOLEAN NOT NULL DEFAULT false;

View File

@@ -1,12 +0,0 @@
/*
Warnings:
- Added the required column `endModId` to the `Ban` table without a default value. This is not possible if the table is not empty.
*/
-- AlterTable
ALTER TABLE "Ban" ADD COLUMN "endModId" TEXT NOT NULL,
ADD COLUMN "endTime" TIMESTAMP(3);
-- AddForeignKey
ALTER TABLE "Ban" ADD CONSTRAINT "Ban_endModId_fkey" FOREIGN KEY ("endModId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;

View File

@@ -1,8 +0,0 @@
-- DropForeignKey
ALTER TABLE "Ban" DROP CONSTRAINT "Ban_endModId_fkey";
-- AlterTable
ALTER TABLE "Ban" ALTER COLUMN "endModId" DROP NOT NULL;
-- AddForeignKey
ALTER TABLE "Ban" ADD CONSTRAINT "Ban_endModId_fkey" FOREIGN KEY ("endModId") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE;

View File

@@ -1,17 +0,0 @@
-- CreateTable
CREATE TABLE "VCMute" (
"id" SERIAL NOT NULL,
"userId" TEXT NOT NULL,
"modId" TEXT NOT NULL,
"time" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"endTime" TIMESTAMP(3),
"reason" TEXT,
CONSTRAINT "VCMute_pkey" PRIMARY KEY ("id")
);
-- AddForeignKey
ALTER TABLE "VCMute" ADD CONSTRAINT "VCMute_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "VCMute" ADD CONSTRAINT "VCMute_modId_fkey" FOREIGN KEY ("modId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;

View File

@@ -1,5 +0,0 @@
-- AlterTable
ALTER TABLE "Restrict" ADD COLUMN "endModId" TEXT;
-- AddForeignKey
ALTER TABLE "Restrict" ADD CONSTRAINT "Restrict_endModId_fkey" FOREIGN KEY ("endModId") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE;

View File

@@ -1,15 +0,0 @@
/*
Warnings:
- You are about to drop the column `endedTime` on the `Restrict` table. All the data in the column will be lost.
- You are about to drop the column `endedTime` on the `TempBan` table. All the data in the column will be lost.
- Added the required column `endTime` to the `TempBan` table without a default value. This is not possible if the table is not empty.
*/
-- AlterTable
ALTER TABLE "Restrict" DROP COLUMN "endedTime",
ADD COLUMN "endTime" TIMESTAMP(3);
-- AlterTable
ALTER TABLE "TempBan" DROP COLUMN "endedTime",
ADD COLUMN "endTime" TIMESTAMP(3) NOT NULL;

View File

@@ -1,8 +0,0 @@
/*
Warnings:
- Added the required column `section` to the `Restrict` table without a default value. This is not possible if the table is not empty.
*/
-- AlterTable
ALTER TABLE "Restrict" ADD COLUMN "section" INTEGER NOT NULL;

View File

@@ -1,17 +0,0 @@
-- CreateTable
CREATE TABLE "Warnings" (
"id" SERIAL NOT NULL,
"userId" TEXT NOT NULL,
"modId" TEXT NOT NULL,
"time" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"active" BOOLEAN NOT NULL DEFAULT true,
"note" TEXT NOT NULL,
CONSTRAINT "Warnings_pkey" PRIMARY KEY ("id")
);
-- AddForeignKey
ALTER TABLE "Warnings" ADD CONSTRAINT "Warnings_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Warnings" ADD CONSTRAINT "Warnings_modId_fkey" FOREIGN KEY ("modId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;

View File

@@ -1,20 +0,0 @@
/*
Warnings:
- You are about to drop the `Warnings` table. If the table is not empty, all the data it contains will be lost.
*/
-- DropForeignKey
ALTER TABLE "Warnings" DROP CONSTRAINT "Warnings_modId_fkey";
-- DropForeignKey
ALTER TABLE "Warnings" DROP CONSTRAINT "Warnings_userId_fkey";
-- RenameTable
ALTER TABLE "Warnings" RENAME TO "Warning";
-- AddForeignKey
ALTER TABLE "Warning" ADD CONSTRAINT "Warning_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Warning" ADD CONSTRAINT "Warning_modId_fkey" FOREIGN KEY ("modId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;

View File

@@ -1,2 +0,0 @@
-- AlterTable
ALTER TABLE "Warning" RENAME CONSTRAINT "Warnings_pkey" TO "Warning_pkey";

View File

@@ -1,5 +0,0 @@
-- AlterTable
ALTER TABLE "TempBan" ADD COLUMN "endModId" TEXT;
-- AddForeignKey
ALTER TABLE "TempBan" ADD CONSTRAINT "TempBan_endModId_fkey" FOREIGN KEY ("endModId") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE;

View File

@@ -1,2 +0,0 @@
-- AlterTable
ALTER TABLE "Verify" ADD COLUMN "manual" BOOLEAN NOT NULL DEFAULT false;

View File

@@ -1,58 +0,0 @@
-- CreateTable
CREATE TABLE "Event" (
"id" SERIAL NOT NULL,
"eventType" TEXT NOT NULL,
"leaderId" TEXT NOT NULL,
"startTime" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"endTime" TIMESTAMP(3),
CONSTRAINT "Event_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "EventType" (
"type" TEXT NOT NULL,
CONSTRAINT "EventType_pkey" PRIMARY KEY ("type")
);
-- CreateTable
CREATE TABLE "Stat" (
"id" SERIAL NOT NULL,
"eventId" INTEGER NOT NULL,
"leaderId" TEXT NOT NULL,
"vegan" INTEGER NOT NULL DEFAULT 0,
"considered" INTEGER NOT NULL DEFAULT 0,
"antivegan" INTEGER NOT NULL DEFAULT 0,
"thanked" INTEGER NOT NULL DEFAULT 0,
"documentary" INTEGER NOT NULL DEFAULT 0,
"educated" INTEGER NOT NULL DEFAULT 0,
CONSTRAINT "Stat_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "ParticipantStat" (
"statId" INTEGER NOT NULL,
"userId" TEXT NOT NULL,
CONSTRAINT "ParticipantStat_pkey" PRIMARY KEY ("statId","userId")
);
-- AddForeignKey
ALTER TABLE "Event" ADD CONSTRAINT "Event_eventType_fkey" FOREIGN KEY ("eventType") REFERENCES "EventType"("type") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Event" ADD CONSTRAINT "Event_leaderId_fkey" FOREIGN KEY ("leaderId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Stat" ADD CONSTRAINT "Stat_eventId_fkey" FOREIGN KEY ("eventId") REFERENCES "Event"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Stat" ADD CONSTRAINT "Stat_leaderId_fkey" FOREIGN KEY ("leaderId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "ParticipantStat" ADD CONSTRAINT "ParticipantStat_statId_fkey" FOREIGN KEY ("statId") REFERENCES "Stat"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "ParticipantStat" ADD CONSTRAINT "ParticipantStat_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;

View File

@@ -1,10 +0,0 @@
-- CreateTable
CREATE TABLE "StatRole" (
"statId" INTEGER NOT NULL,
"roleId" TEXT NOT NULL,
CONSTRAINT "StatRole_pkey" PRIMARY KEY ("statId")
);
-- AddForeignKey
ALTER TABLE "StatRole" ADD CONSTRAINT "StatRole_statId_fkey" FOREIGN KEY ("statId") REFERENCES "Stat"("id") ON DELETE RESTRICT ON UPDATE CASCADE;

View File

@@ -1,12 +0,0 @@
-- CreateTable
CREATE TABLE "LeaveLog" (
"id" SERIAL NOT NULL,
"userId" TEXT NOT NULL,
"time" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"roles" TEXT[],
CONSTRAINT "LeaveLog_pkey" PRIMARY KEY ("id")
);
-- AddForeignKey
ALTER TABLE "LeaveLog" ADD CONSTRAINT "LeaveLog_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;

View File

@@ -1,41 +0,0 @@
-- CreateTable
CREATE TABLE "Balance" (
"userId" TEXT NOT NULL,
"balance" INTEGER NOT NULL,
CONSTRAINT "Balance_pkey" PRIMARY KEY ("userId")
);
-- CreateTable
CREATE TABLE "Daily" (
"id" SERIAL NOT NULL,
"userId" TEXT NOT NULL,
"amount" INTEGER NOT NULL,
"time" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "Daily_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "Payment" (
"id" SERIAL NOT NULL,
"senderId" TEXT NOT NULL,
"recipientId" TEXT NOT NULL,
"amount" INTEGER NOT NULL,
"reason" TEXT NOT NULL,
"time" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "Payment_pkey" PRIMARY KEY ("id")
);
-- AddForeignKey
ALTER TABLE "Balance" ADD CONSTRAINT "Balance_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Daily" ADD CONSTRAINT "Daily_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Payment" ADD CONSTRAINT "Payment_senderId_fkey" FOREIGN KEY ("senderId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Payment" ADD CONSTRAINT "Payment_recipientId_fkey" FOREIGN KEY ("recipientId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;

View File

@@ -1,29 +0,0 @@
-- CreateTable
CREATE TABLE "RoleLog" (
"id" SERIAL NOT NULL,
"userId" TEXT NOT NULL,
"modId" TEXT NOT NULL,
"roleId" TEXT NOT NULL,
"add" BOOLEAN NOT NULL DEFAULT false,
"time" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "RoleLog_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "Role" (
"id" TEXT NOT NULL,
"name" TEXT NOT NULL,
"staff" BOOLEAN NOT NULL,
CONSTRAINT "Role_pkey" PRIMARY KEY ("id")
);
-- AddForeignKey
ALTER TABLE "RoleLog" ADD CONSTRAINT "RoleLog_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "RoleLog" ADD CONSTRAINT "RoleLog_modId_fkey" FOREIGN KEY ("modId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "RoleLog" ADD CONSTRAINT "RoleLog_roleId_fkey" FOREIGN KEY ("roleId") REFERENCES "Role"("id") ON DELETE RESTRICT ON UPDATE CASCADE;

View File

@@ -1,13 +0,0 @@
-- CreateTable
CREATE TABLE "Xp" (
"userId" TEXT NOT NULL,
"level" INTEGER NOT NULL DEFAULT 0,
"xp" INTEGER NOT NULL DEFAULT 0,
"messageCount" INTEGER NOT NULL DEFAULT 0,
"lastMessage" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "Xp_pkey" PRIMARY KEY ("userId")
);
-- AddForeignKey
ALTER TABLE "Xp" ADD CONSTRAINT "Xp_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;

View File

@@ -1,29 +0,0 @@
-- CreateTable
CREATE TABLE "FunLog" (
"id" SERIAL NOT NULL,
"sendUserId" TEXT NOT NULL,
"receiveUserId" TEXT,
"typeId" INTEGER NOT NULL,
CONSTRAINT "FunLog_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "FunType" (
"id" SERIAL NOT NULL,
"name" TEXT NOT NULL,
CONSTRAINT "FunType_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE UNIQUE INDEX "FunType_name_key" ON "FunType"("name");
-- AddForeignKey
ALTER TABLE "FunLog" ADD CONSTRAINT "FunLog_sendUserId_fkey" FOREIGN KEY ("sendUserId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "FunLog" ADD CONSTRAINT "FunLog_receiveUserId_fkey" FOREIGN KEY ("receiveUserId") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "FunLog" ADD CONSTRAINT "FunLog_typeId_fkey" FOREIGN KEY ("typeId") REFERENCES "FunType"("id") ON DELETE RESTRICT ON UPDATE CASCADE;

View File

@@ -1,2 +0,0 @@
-- AlterTable
ALTER TABLE "Xp" ADD COLUMN "xpToNextLevel" INTEGER NOT NULL DEFAULT 0;

View File

@@ -1,9 +0,0 @@
/*
Warnings:
- You are about to drop the column `xpToNextLevel` on the `Xp` table. All the data in the column will be lost.
*/
-- AlterTable
ALTER TABLE "Xp" DROP COLUMN "xpToNextLevel",
ADD COLUMN "xpForNextLevel" INTEGER NOT NULL DEFAULT 0;

View File

@@ -1,11 +0,0 @@
-- CreateTable
CREATE TABLE "Counting" (
"id" SERIAL NOT NULL,
"userId" TEXT NOT NULL,
"number" INTEGER NOT NULL,
CONSTRAINT "Counting_pkey" PRIMARY KEY ("id")
);
-- AddForeignKey
ALTER TABLE "Counting" ADD CONSTRAINT "Counting_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;

View File

@@ -1,8 +0,0 @@
/*
Warnings:
- You are about to drop the column `active` on the `Warning` table. All the data in the column will be lost.
*/
-- AlterTable
ALTER TABLE "Warning" DROP COLUMN "active";

View File

@@ -1,8 +0,0 @@
/*
Warnings:
- Added the required column `channelId` to the `StatRole` table without a default value. This is not possible if the table is not empty.
*/
-- AlterTable
ALTER TABLE "StatRole" ADD COLUMN "channelId" TEXT NOT NULL;

View File

@@ -26,219 +26,45 @@ datasource db {
} }
model User { model User {
id String @id @db.VarChar(255) id String @id @db.VarChar(255)
vegan Boolean @default(false) level Int @default(0)
trusted Boolean @default(false) xp Int @default(0)
activist Boolean @default(false) balance Int @default(0)
plus Boolean @default(false) lastDaily DateTime?
notVegan Boolean @default(false) vegan Boolean @default(false)
vegCurious Boolean @default(false) trusted Boolean @default(false)
convinced Boolean @default(false) activist Boolean @default(false)
muted Boolean @default(false) plus Boolean @default(false)
VerifyUser Verify[] @relation("verUser") notVegan Boolean @default(false)
VerifyVerifier Verify[] @relation("verVerifier") vegCurious Boolean @default(false)
Xp Xp? convinced Boolean @default(false)
Balance Balance? muted Boolean @default(false)
Daily Daily[] VerifyUser Verify[] @relation("verUser")
SendPayment Payment[] @relation("sendPayment") VerifyVerifier Verify[] @relation("verVerifier")
RecievePayment Payment[] @relation("recievePayment") SusUser Sus[] @relation("susUser")
LeaveLog LeaveLog[] SusMod Sus[] @relation("susMod")
UserRoleLog RoleLog[] @relation("userRoleLog") RestrictUser Restrict[] @relation("restUser")
ModRoleLog RoleLog[] @relation("modRoleLog") RestrictMod Restrict[] @relation("restMod")
FunLogSender FunLog[] @relation("sendFunLog") BanUser Ban[] @relation("banUser")
FunLogReciever FunLog[] @relation("receiveFunLog") BanMod Ban[] @relation("banMod")
Counting Counting[] @relation("counting") TempBanUser TempBan[] @relation("tbanUser")
EventLeader Event[] @relation("eventLeader") TempBanMod TempBan[] @relation("tbanMod")
StatLeader Stat[] @relation("statLeader")
OutreachParticipation ParticipantStat[] @relation("participantUser")
SusUser Sus[] @relation("susUser")
SusMod Sus[] @relation("susMod")
WarnUser Warning[] @relation("warnUser")
WarnMod Warning[] @relation("warnMod")
RestrictUser Restrict[] @relation("restUser")
RestrictMod Restrict[] @relation("restMod")
RestrictEndMod Restrict[] @relation("endRestMod")
BanUser Ban[] @relation("banUser")
BanMod Ban[] @relation("banMod")
BanEndMod Ban[] @relation("endBanMod")
TempBanUser TempBan[] @relation("tbanUser")
TempBanMod TempBan[] @relation("tbanMod")
TempBanEndMod TempBan[] @relation("endTbanMod")
VCMuteUser VCMute[] @relation("vcMuteUser")
VCMuteMod VCMute[] @relation("vcMuteMod")
} }
model Verify { model Verify {
id String @id id Int @id @default(autoincrement())
user User @relation("verUser", fields: [userId], references: [id]) user User @relation("verUser", fields: [userId], references: [id])
userId String userId String
verifier User? @relation("verVerifier", fields: [verifierId], references: [id]) verifier User? @relation("verVerifier", fields: [verifierId], references: [id])
verifierId String? verifierId String?
joinTime DateTime @default(now()) time DateTime @default(now())
startTime DateTime? timedOut Boolean @default(false) // If they got kicked out of verification because they timed out
finishTime DateTime? vegan Boolean @default(false) // If they got verified as a vegan
manual Boolean @default(false) // If they were verified with the verify command text Boolean @default(false) // If they used text verification
timedOut Boolean @default(false) // If they got kicked out of verification because they timed out serverVegan Boolean @default(false) // People that went vegan on the server
// complete Boolean @default(false) // If the verification was incomplete
// Roles they got from verification
vegan Boolean @default(false) // If they got verified as a vegan
activist Boolean @default(false) // If they got the activist role when they verified
trusted Boolean @default(false) // If they got the trusted role when they verified
vegCurious Boolean @default(false) // If they got the Veg Curious role
convinced Boolean @default(false)
text Boolean @default(false) // If they used text verification
serverVegan Boolean @default(false) // People that went vegan on the server
// Stats on verification
reason Int?
where Int?
length Int?
reasoning Int?
life Int?
food Int?
notes String? notes String?
} }
model Xp {
user User @relation(fields: [userId], references: [id])
userId String @id
level Int @default(0)
xp Int @default(0)
xpForNextLevel Int @default(0)
messageCount Int @default(0)
lastMessage DateTime @default(now())
}
// Economy
model Balance {
user User @relation(fields: [userId], references: [id])
userId String @id
balance Int
}
model Daily {
id Int @id @default(autoincrement())
user User @relation(fields: [userId], references: [id])
userId String
amount Int
time DateTime @default(now())
}
model Payment {
id Int @id @default(autoincrement())
sender User @relation("sendPayment", fields: [senderId], references: [id])
senderId String
recipient User @relation("recievePayment", fields: [recipientId], references: [id])
recipientId String
amount Int
reason String
time DateTime @default(now())
}
// Tracking roles for leaving the server
model LeaveLog {
id Int @id @default(autoincrement())
user User @relation(fields: [userId], references: [id])
userId String
time DateTime @default(now())
roles String[]
}
model RoleLog {
id Int @id @default(autoincrement())
user User @relation("userRoleLog", fields: [userId], references: [id])
userId String
mod User @relation("modRoleLog", fields: [modId], references: [id])
modId String
role Role @relation(fields: [roleId], references: [id])
roleId String
add Boolean @default(false)
time DateTime @default(now())
}
model Role {
id String @id
name String
staff Boolean
RoleLog RoleLog[]
}
model FunLog {
id Int @id @default(autoincrement())
sendUser User @relation("sendFunLog", fields: [sendUserId], references: [id])
sendUserId String
receiveUser User? @relation("receiveFunLog", fields: [receiveUserId], references: [id])
receiveUserId String?
type FunType @relation(fields: [typeId], references: [id])
typeId Int
}
model FunType {
id Int @id @default(autoincrement())
name String @unique
FunLog FunLog[]
}
model Counting {
id Int @id @default(autoincrement())
user User @relation("counting", fields: [userId], references: [id])
userId String
number Int // This is the number that the user has counted, if the count failed, the number should be 0
}
// Outreach
model Event {
id Int @id @default(autoincrement())
type EventType @relation(fields: [eventType], references: [type])
eventType String
leader User @relation("eventLeader", fields: [leaderId], references: [id]) // Not sure if this will stay
leaderId String
startTime DateTime @default(now())
endTime DateTime?
stats Stat[]
}
model EventType {
type String @id
Event Event[]
}
model Stat {
id Int @id @default(autoincrement())
event Event @relation(fields: [eventId], references: [id])
eventId Int
leader User @relation("statLeader", fields: [leaderId], references: [id]) // Not sure if this will stay
leaderId String
vegan Int @default(0)
considered Int @default(0)
antivegan Int @default(0)
thanked Int @default(0)
documentary Int @default(0)
educated Int @default(0)
participants ParticipantStat[]
role StatRole?
}
model StatRole {
stat Stat @relation(fields: [statId], references: [id])
statId Int @id
roleId String
channelId String
}
model ParticipantStat {
stat Stat @relation(fields: [statId], references: [id])
statId Int
user User @relation("participantUser", fields: [userId], references: [id])
userId String
@@id([statId, userId])
}
// Moderation
model Sus { model Sus {
id Int @id @default(autoincrement()) id Int @id @default(autoincrement())
user User @relation("susUser", fields: [userId], references: [id]) user User @relation("susUser", fields: [userId], references: [id])
@@ -250,16 +76,6 @@ model Sus {
note String note String
} }
model Warning {
id Int @id @default(autoincrement())
user User @relation("warnUser", fields: [userId], references: [id])
userId String
mod User @relation("warnMod", fields: [modId], references: [id])
modId String
time DateTime @default(now())
note String
}
model Restrict { model Restrict {
id Int @id @default(autoincrement()) id Int @id @default(autoincrement())
user User @relation("restUser", fields: [userId], references: [id]) user User @relation("restUser", fields: [userId], references: [id])
@@ -267,25 +83,19 @@ model Restrict {
mod User @relation("restMod", fields: [modId], references: [id]) mod User @relation("restMod", fields: [modId], references: [id])
modId String modId String
startTime DateTime @default(now()) startTime DateTime @default(now())
endMod User? @relation("endRestMod", fields: [endModId], references: [id]) endedTime DateTime?
endModId String?
endTime DateTime?
reason String reason String
section Int
} }
model Ban { model Ban {
id Int @id @default(autoincrement()) id Int @id @default(autoincrement())
user User @relation("banUser", fields: [userId], references: [id]) user User @relation("banUser", fields: [userId], references: [id])
userId String userId String
mod User @relation("banMod", fields: [modId], references: [id]) mod User @relation("banMod", fields: [modId], references: [id])
modId String modId String
time DateTime @default(now()) time DateTime @default(now())
endMod User? @relation("endBanMod", fields: [endModId], references: [id]) active Boolean @default(true)
endModId String? reason String
endTime DateTime?
active Boolean @default(true)
reason String
} }
model TempBan { model TempBan {
@@ -295,20 +105,7 @@ model TempBan {
mod User @relation("tbanMod", fields: [modId], references: [id]) mod User @relation("tbanMod", fields: [modId], references: [id])
modId String modId String
startTime DateTime @default(now()) startTime DateTime @default(now())
endMod User? @relation("endTbanMod", fields: [endModId], references: [id]) endedTime DateTime
endModId String?
endTime DateTime
active Boolean @default(true) active Boolean @default(true)
reason String reason String
} }
model VCMute {
id Int @id @default(autoincrement())
user User @relation("vcMuteUser", fields: [userId], references: [id])
userId String
mod User @relation("vcMuteMod", fields: [modId], references: [id])
modId String
time DateTime @default(now())
endTime DateTime?
reason String?
}

View File

@@ -1,231 +0,0 @@
// SPDX-License-Identifier: GPL-3.0-or-later
/*
Animal Rights Advocates Discord Bot
Copyright (C) 2023 Anthony Berg
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { Command, RegisterBehavior } from '@sapphire/framework';
import { ChannelType } from 'discord.js';
import IDs from '#utils/ids';
export class AccessCommand extends Command {
public constructor(context: Command.LoaderContext, options: Command.Options) {
super(context, {
...options,
name: 'access',
description:
'Manages channel permissions for ModMails, Private channels, and restricted channels',
preconditions: ['CoordinatorOnly'],
});
}
// Registers that this is a slash command
public override registerApplicationCommands(registry: Command.Registry) {
registry.registerChatInputCommand(
(builder) =>
builder
.setName(this.name)
.setDescription(this.description)
.addStringOption((option) =>
option
.setName('permission')
.setDescription('Select permissions for the user/role')
.setRequired(true)
.addChoices(
{ name: 'Add', value: 'add' },
{ name: 'Read', value: 'read' },
{ name: 'Remove', value: 'remove' },
{ name: 'Reset', value: 'reset' },
),
)
.addChannelOption((option) =>
option
.setName('channel')
.setDescription('Channel to change these permissions on')
.setRequired(true),
)
.addUserOption((option) =>
option
.setName('user')
.setDescription('User to set these permissions for'),
)
.addRoleOption((option) =>
option
.setName('role')
.setDescription('Role to set these permissions for'),
),
{
behaviorWhenNotIdentical: RegisterBehavior.Overwrite,
},
);
}
// Command run
public async chatInputRun(interaction: Command.ChatInputCommandInteraction) {
// Check that the command was run in the Guild
if (!interaction.inCachedGuild()) {
await interaction.reply({
content: 'This command can only be run in a server!',
ephemeral: true,
});
return;
}
// Get the arguments
const permission = interaction.options.getString('permission', true);
const channel = interaction.options.getChannel('channel', true);
const user = interaction.options.getUser('user');
const role = interaction.options.getRole('role');
// Checks if all the variables are of the right type
if (user === null && role === null) {
await interaction.reply({
content: 'Error fetching slash command data!',
ephemeral: true,
});
return;
}
// If user and role is provided, the return an error
if (user !== null && role !== null) {
await interaction.reply({
content:
'You have entered a user and a role at the same time! Please only enter one at a time.',
ephemeral: true,
});
return;
}
// Checks that the channel is a GuildText or GuildVoice, otherwise, return error
if (
channel.type !== ChannelType.GuildText &&
channel.type !== ChannelType.GuildVoice
) {
await interaction.reply({
content: 'Please only select a text or voice channel!',
ephemeral: true,
});
return;
}
// If the channel is not in the categories ModMail, Private, Restricted, the return error
if (
channel.parentId !== IDs.categories.modMail &&
channel.parentId !== IDs.categories.private &&
channel.parentId !== IDs.categories.restricted
) {
await interaction.reply({
content: 'Channel is not in ModMail/Private/Restricted category!',
ephemeral: true,
});
return;
}
// Create variable for either User or Role to update permissions for
let permId: string;
if (user !== null) {
permId = user.id;
} else if (role !== null) {
permId = role.id;
} else {
await interaction.reply({
content: 'Could not find the role to edit permissions!',
ephemeral: true,
});
return;
}
// Set permissions of voice channel
if (channel.type === ChannelType.GuildVoice) {
switch (permission) {
case 'add':
await channel.permissionOverwrites.create(permId, {
ViewChannel: true,
Connect: true,
Speak: true,
SendMessages: true,
ReadMessageHistory: true,
});
break;
case 'view':
await channel.permissionOverwrites.create(permId, {
ViewChannel: true,
Connect: true,
Speak: false,
SendMessages: false,
AddReactions: false,
ReadMessageHistory: true,
});
break;
case 'remove':
await channel.permissionOverwrites.create(permId, {
ViewChannel: false,
Connect: false,
Speak: false,
SendMessages: false,
ReadMessageHistory: false,
});
break;
case 'reset':
await channel.permissionOverwrites.delete(permId);
break;
default:
await interaction.reply({
content: 'Incorrect permission option!',
ephemeral: true,
});
return;
}
} else {
// Set permissions of text channel
switch (permission) {
case 'add':
await channel.permissionOverwrites.create(permId, {
ViewChannel: true,
SendMessages: true,
ReadMessageHistory: true,
});
break;
case 'view':
await channel.permissionOverwrites.create(permId, {
ViewChannel: true,
SendMessages: false,
AddReactions: false,
ReadMessageHistory: true,
});
break;
case 'remove':
await channel.permissionOverwrites.create(permId, {
ViewChannel: false,
SendMessages: false,
ReadMessageHistory: false,
});
break;
case 'reset':
await channel.permissionOverwrites.delete(permId);
break;
default:
await interaction.reply({
content: 'Incorrect permission option!',
ephemeral: true,
});
return;
}
}
await interaction.reply(`Successfully updated permissions for ${channel}`);
}
}

View File

@@ -1,143 +0,0 @@
// SPDX-License-Identifier: GPL-3.0-or-later
/*
Animal Rights Advocates Discord Bot
Copyright (C) 2023 Anthony Berg
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { Args, Command, RegisterBehavior } from '@sapphire/framework';
import { Message, MessageFlagsBitField } from 'discord.js';
import { ChannelType, TextChannel } from 'discord.js';
export class AnonymousCommand extends Command {
public constructor(context: Command.LoaderContext, options: Command.Options) {
super(context, {
...options,
name: 'anonymous',
aliases: ['anon'],
description: 'Bot sends a message for you',
preconditions: ['CoordinatorOnly'],
});
}
// Registers that this is a slash command
public override registerApplicationCommands(registry: Command.Registry) {
registry.registerChatInputCommand(
(builder) =>
builder
.setName(this.name)
.setDescription(this.description)
.addStringOption((option) =>
option
.setName('message')
.setDescription('The message the bot will send')
.setRequired(true),
)
.addChannelOption((option) =>
option
.setName('channel')
.setDescription('The channel the bot will send the message'),
),
{
behaviorWhenNotIdentical: RegisterBehavior.Overwrite,
},
);
}
// Command run
public async chatInputRun(interaction: Command.ChatInputCommandInteraction) {
// Get the arguments
const message = interaction.options.getString('message', true);
let channel = interaction.options.getChannel('channel');
const { guild } = interaction;
// Checks if all the variables are of the right type
if (guild === null) {
await interaction.reply({
content: 'Error fetching guild!',
flags: MessageFlagsBitField.Flags.Ephemeral,
withResponse: true,
});
return;
}
if (channel === null) {
if (interaction.channel === null) {
await interaction.reply({
content: 'Error getting the channel!',
flags: MessageFlagsBitField.Flags.Ephemeral,
withResponse: true,
});
return;
}
if (!interaction.channel.isSendable()) {
await interaction.reply({
content: `I do not have sufficient permissions to send a message in ${interaction.channel}!`,
flags: MessageFlagsBitField.Flags.Ephemeral,
withResponse: true,
});
return;
}
await interaction.channel.send(message);
await interaction.reply({
content: 'Sent the message',
flags: MessageFlagsBitField.Flags.Ephemeral,
withResponse: true,
});
return;
}
if (channel.type !== ChannelType.GuildText) {
await interaction.reply({
content: 'Could not send, unsupported text channel!',
flags: MessageFlagsBitField.Flags.Ephemeral,
withResponse: true,
});
}
channel = channel as TextChannel;
await channel.send(message);
await interaction.reply({
content: 'Sent the message',
flags: MessageFlagsBitField.Flags.Ephemeral,
withResponse: true,
});
}
public async messageRun(message: Message, args: Args) {
// Get arguments
const channel = await args.pick('channel');
const text = args.finished ? null : await args.rest('string');
if (text === null) {
await message.react('❌');
await message.reply('No message was provided!');
return;
}
if (channel.isSendable()) {
await channel.send(text);
} else {
await message.react('❌');
await message.reply('No channel was provided!');
return;
}
await message.react('✅');
}
}

View File

@@ -1,108 +0,0 @@
// SPDX-License-Identifier: GPL-3.0-or-later
/*
Animal Rights Advocates Discord Bot
Copyright (C) 2023 Anthony Berg
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { Args, Command, RegisterBehavior } from '@sapphire/framework';
import type { Message } from 'discord.js';
export class ClearCommand extends Command {
public constructor(context: Command.LoaderContext, options: Command.Options) {
super(context, {
...options,
name: 'clear',
description: 'Deletes 1-100 messages in bulk',
preconditions: ['CoordinatorOnly'],
});
}
// Registers that this is a slash command
public override registerApplicationCommands(registry: Command.Registry) {
registry.registerChatInputCommand(
(builder) =>
builder
.setName(this.name)
.setDescription(this.description)
.addIntegerOption((option) =>
option
.setName('messages')
.setDescription('Number of messages to clear')
.setRequired(true)
.setMinValue(1)
.setMaxValue(100),
),
{
behaviorWhenNotIdentical: RegisterBehavior.Overwrite,
},
);
}
// Command run
public async chatInputRun(interaction: Command.ChatInputCommandInteraction) {
// Get the arguments
const messages = interaction.options.getInteger('messages', true);
const { channel } = interaction;
if (channel === null || channel.isDMBased()) {
await interaction.reply({
content: 'Could not fetch channel!',
ephemeral: true,
fetchReply: true,
});
return;
}
await channel.bulkDelete(messages);
await interaction.reply({
content: `Successfully deleted ${messages} messages!`,
ephemeral: true,
fetchReply: true,
});
}
public async messageRun(message: Message, args: Args) {
// Get arguments
const messages = await args.pick('integer');
if (messages > 100) {
await message.react('❌');
await message.reply('You can only clear up to 100 messages at a time!');
return;
}
if (messages < 1) {
await message.react('❌');
await message.reply('You need to at least clear 1 message!');
return;
}
const { channel } = message;
if (!channel.isTextBased() || channel.isDMBased()) {
await message.react('❌');
await message.reply('Unsupported channel type!');
return;
}
await channel.bulkDelete(messages);
await message.reply({
content: `Successfully deleted ${messages} messages!`,
failIfNotExists: false,
});
}
}

View File

@@ -1,397 +0,0 @@
// SPDX-License-Identifier: GPL-3.0-or-later
/*
Animal Rights Advocates Discord Bot
Copyright (C) 2023 Anthony Berg
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { RegisterBehavior } from '@sapphire/framework';
import { Subcommand } from '@sapphire/plugin-subcommands';
import type { Guild, TextChannel, Snowflake } from 'discord.js';
import {
CategoryChannel,
ChannelType,
EmbedBuilder,
GuildMember,
PermissionsBitField,
time,
} from 'discord.js';
import IDs from '#utils/ids';
export class PrivateCommand extends Subcommand {
public constructor(
context: Subcommand.LoaderContext,
options: Subcommand.Options,
) {
super(context, {
...options,
name: 'private',
subcommands: [
{
name: 'create',
chatInputRun: 'create',
},
{
name: 'delete',
chatInputRun: 'delete',
},
],
description: 'Creates/deletes private channels for a user',
preconditions: ['CoordinatorOnly'],
});
}
// Registers that this is a slash command
public override registerApplicationCommands(registry: Subcommand.Registry) {
registry.registerChatInputCommand(
(builder) =>
builder
.setName(this.name)
.setDescription(this.description)
.addSubcommand((command) =>
command
.setName('create')
.setDescription('Create a private channel')
.addUserOption((option) =>
option
.setName('user')
.setDescription('User to create a private channel with')
.setRequired(true),
),
)
.addSubcommand((command) =>
command
.setName('delete')
.setDescription('Delete a private channel')
.addUserOption((option) =>
option
.setName('user')
.setDescription('User to delete a private channel from'),
),
),
{
behaviorWhenNotIdentical: RegisterBehavior.Overwrite,
},
);
}
public async create(interaction: Subcommand.ChatInputCommandInteraction) {
// Get the arguments
const user = interaction.options.getUser('user', true);
const modUser = interaction.user;
const { guild } = interaction;
await interaction.deferReply({ ephemeral: true });
// Checks if all the variables are of the right type
if (guild === null) {
await interaction.editReply({
content: 'Error fetching mod!',
});
return;
}
const member = guild.members.cache.get(user.id);
const mod = guild.members.cache.get(modUser.id);
// Checks if guildMember is null
if (member === undefined || mod === undefined) {
await interaction.editReply({
content: 'Error fetching users!',
});
return;
}
const [name, coordinator] = this.getCoordinator(mod);
if (this.checkPrivate(member.id, coordinator, guild)) {
await interaction.editReply({
content: 'A private channel already exists!',
});
return;
}
const voiceChannel = await guild.channels.create({
name: 'Private Voice Channel',
type: ChannelType.GuildVoice,
parent: IDs.categories.private,
permissionOverwrites: [
{
id: guild.roles.everyone,
deny: [PermissionsBitField.Flags.ViewChannel],
},
{
id: user.id,
allow: [PermissionsBitField.Flags.ViewChannel],
},
{
id: coordinator,
allow: [
PermissionsBitField.Flags.SendMessages,
PermissionsBitField.Flags.ViewChannel,
PermissionsBitField.Flags.Connect,
PermissionsBitField.Flags.MuteMembers,
],
},
],
});
let privateChannel: TextChannel;
let bannedName = false;
try {
privateChannel = await guild.channels.create({
name: `🍂┃${member.user.username}-private-${name}`,
type: ChannelType.GuildText,
topic: `Private channel. ${user.id} ${coordinator} ${voiceChannel.id} (Please do not change this)`,
parent: IDs.categories.private,
permissionOverwrites: [
{
id: guild.roles.everyone,
allow: [PermissionsBitField.Flags.ReadMessageHistory],
deny: [PermissionsBitField.Flags.ViewChannel],
},
{
id: user.id,
allow: [PermissionsBitField.Flags.ViewChannel],
},
{
id: coordinator,
allow: [
PermissionsBitField.Flags.SendMessages,
PermissionsBitField.Flags.ViewChannel,
],
},
],
});
} catch {
privateChannel = await guild.channels.create({
name: `🍂┃${member.user.id}-private-${name}`,
type: ChannelType.GuildText,
topic: `Private channel. ${user.id} ${coordinator} ${voiceChannel.id} (Please do not change this)`,
parent: IDs.categories.private,
permissionOverwrites: [
{
id: guild.roles.everyone,
allow: [PermissionsBitField.Flags.ReadMessageHistory],
deny: [PermissionsBitField.Flags.ViewChannel],
},
{
id: user.id,
allow: [PermissionsBitField.Flags.ViewChannel],
},
{
id: coordinator,
allow: [
PermissionsBitField.Flags.SendMessages,
PermissionsBitField.Flags.ViewChannel,
],
},
],
});
bannedName = true;
}
if (!bannedName) {
await voiceChannel.setName(`${member.user.username}-private-${name}`);
} else {
await voiceChannel.setName(`${member.user.id}-private-${name}`);
}
const joinTime = time(member.joinedAt!);
const registerTime = time(member.user.createdAt);
const embed = new EmbedBuilder()
.setColor(member.displayHexColor)
.setTitle(`Private channel for ${member.user.username}`)
.setDescription(`${member}`)
.setThumbnail(member.user.displayAvatarURL())
.addFields(
{ name: 'Joined:', value: `${joinTime}`, inline: true },
{ name: 'Created:', value: `${registerTime}`, inline: true },
);
await privateChannel.send({ embeds: [embed] });
await interaction.editReply({
content: `Created the private channel: ${privateChannel}`,
});
}
public async delete(interaction: Subcommand.ChatInputCommandInteraction) {
// Get the arguments
const user = interaction.options.getUser('user');
const modUser = interaction.user;
const { guild, channel } = interaction;
await interaction.deferReply({ ephemeral: true });
// Checks if all the variables are of the right type
if (guild === null || channel === null) {
await interaction.editReply({
content: 'Error fetching user!',
});
return;
}
const mod = guild.members.cache.get(modUser.id);
// Checks if guildMember is null
if (mod === undefined) {
await interaction.editReply({
content: 'Error fetching users!',
});
return;
}
const coordinatorInfo = this.getCoordinator(mod);
const coordinator = coordinatorInfo[1];
let topic: string[];
if (user === null) {
if (channel.type !== ChannelType.GuildText) {
await interaction.editReply({
content:
'Please make sure you ran this command in the original private text channel!',
});
return;
}
if (channel.parentId !== IDs.categories.private) {
await interaction.editReply({
content:
'Please make sure you ran this command in the original private text channel!',
});
return;
}
if (channel.topic === null) {
await interaction.editReply({
content: "There was an error with this channel's topic!",
});
return;
}
topic = channel.topic.split(' ');
await channel.delete();
const vcId = topic[topic.indexOf(coordinator) + 1];
const voiceChannel = guild.channels.cache.get(vcId);
if (
voiceChannel !== undefined &&
voiceChannel.parentId === IDs.categories.private
) {
await voiceChannel.delete();
}
return;
}
const category = guild.channels.cache.get(IDs.categories.private) as
| CategoryChannel
| undefined;
if (category === undefined) {
await interaction.editReply({
content: 'Could not find category!',
});
return;
}
const textChannels = category.children.cache.filter(
(c) => c.type === ChannelType.GuildText,
);
textChannels.forEach((c) => {
const textChannel = c as TextChannel;
// Checks if the channel topic has the user's snowflake
if (textChannel.topic?.includes(user?.id)) {
topic = textChannel.topic.split(' ');
const vcId = topic[topic.indexOf(coordinator) + 1];
const voiceChannel = guild.channels.cache.get(vcId);
if (
voiceChannel !== undefined &&
voiceChannel.parentId === IDs.categories.private
) {
voiceChannel.delete();
}
textChannel.delete();
}
});
await interaction.editReply({
content: `Successfully deleted the channel for ${user}`,
});
}
private getCoordinator(user: GuildMember) {
let name: string;
let id: string;
if (user.roles.cache.has(IDs.roles.staff.devCoordinator)) {
name = 'dev';
id = IDs.roles.staff.devCoordinator;
} else if (user.roles.cache.has(IDs.roles.staff.modCoordinator)) {
name = 'mod';
id = IDs.roles.staff.modCoordinator;
} else if (user.roles.cache.has(IDs.roles.staff.diversityCoordinator)) {
name = 'diversity';
id = IDs.roles.staff.diversityCoordinator;
} else if (user.roles.cache.has(IDs.roles.staff.mentorCoordinator)) {
name = 'mentor';
id = IDs.roles.staff.mentorCoordinator;
} else if (user.roles.cache.has(IDs.roles.staff.verifierCoordinator)) {
name = 'verifier';
id = IDs.roles.staff.verifierCoordinator;
} else if (user.roles.cache.has(IDs.roles.staff.eventCoordinator)) {
name = 'event';
id = IDs.roles.staff.eventCoordinator;
} else if (user.roles.cache.has(IDs.roles.staff.mediaCoordinator)) {
name = 'media';
id = IDs.roles.staff.mediaCoordinator;
} else if (user.roles.cache.has(IDs.roles.staff.hrCoordinator)) {
name = 'hr';
id = IDs.roles.staff.hrCoordinator;
} else {
name = 'coordinator';
id = IDs.roles.staff.coordinator;
}
return [name, id];
}
private checkPrivate(user: Snowflake, coordinator: string, guild: Guild) {
const category = guild.channels.cache.get(IDs.categories.private) as
| CategoryChannel
| undefined;
if (category === undefined) {
return true;
}
const textChannels = category.children.cache.filter(
(c) => c.type === ChannelType.GuildText,
);
let exists = false;
textChannels.forEach((c) => {
const textChannel = c as TextChannel;
// Checks if the channel topic has the user's snowflake
if (
textChannel.topic?.includes(user) &&
textChannel.topic?.includes(coordinator)
) {
exists = true;
}
});
return exists;
}
}

View File

@@ -1,125 +0,0 @@
// SPDX-License-Identifier: GPL-3.0-or-later
/*
Animal Rights Advocates Discord Bot
Copyright (C) 2023 Anthony Berg
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { Command, RegisterBehavior } from '@sapphire/framework';
import type { User, Guild, Message } from 'discord.js';
import { updateUser } from '#utils/database/dbExistingUser';
import { getBalance } from '#utils/database/fun/economy';
import { EmbedBuilder } from 'discord.js';
export class BalanceCommand extends Command {
public constructor(context: Command.LoaderContext, options: Command.Options) {
super(context, {
...options,
name: 'balance',
aliases: ['bal'],
description: 'Gets the amount of ARAs you have',
});
}
// Registers that this is a slash command
public override registerApplicationCommands(registry: Command.Registry) {
registry.registerChatInputCommand(
(builder) => builder.setName(this.name).setDescription(this.description),
{
behaviorWhenNotIdentical: RegisterBehavior.Overwrite,
},
);
}
// Command run
public async chatInputRun(interaction: Command.ChatInputCommandInteraction) {
const { user, guild } = interaction;
if (guild === null) {
await interaction.reply({
content: 'Could not find the guild!',
ephemeral: true,
});
return;
}
await interaction.deferReply();
const info = await this.balance(user, guild);
await interaction.editReply({
content: info.message,
embeds: info.embeds,
});
}
public async messageRun(message: Message) {
const user = message.member?.user;
const { guild } = message;
if (user === undefined) {
await message.react('❌');
await message.reply('Could not find your user!');
return;
}
if (guild === null) {
await message.react('❌');
await message.reply('Could not find the guild!');
return;
}
const info = await this.balance(user, guild);
await message.reply({
content: info.message,
embeds: info.embeds,
});
if (!info.success) {
await message.react('❌');
}
}
private async balance(user: User, guild: Guild) {
const info = {
message: '',
embeds: [] as EmbedBuilder[],
success: false,
};
const member = guild.members.cache.get(user.id);
if (member === undefined) {
info.message = 'Could not find your guild member!';
return info;
}
await updateUser(member);
const balance = await getBalance(user.id);
const embed = new EmbedBuilder()
.setColor('#00ff7d')
.setAuthor({
name: `${member.displayName}'s Account`,
iconURL: `${user.displayAvatarURL()}`,
})
.addFields({ name: 'Balance', value: `${balance.balance} ARA` });
info.success = true;
info.embeds.push(embed);
return info;
}
}

View File

@@ -1,187 +0,0 @@
// SPDX-License-Identifier: GPL-3.0-or-later
/*
Animal Rights Advocates Discord Bot
Copyright (C) 2023 Anthony Berg
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { Command, RegisterBehavior } from '@sapphire/framework';
import { Time } from '@sapphire/time-utilities';
import type { User, Guild, GuildMember, Message } from 'discord.js';
import { updateUser } from '#utils/database/dbExistingUser';
import { daily, getLastDaily } from '#utils/database/fun/economy';
import { EmbedBuilder } from 'discord.js';
import IDs from '#utils/ids';
export class DailyCommand extends Command {
public constructor(context: Command.LoaderContext, options: Command.Options) {
super(context, {
...options,
name: 'daily',
description: 'Get given an amount of money once a day',
});
}
// Registers that this is a slash command
public override registerApplicationCommands(registry: Command.Registry) {
registry.registerChatInputCommand(
(builder) => builder.setName(this.name).setDescription(this.description),
{
behaviorWhenNotIdentical: RegisterBehavior.Overwrite,
},
);
}
// Command run
public async chatInputRun(interaction: Command.ChatInputCommandInteraction) {
const { user, guild } = interaction;
if (guild === null) {
await interaction.reply({
content: 'Could not find the guild!',
ephemeral: true,
});
return;
}
await interaction.deferReply();
const info = await this.runDaily(user, guild);
await interaction.editReply({
content: info.message,
embeds: info.embeds,
});
}
public async messageRun(message: Message) {
const user = message.member?.user;
const { guild } = message;
if (user === undefined) {
await message.react('❌');
await message.reply('Could not find your user!');
return;
}
if (guild === null) {
await message.react('❌');
await message.reply('Could not find the guild!');
return;
}
const info = await this.runDaily(user, guild);
await message.reply({
content: info.message,
embeds: info.embeds,
});
if (!info.success) {
await message.react('❌');
}
}
private async runDaily(user: User, guild: Guild) {
const amount = 10;
const time = Time.Hour * 18;
const info = {
message: '',
embeds: [] as EmbedBuilder[],
success: false,
};
const lastDaily = await getLastDaily(user.id);
if (
lastDaily !== null &&
new Date().getTime() - lastDaily.time.getTime() < time
) {
info.message =
'You have already claimed your daily, come back later to claim it!';
return info;
}
const member = guild.members.cache.get(user.id);
if (member === undefined) {
info.message = 'Could not find your guild member!';
return info;
}
// Give bonus for the user
const bonus = await this.giveBonus(member);
await updateUser(member);
const [db] = await Promise.all([daily(user.id, amount + bonus)]);
const balance = db.Balance?.balance;
const embed = new EmbedBuilder()
.setColor('#00ff7d')
.setAuthor({
name: 'Daily Reward',
iconURL: `${user.displayAvatarURL()}`,
})
.addFields({
name: 'Collected',
value: `${amount} ARA`,
inline: bonus > 0,
});
if (bonus > 0) {
embed.addFields(
{ name: 'Bonus', value: `${bonus} ARA`, inline: true },
{ name: '\u200B', value: 'Thank you for contributing to ARA! :D' },
);
}
if (balance !== undefined) {
embed.setFooter({ text: `New Balance: ${balance}` });
}
info.success = true;
info.embeds.push(embed);
return info;
}
private async giveBonus(member: GuildMember) {
let bonus = 0;
const amount = [
{ role: member.roles.premiumSubscriberRole?.id, amount: 5 },
{ role: IDs.roles.staff.coordinator, amount: 2 },
{ role: IDs.roles.staff.moderator, amount: 2 },
{ role: IDs.roles.staff.trialModerator, amount: 2 },
{ role: IDs.roles.staff.restricted, amount: 1 },
{ role: IDs.roles.staff.verifier, amount: 2 },
{ role: IDs.roles.staff.trialVerifier, amount: 2 },
{ role: IDs.roles.staff.developer, amount: 2 },
{ role: IDs.roles.staff.mentor, amount: 2 },
{ role: IDs.roles.stageHost, amount: 1 },
];
member.roles.cache.forEach((role) => {
amount.forEach((check) => {
if (role.id === check.role) {
bonus += check.amount;
}
});
});
return bonus;
}
}

View File

@@ -1,228 +0,0 @@
// SPDX-License-Identifier: GPL-3.0-or-later
/*
Animal Rights Advocates Discord Bot
Copyright (C) 2023 Anthony Berg
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { Args, Command, RegisterBehavior } from '@sapphire/framework';
import type { User, Guild, Message } from 'discord.js';
import { updateUser } from '#utils/database/dbExistingUser';
import { getBalance, transfer } from '#utils/database/fun/economy';
import { EmbedBuilder, TextChannel } from 'discord.js';
import IDs from '#utils/ids';
export class BalanceCommand extends Command {
public constructor(context: Command.LoaderContext, options: Command.Options) {
super(context, {
...options,
name: 'pay',
description: 'Give a user an amount of money',
});
}
// Registers that this is a slash command
public override registerApplicationCommands(registry: Command.Registry) {
registry.registerChatInputCommand(
(builder) =>
builder
.setName(this.name)
.setDescription(this.description)
.addUserOption((option) =>
option
.setName('user')
.setDescription('The user to give the money to')
.setRequired(true),
)
.addIntegerOption((option) =>
option
.setName('amount')
.setDescription('The amount to give to the user')
.setMinValue(1)
.setRequired(true),
)
.addStringOption((option) =>
option
.setName('reason')
.setDescription('The reason/reference for the transaction')
.setRequired(true),
),
{
behaviorWhenNotIdentical: RegisterBehavior.Overwrite,
},
);
}
// Command run
public async chatInputRun(interaction: Command.ChatInputCommandInteraction) {
const recipient = interaction.options.getUser('user', true);
const amount = interaction.options.getInteger('amount', true);
const reason = interaction.options.getString('reason', true);
const { user, guild } = interaction;
if (guild === null) {
await interaction.reply({
content: 'Could not find the guild!',
ephemeral: true,
});
return;
}
await interaction.deferReply();
const info = await this.pay(user, recipient, amount, reason, guild);
await interaction.editReply({
content: info.message,
embeds: info.embeds,
});
}
public async messageRun(message: Message, args: Args) {
let recipient: User;
try {
recipient = await args.pick('user');
} catch {
await message.react('❌');
await message.reply('User was not provided!');
return;
}
let amount: number;
try {
amount = await args.pick('integer');
} catch {
await message.react('❌');
await message.reply('Amount was not provided!');
return;
}
const reason = args.finished ? null : await args.rest('string');
if (reason === null) {
await message.react('❌');
await message.reply('Reason/reference was not provided!');
return;
}
const user = message.member?.user;
const { guild } = message;
if (user === undefined) {
await message.react('❌');
await message.reply('Could not find your user!');
return;
}
if (guild === null) {
await message.react('❌');
await message.reply('Could not find the guild!');
return;
}
const info = await this.pay(user, recipient, amount, reason, guild);
await message.reply({
content: info.message,
embeds: info.embeds,
});
if (!info.success) {
await message.react('❌');
}
}
private async pay(
user: User,
recipient: User,
amount: number,
reason: string,
guild: Guild,
) {
const info = {
message: '',
embeds: [] as EmbedBuilder[],
success: false,
};
// Check the amount to be paid is greater than 0
if (amount < 1) {
info.message =
"You need to actually give money, you can't send nothing or try to break the " +
'economy 😭';
return info;
}
const member = guild.members.cache.get(user.id);
const recipientMember = guild.members.cache.get(recipient.id);
if (member === undefined) {
info.message = 'Could not find your guild member!';
return info;
}
if (recipientMember === undefined) {
info.message = 'Could not find the user!';
return info;
}
await updateUser(member);
await updateUser(recipientMember);
const balance = await getBalance(user.id);
if (balance.balance < amount) {
info.message = "You don't have enough money to send!";
return info;
}
await transfer(user.id, recipient.id, amount, reason);
const embed = new EmbedBuilder()
.setColor('#00ff7d')
.setAuthor({
name: `Transfer to ${recipientMember.displayName}`,
iconURL: `${recipientMember.displayAvatarURL()}`,
})
.addFields(
{ name: 'From', value: `${user}`, inline: true },
{ name: 'To', value: `${recipient}`, inline: true },
{ name: 'Amount', value: `${amount} ARA` },
{ name: 'Reason', value: reason },
);
info.success = true;
info.embeds.push(embed);
// Log the payment in the server
let logChannel = guild.channels.cache.get(IDs.channels.logs.economy) as
| TextChannel
| undefined;
if (logChannel === undefined) {
logChannel = (await guild.channels.fetch(IDs.channels.logs.economy)) as
| TextChannel
| undefined;
if (logChannel === undefined) {
this.container.logger.error('Pay Error: Could not fetch log channel');
return info;
}
}
const logEmbed = new EmbedBuilder(embed.data);
logEmbed.setTimestamp().setFooter({ text: `ID: ${user.id}` });
await logChannel.send({ embeds: [logEmbed] });
return info;
}
}

View File

@@ -18,24 +18,25 @@
*/ */
import { Command, RegisterBehavior } from '@sapphire/framework'; import { Command, RegisterBehavior } from '@sapphire/framework';
import { EmbedBuilder, GuildMember } from 'discord.js'; import { MessageEmbed } from 'discord.js';
import { N1984 } from '#utils/gifs'; import { N1984 } from '../../utils/gifs';
import { addFunLog, countTotal } from '#utils/database/fun/fun';
export class N1984Command extends Command { class N1984Command extends Command {
public constructor(context: Command.LoaderContext, options: Command.Options) { public constructor(context: Command.Context, options: Command.Options) {
super(context, { super(context, {
...options, ...options,
name: '1984', name: '1984',
description: 'this is literally 1984', description: 'this is literally 1984',
preconditions: [['CoordinatorOnly', 'ModOnly']], preconditions: ['ModOnly'],
}); });
} }
// Registers that this is a slash command // Registers that this is a slash command
public override registerApplicationCommands(registry: Command.Registry) { public override registerApplicationCommands(registry: Command.Registry) {
registry.registerChatInputCommand( registry.registerChatInputCommand(
(builder) => builder.setName(this.name).setDescription(this.description), (builder) => builder
.setName(this.name)
.setDescription(this.description),
{ {
behaviorWhenNotIdentical: RegisterBehavior.Overwrite, behaviorWhenNotIdentical: RegisterBehavior.Overwrite,
}, },
@@ -43,42 +44,24 @@ export class N1984Command extends Command {
} }
// Command run // Command run
public async chatInputRun(interaction: Command.ChatInputCommandInteraction) { public async chatInputRun(interaction: Command.ChatInputInteraction) {
// Get the user // Get the user
const { member } = interaction; // TODO exception handling
const member = interaction.member!.user;
// Type checks const memberGuildMember = interaction.guild!.members.cache.get(member.id)!;
if (!(member instanceof GuildMember)) {
await interaction.reply({
ephemeral: true,
content: 'Failed to fetch your user on the bot!',
});
return;
}
await addFunLog(member.id, '1984');
const count = await countTotal(member.id, '1984');
let embedFooter: string;
if (count === 1) {
embedFooter = `${member.displayName} 1984'd the server for the first time!`;
} else {
embedFooter = `${member.displayName} 1984'd the server ${count} times!`;
}
// Creates the embed for the 1984 reaction // Creates the embed for the 1984 reaction
// Add a 1 in 1000 chance of Dantas literally making ARA 1984 // Add a 1 in 1000 chance of Dantas literally making ARA 1984
const random1984 = const random1984 = Math.random() < 0.001 ? 'https://c.tenor.com/0BwU0BjWYX4AAAAC/arthuria-dantas.gif'
Math.random() < 0.001 : N1984[Math.floor(Math.random() * N1984.length)];
? 'https://c.tenor.com/0BwU0BjWYX4AAAAC/arthuria-dantas.gif' const n1984Embed = new MessageEmbed()
: N1984[Math.floor(Math.random() * N1984.length)];
const n1984Embed = new EmbedBuilder()
.setColor('#ffffff') .setColor('#ffffff')
.setTitle(`${member.displayName} is happy!`) .setTitle(`${memberGuildMember.displayName} is happy!`)
.setImage(random1984) .setImage(random1984);
.setFooter({ text: embedFooter });
// Send the embed // Send the embed
await interaction.reply({ embeds: [n1984Embed], fetchReply: true }); await interaction.reply({ embeds: [n1984Embed], fetchReply: true });
} }
} }
export default N1984Command;

View File

@@ -1,79 +0,0 @@
// SPDX-License-Identifier: GPL-3.0-or-later
/*
Animal Rights Advocates Discord Bot
Copyright (C) 2023 Anthony Berg, Stefanie Merceron
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { Command, RegisterBehavior } from '@sapphire/framework';
import { EmbedBuilder, GuildMember } from 'discord.js';
import { Cringe } from '#utils/gifs';
import { addFunLog, countTotal } from '#utils/database/fun/fun';
export class CringeCommand extends Command {
public constructor(context: Command.LoaderContext, options: Command.Options) {
super(context, {
...options,
name: 'cringe',
description: 'Express your cringe',
});
}
// Registers that this is a slash command
public override registerApplicationCommands(registry: Command.Registry) {
registry.registerChatInputCommand(
(builder) => builder.setName(this.name).setDescription(this.description),
{
behaviorWhenNotIdentical: RegisterBehavior.Overwrite,
},
);
}
// Command run
public async chatInputRun(interaction: Command.ChatInputCommandInteraction) {
// Get the user
const { member } = interaction;
// Type check
if (!(member instanceof GuildMember)) {
await interaction.reply({
ephemeral: true,
content: 'Failed to fetch your user on the bot!',
});
return;
}
await addFunLog(member.id, 'cringe');
const count = await countTotal(member.id, 'cringe');
let embedFooter: string;
if (count === 1) {
embedFooter = `${member.displayName} cringed for the first time!`;
} else {
embedFooter = `${member.displayName} cringed ${count} times!`;
}
// Creates the embed for the cringe reaction
const randomCringe = Cringe[Math.floor(Math.random() * Cringe.length)];
const cringeEmbed = new EmbedBuilder()
.setColor('#001148')
.setTitle(`${member.displayName} feels immense cringe...`)
.setImage(randomCringe)
.setFooter({ text: embedFooter });
// Send the embed
await interaction.reply({ embeds: [cringeEmbed], fetchReply: true });
}
}

View File

@@ -18,22 +18,25 @@
*/ */
import { Command, RegisterBehavior } from '@sapphire/framework'; import { Command, RegisterBehavior } from '@sapphire/framework';
import { EmbedBuilder, GuildMember } from 'discord.js'; import { MessageEmbed } from 'discord.js';
import { Happy } from '#utils/gifs'; import { Happy } from '../../utils/gifs';
export class HappyCommand extends Command { class HappyCommand extends Command {
public constructor(context: Command.LoaderContext, options: Command.Options) { public constructor(context: Command.Context, options: Command.Options) {
super(context, { super(context, {
...options, ...options,
name: 'happy', name: 'happy',
description: 'Express your happiness', description: 'Express your happiness',
preconditions: [['CoordinatorOnly', 'PatreonOnly']],
}); });
} }
// Registers that this is a slash command // Registers that this is a slash command
public override registerApplicationCommands(registry: Command.Registry) { public override registerApplicationCommands(registry: Command.Registry) {
registry.registerChatInputCommand( registry.registerChatInputCommand(
(builder) => builder.setName(this.name).setDescription(this.description), (builder) => builder
.setName(this.name)
.setDescription(this.description),
{ {
behaviorWhenNotIdentical: RegisterBehavior.Overwrite, behaviorWhenNotIdentical: RegisterBehavior.Overwrite,
}, },
@@ -41,27 +44,22 @@ export class HappyCommand extends Command {
} }
// Command run // Command run
public async chatInputRun(interaction: Command.ChatInputCommandInteraction) { public async chatInputRun(interaction: Command.ChatInputInteraction) {
// Get the user // Get the user
const { member } = interaction; // TODO exception handling
const member = interaction.member!.user;
// Type checks const memberGuildMember = interaction.guild!.members.cache.get(member.id)!;
if (!(member instanceof GuildMember)) {
await interaction.reply({
ephemeral: true,
content: 'Failed to fetch your user on the bot!',
});
return;
}
// Creates the embed for the happy reaction // Creates the embed for the happy reaction
const randomHappy = Happy[Math.floor(Math.random() * Happy.length)]; const randomHappy = Happy[Math.floor(Math.random() * Happy.length)];
const happyEmbed = new EmbedBuilder() const happyEmbed = new MessageEmbed()
.setColor('#40ff00') .setColor('#40ff00')
.setTitle(`${member.displayName} is happy!`) .setTitle(`${memberGuildMember.displayName} is happy!`)
.setImage(randomHappy); .setImage(randomHappy);
// Send the embed // Send the embed
await interaction.reply({ embeds: [happyEmbed], fetchReply: true }); await interaction.reply({ embeds: [happyEmbed], fetchReply: true });
} }
} }
export default HappyCommand;

View File

@@ -18,12 +18,11 @@
*/ */
import { Command, RegisterBehavior } from '@sapphire/framework'; import { Command, RegisterBehavior } from '@sapphire/framework';
import { EmbedBuilder, GuildMember } from 'discord.js'; import { MessageEmbed } from 'discord.js';
import { Hugs } from '#utils/gifs'; import { Hugs } from '../../utils/gifs';
import { addFunLog, countTotal } from '#utils/database/fun/fun';
export class HugCommand extends Command { class HugCommand extends Command {
public constructor(context: Command.LoaderContext, options: Command.Options) { public constructor(context: Command.Context, options: Command.Options) {
super(context, { super(context, {
...options, ...options,
name: 'hug', name: 'hug',
@@ -34,16 +33,12 @@ export class HugCommand extends Command {
// Registers that this is a slash command // Registers that this is a slash command
public override registerApplicationCommands(registry: Command.Registry) { public override registerApplicationCommands(registry: Command.Registry) {
registry.registerChatInputCommand( registry.registerChatInputCommand(
(builder) => (builder) => builder
builder .setName(this.name)
.setName(this.name) .setDescription(this.description)
.setDescription(this.description) .addUserOption((option) => option.setName('user')
.addUserOption((option) => .setDescription('User you want to hug')
option .setRequired(true)),
.setName('user')
.setDescription('User you want to hug')
.setRequired(true),
),
{ {
behaviorWhenNotIdentical: RegisterBehavior.Overwrite, behaviorWhenNotIdentical: RegisterBehavior.Overwrite,
}, },
@@ -51,52 +46,23 @@ export class HugCommand extends Command {
} }
// Command run // Command run
public async chatInputRun(interaction: Command.ChatInputCommandInteraction) { public async chatInputRun(interaction: Command.ChatInputInteraction) {
// Get the users // Get the users
const user = interaction.options.getUser('user', true); // TODO exception handling
const hugger = interaction.member; const user = interaction.options.getUser('user')!;
const hugger = interaction.member!.user;
// Type Checks const huggerGuildMember = interaction.guild!.members.cache.get(hugger.id)!;
if (!(hugger instanceof GuildMember)) {
await interaction.reply({
ephemeral: true,
content: 'Failed to fetch your user on the bot!',
});
return;
}
await addFunLog(hugger.id, 'hug', user.id);
const count = await countTotal(hugger.id, 'hug', user.id);
let embedFooter: string;
if (hugger.id === user.id) {
if (count === 1) {
embedFooter = `You hugged yourself for the first time!`;
} else {
embedFooter = `You hugged yourself ${count} times!`;
}
} else {
if (count === 1) {
embedFooter = `${hugger.displayName} hugged you for the first time!`;
} else {
embedFooter = `${hugger.displayName} hugged you ${count} times!`;
}
}
// Creates the embed for the hug // Creates the embed for the hug
const randomHug = Hugs[Math.floor(Math.random() * Hugs.length)]; const randomHug = Hugs[Math.floor(Math.random() * Hugs.length)];
const hugEmbed = new EmbedBuilder() const hugEmbed = new MessageEmbed()
.setColor('#0099ff') .setColor('#0099ff')
.setTitle(`Hug from ${hugger.displayName}`) .setTitle(`Hug from ${huggerGuildMember.displayName}`)
.setImage(randomHug) .setImage(randomHug);
.setFooter({ text: embedFooter });
// Send the hug // Send the hug
await interaction.reply({ await interaction.reply({ content: `<@${user.id}>`, embeds: [hugEmbed], fetchReply: true });
content: `${user}`,
embeds: [hugEmbed],
fetchReply: true,
});
} }
} }
export default HugCommand;

View File

@@ -18,12 +18,11 @@
*/ */
import { Command, RegisterBehavior } from '@sapphire/framework'; import { Command, RegisterBehavior } from '@sapphire/framework';
import { EmbedBuilder, GuildMember } from 'discord.js'; import { MessageEmbed } from 'discord.js';
import { Kill } from '#utils/gifs'; import { Kill } from '../../utils/gifs';
import { addFunLog, countTotal } from '#utils/database/fun/fun';
export class KillCommand extends Command { class KillCommand extends Command {
public constructor(context: Command.LoaderContext, options: Command.Options) { public constructor(context: Command.Context, options: Command.Options) {
super(context, { super(context, {
...options, ...options,
name: 'kill', name: 'kill',
@@ -34,16 +33,12 @@ export class KillCommand extends Command {
// Registers that this is a slash command // Registers that this is a slash command
public override registerApplicationCommands(registry: Command.Registry) { public override registerApplicationCommands(registry: Command.Registry) {
registry.registerChatInputCommand( registry.registerChatInputCommand(
(builder) => (builder) => builder
builder .setName(this.name)
.setName(this.name) .setDescription(this.description)
.setDescription(this.description) .addUserOption((option) => option.setName('user')
.addUserOption((option) => .setDescription('User you want to kill')
option .setRequired(true)),
.setName('user')
.setDescription('User you want to kill')
.setRequired(true),
),
{ {
behaviorWhenNotIdentical: RegisterBehavior.Overwrite, behaviorWhenNotIdentical: RegisterBehavior.Overwrite,
}, },
@@ -51,48 +46,23 @@ export class KillCommand extends Command {
} }
// Command run // Command run
public async chatInputRun(interaction: Command.ChatInputCommandInteraction) { public async chatInputRun(interaction: Command.ChatInputInteraction) {
// Get the users // Get the users
const user = interaction.options.getUser('user', true)!; // TODO exception handling
const sender = interaction.member; const user = interaction.options.getUser('user')!;
const killer = interaction.member!.user;
// Type checks const killerGuildMember = interaction.guild!.members.cache.get(killer.id)!;
if (!(sender instanceof GuildMember)) {
await interaction.reply({
ephemeral: true,
content: 'Failed to fetch your user on the bot!',
});
return;
}
if (user.id === sender.id) {
await interaction.reply('You changed your mind');
return;
}
await addFunLog(sender.id, 'kill', user.id);
const count = await countTotal(sender.id, 'kill', user.id);
let embedFooter: string;
if (count === 1) {
embedFooter = `${sender.displayName} killed you for the first time!`;
} else {
embedFooter = `${sender.displayName} killed you ${count} times!`;
}
// Creates the embed for the kill // Creates the embed for the kill
const randomKill = Kill[Math.floor(Math.random() * Kill.length)]; const randomKill = Kill[Math.floor(Math.random() * Kill.length)];
const killEmbed = new EmbedBuilder() const killEmbed = new MessageEmbed()
.setColor('#ff0000') .setColor('#ff0000')
.setTitle(`Kill from ${sender.displayName}`) .setTitle(`Kill from ${killerGuildMember.displayName}`)
.setImage(randomKill) .setImage(randomKill);
.setFooter({ text: embedFooter });
// Send the kill // Send the kill
await interaction.reply({ await interaction.reply({ content: `<@${user.id}>`, embeds: [killEmbed], fetchReply: true });
content: `${user}`,
embeds: [killEmbed],
fetchReply: true,
});
} }
} }
export default KillCommand;

View File

@@ -18,32 +18,28 @@
*/ */
import { Command, RegisterBehavior } from '@sapphire/framework'; import { Command, RegisterBehavior } from '@sapphire/framework';
import { EmbedBuilder, GuildMember } from 'discord.js'; import { MessageEmbed } from 'discord.js';
import { Poke } from '#utils/gifs'; import { Poke } from '../../utils/gifs';
import { addFunLog, countTotal } from '#utils/database/fun/fun';
export class PokeCommand extends Command { class PokeCommand extends Command {
public constructor(context: Command.LoaderContext, options: Command.Options) { public constructor(context: Command.Context, options: Command.Options) {
super(context, { super(context, {
...options, ...options,
name: 'poke', name: 'poke',
description: 'Poke a user', description: 'Poke a user',
preconditions: [['CoordinatorOnly', 'PatreonOnly']],
}); });
} }
// Registers that this is a slash command // Registers that this is a slash command
public override registerApplicationCommands(registry: Command.Registry) { public override registerApplicationCommands(registry: Command.Registry) {
registry.registerChatInputCommand( registry.registerChatInputCommand(
(builder) => (builder) => builder
builder .setName(this.name)
.setName(this.name) .setDescription(this.description)
.setDescription(this.description) .addUserOption((option) => option.setName('user')
.addUserOption((option) => .setDescription('User you want to poke')
option .setRequired(true)),
.setName('user')
.setDescription('User you want to poke')
.setRequired(true),
),
{ {
behaviorWhenNotIdentical: RegisterBehavior.Overwrite, behaviorWhenNotIdentical: RegisterBehavior.Overwrite,
}, },
@@ -51,51 +47,23 @@ export class PokeCommand extends Command {
} }
// Command run // Command run
public async chatInputRun(interaction: Command.ChatInputCommandInteraction) { public async chatInputRun(interaction: Command.ChatInputInteraction) {
// Get the users // Get the users
const user = interaction.options.getUser('user', true)!; // TODO exception handling
const sender = interaction.member; const user = interaction.options.getUser('user')!;
const poker = interaction.member!.user;
// Type checks const pokerGuildMember = interaction.guild!.members.cache.get(poker.id)!;
if (!(sender instanceof GuildMember)) {
await interaction.reply({
ephemeral: true,
content: 'Failed to fetch your user on the bot!',
});
return;
}
await addFunLog(sender.id, 'poke', user.id);
const count = await countTotal(sender.id, 'poke', user.id);
let embedFooter: string;
if (sender.id === user.id) {
if (count === 1) {
embedFooter = `You poked yourself for the first time!`;
} else {
embedFooter = `You poked yourself ${count} times!`;
}
} else {
if (count === 1) {
embedFooter = `${sender.displayName} poked you for the first time!`;
} else {
embedFooter = `${sender.displayName} poked you ${count} times!`;
}
}
// Creates the embed for the poke // Creates the embed for the poke
const randomPoke = Poke[Math.floor(Math.random() * Poke.length)]; const randomPoke = Poke[Math.floor(Math.random() * Poke.length)];
const pokeEmbed = new EmbedBuilder() const pokeEmbed = new MessageEmbed()
.setColor('#0099ff') .setColor('#0099ff')
.setTitle(`Poke from ${sender.displayName}`) .setTitle(`Poke from ${pokerGuildMember.displayName}`)
.setImage(randomPoke) .setImage(randomPoke);
.setFooter({ text: embedFooter });
// Send the poke // Send the poke
await interaction.reply({ await interaction.reply({ content: `<@${user.id}>`, embeds: [pokeEmbed], fetchReply: true });
content: `${user}`,
embeds: [pokeEmbed],
fetchReply: true,
});
} }
} }
export default PokeCommand;

View File

@@ -18,22 +18,25 @@
*/ */
import { Command, RegisterBehavior } from '@sapphire/framework'; import { Command, RegisterBehavior } from '@sapphire/framework';
import { EmbedBuilder, GuildMember } from 'discord.js'; import { MessageEmbed } from 'discord.js';
import { Sad } from '#utils/gifs'; import { Sad } from '../../utils/gifs';
export class SadCommand extends Command { class SadCommand extends Command {
public constructor(context: Command.LoaderContext, options: Command.Options) { public constructor(context: Command.Context, options: Command.Options) {
super(context, { super(context, {
...options, ...options,
name: 'sad', name: 'sad',
description: 'Express your sadness', description: 'Express your sadness',
preconditions: [['CoordinatorOnly', 'PatreonOnly']],
}); });
} }
// Registers that this is a slash command // Registers that this is a slash command
public override registerApplicationCommands(registry: Command.Registry) { public override registerApplicationCommands(registry: Command.Registry) {
registry.registerChatInputCommand( registry.registerChatInputCommand(
(builder) => builder.setName(this.name).setDescription(this.description), (builder) => builder
.setName(this.name)
.setDescription(this.description),
{ {
behaviorWhenNotIdentical: RegisterBehavior.Overwrite, behaviorWhenNotIdentical: RegisterBehavior.Overwrite,
}, },
@@ -41,27 +44,22 @@ export class SadCommand extends Command {
} }
// Command run // Command run
public async chatInputRun(interaction: Command.ChatInputCommandInteraction) { public async chatInputRun(interaction: Command.ChatInputInteraction) {
// Get the user // Get the user
const { member } = interaction; // TODO exception handling
const member = interaction.member!.user;
// Type checks const memberGuildMember = interaction.guild!.members.cache.get(member.id)!;
if (!(member instanceof GuildMember)) {
await interaction.reply({
ephemeral: true,
content: 'Failed to fetch your user on the bot!',
});
return;
}
// Creates the embed for the sad reaction // Creates the embed for the sad reaction
const randomSad = Sad[Math.floor(Math.random() * Sad.length)]; const randomSad = Sad[Math.floor(Math.random() * Sad.length)];
const sadEmbed = new EmbedBuilder() const sadEmbed = new MessageEmbed()
.setColor('#001148') .setColor('#001148')
.setTitle(`${member.displayName} is sad...`) .setTitle(`${memberGuildMember.displayName} is sad...`)
.setImage(randomSad); .setImage(randomSad);
// Send the embed // Send the embed
await interaction.reply({ embeds: [sadEmbed], fetchReply: true }); await interaction.reply({ embeds: [sadEmbed], fetchReply: true });
} }
} }
export default SadCommand;

View File

@@ -18,22 +18,25 @@
*/ */
import { Command, RegisterBehavior } from '@sapphire/framework'; import { Command, RegisterBehavior } from '@sapphire/framework';
import { EmbedBuilder, GuildMember } from 'discord.js'; import { MessageEmbed } from 'discord.js';
import { Shrug } from '#utils/gifs'; import { Shrug } from '../../utils/gifs';
export class ShrugCommand extends Command { class ShrugCommand extends Command {
public constructor(context: Command.LoaderContext, options: Command.Options) { public constructor(context: Command.Context, options: Command.Options) {
super(context, { super(context, {
...options, ...options,
name: 'shrug', name: 'shrug',
description: 'Ugh... whatever... idk...', description: 'Ugh... whatever... idk...',
preconditions: [['CoordinatorOnly', 'PatreonOnly']],
}); });
} }
// Registers that this is a slash command // Registers that this is a slash command
public override registerApplicationCommands(registry: Command.Registry) { public override registerApplicationCommands(registry: Command.Registry) {
registry.registerChatInputCommand( registry.registerChatInputCommand(
(builder) => builder.setName(this.name).setDescription(this.description), (builder) => builder
.setName(this.name)
.setDescription(this.description),
{ {
behaviorWhenNotIdentical: RegisterBehavior.Overwrite, behaviorWhenNotIdentical: RegisterBehavior.Overwrite,
}, },
@@ -41,27 +44,22 @@ export class ShrugCommand extends Command {
} }
// Command run // Command run
public async chatInputRun(interaction: Command.ChatInputCommandInteraction) { public async chatInputRun(interaction: Command.ChatInputInteraction) {
// Get the user // Get the user
const { member } = interaction; // TODO exception handling
const member = interaction.member!.user;
// Type checks const memberGuildMember = interaction.guild!.members.cache.get(member.id)!;
if (!(member instanceof GuildMember)) {
await interaction.reply({
ephemeral: true,
content: 'Failed to fetch your user on the bot!',
});
return;
}
// Creates the embed for the shrug reaction // Creates the embed for the shrug reaction
const randomShrug = Shrug[Math.floor(Math.random() * Shrug.length)]; const randomShrug = Shrug[Math.floor(Math.random() * Shrug.length)];
const shrugEmbed = new EmbedBuilder() const shrugEmbed = new MessageEmbed()
.setColor('#001980') .setColor('#001980')
.setTitle(`${member.displayName} shrugs`) .setTitle(`${memberGuildMember.displayName} shrugs`)
.setImage(randomShrug); .setImage(randomShrug);
// Send the embed // Send the embed
await interaction.reply({ embeds: [shrugEmbed], fetchReply: true }); await interaction.reply({ embeds: [shrugEmbed], fetchReply: true });
} }
} }
export default ShrugCommand;

View File

@@ -1,242 +0,0 @@
// SPDX-License-Identifier: GPL-3.0-or-later
/*
Animal Rights Advocates Discord Bot
Copyright (C) 2023 Anthony Berg
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { Args, Command, RegisterBehavior } from '@sapphire/framework';
import type { User, Message, Snowflake, TextChannel, Guild } from 'discord.js';
import { EmbedBuilder } from 'discord.js';
import IDs from '#utils/ids';
import { addBan, checkBan } from '#utils/database/moderation/ban';
import { addEmptyUser, updateUser } from '#utils/database/dbExistingUser';
import {
checkTempBan,
removeTempBan,
} from '#utils/database/moderation/tempBan';
export class BanCommand extends Command {
public constructor(context: Command.LoaderContext, options: Command.Options) {
super(context, {
...options,
name: 'ban',
description: 'Bans a user',
preconditions: ['RestrictedAccessOnly'],
});
}
// Registers that this is a slash command
public override registerApplicationCommands(registry: Command.Registry) {
registry.registerChatInputCommand(
(builder) =>
builder
.setName(this.name)
.setDescription(this.description)
.addUserOption((option) =>
option
.setName('user')
.setDescription('User to ban')
.setRequired(true),
)
.addStringOption((option) =>
option
.setName('reason')
.setDescription('Note about the user')
.setRequired(true),
),
{
behaviorWhenNotIdentical: RegisterBehavior.Overwrite,
},
);
}
// Command run
public async chatInputRun(interaction: Command.ChatInputCommandInteraction) {
// Get the arguments
const user = interaction.options.getUser('user', true);
const reason = interaction.options.getString('reason', true);
const mod = interaction.user;
const { guild } = interaction;
// Checks if all the variables are of the right type
if (guild === null) {
await interaction.reply({
content: 'Error fetching guild!',
ephemeral: true,
fetchReply: true,
});
return;
}
await interaction.deferReply({ ephemeral: true });
const ban = await this.ban(user.id, mod.id, reason, guild);
await interaction.editReply({ content: ban.message });
}
// Non Application Command method of banning a user
public async messageRun(message: Message, args: Args) {
// Get arguments
let user: User;
try {
user = await args.pick('user');
} catch {
await message.react('❌');
await message.reply('User was not provided!');
return;
}
const reason = args.finished ? null : await args.rest('string');
const mod = message.author;
if (reason === null) {
await message.react('❌');
await message.reply('Ban reason was not provided!');
return;
}
const { guild } = message;
if (guild === null) {
await message.react('❌');
await message.reply('Guild not found! Try again or contact a developer!');
return;
}
if (message.channel.id !== IDs.channels.restricted.moderators) {
await message.react('❌');
await message.reply(
`You can only run this command in <#${IDs.channels.restricted.moderators}> ` +
'or alternatively use the slash command!',
);
return;
}
const ban = await this.ban(user.id, mod.id, reason, guild);
await message.reply(ban.message);
await message.react(ban.success ? '✅' : '❌');
}
private async ban(
userId: Snowflake,
modId: Snowflake,
reason: string,
guild: Guild,
) {
const info = {
message: '',
success: false,
};
let user = guild.client.users.cache.get(userId);
if (user === undefined) {
user = (await guild.client.users.fetch(userId)) as User;
}
// Gets mod's GuildMember
const mod = guild.members.cache.get(modId);
// Checks if guildMember is null
if (mod === undefined) {
info.message = 'Error fetching mod!';
return info;
}
if (await checkBan(userId)) {
info.message = `${user} is already banned!`;
return info;
}
// Check if mod is in database
await updateUser(mod);
// Gets guildMember
let member = guild.members.cache.get(userId);
if (member === undefined) {
member = await guild.members.fetch(userId).catch(() => undefined);
}
if (member !== undefined) {
// Checks if the user is not restricted
if (member.roles.cache.has(IDs.roles.vegan.vegan)) {
info.message = 'You need to restrict the user first!';
return info;
}
await updateUser(member);
// Send DM for reason of ban
await member
.send(
`You have been banned from ARA for: ${reason}` +
'\n\nhttps://vbcamp.org/ARA',
)
.catch(() => {});
// Ban the user
await member.ban({ reason });
} else {
await addEmptyUser(userId);
}
// Add ban to database
await addBan(userId, modId, reason);
if (await checkTempBan(userId)) {
await removeTempBan(userId);
}
info.message = `${user} has been banned.`;
info.success = true;
// Log the ban
let logChannel = guild.channels.cache.get(IDs.channels.logs.restricted) as
| TextChannel
| undefined;
if (logChannel === undefined) {
logChannel = (await guild.channels.fetch(
IDs.channels.logs.restricted,
)) as TextChannel | undefined;
if (logChannel === undefined) {
this.container.logger.error('Ban Error: Could not fetch log channel');
info.message = `${user} has been banned. This hasn't been logged in a text channel as log channel could not be found`;
return info;
}
}
const log = new EmbedBuilder()
.setColor('#FF0000')
.setAuthor({
name: `Banned ${user.tag}`,
iconURL: `${user.displayAvatarURL()}`,
})
.addFields(
{ name: 'User', value: `${user}`, inline: true },
{ name: 'Moderator', value: `${mod}`, inline: true },
{ name: 'Reason', value: reason },
)
.setTimestamp()
.setFooter({ text: `ID: ${user.id}` });
await logChannel.send({ embeds: [log] });
return info;
}
}

View File

@@ -1,314 +0,0 @@
// SPDX-License-Identifier: GPL-3.0-or-later
/*
Animal Rights Advocates Discord Bot
Copyright (C) 2023 Anthony Berg
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { Args, Command, RegisterBehavior } from '@sapphire/framework';
import { Duration, DurationFormatter } from '@sapphire/time-utilities';
import type { User, Snowflake, TextChannel, Guild } from 'discord.js';
import { EmbedBuilder, Message } from 'discord.js';
import IDs from '#utils/ids';
import { addTempBan, checkTempBan } from '#utils/database/moderation/tempBan';
import { addEmptyUser, updateUser } from '#utils/database/dbExistingUser';
export class TempBanCommand extends Command {
public constructor(context: Command.LoaderContext, options: Command.Options) {
super(context, {
...options,
name: 'tempban',
description: 'Bans a user for a certain amount of time',
preconditions: ['RestrictedAccessOnly'],
});
}
// Registers that this is a slash command
public override registerApplicationCommands(registry: Command.Registry) {
registry.registerChatInputCommand(
(builder) =>
builder
.setName(this.name)
.setDescription(this.description)
.addUserOption((option) =>
option
.setName('user')
.setDescription('User to ban')
.setRequired(true),
)
.addStringOption((option) =>
option
.setName('duration')
.setDescription('How long to ban the user for')
.setRequired(true),
)
.addStringOption((option) =>
option
.setName('reason')
.setDescription('Note about the user')
.setRequired(true),
),
{
behaviorWhenNotIdentical: RegisterBehavior.Overwrite,
},
);
}
// Command run
public async chatInputRun(interaction: Command.ChatInputCommandInteraction) {
// Get the arguments
const user = interaction.options.getUser('user', true);
const duration = interaction.options.getString('duration', true);
const reason = interaction.options.getString('reason', true);
const mod = interaction.user;
const { guild } = interaction;
// Checks if all the variables are of the right type
if (guild === null) {
await interaction.reply({
content: 'Error fetching guild!',
ephemeral: true,
fetchReply: true,
});
return;
}
const time = new Duration(duration);
if (Number.isNaN(time.offset)) {
await interaction.reply({
content: 'Invalid ban duration input',
});
return;
}
await interaction.deferReply({ ephemeral: true });
const ban = await this.ban(user.id, mod.id, time, reason, guild);
await interaction.editReply({ content: ban.message });
}
// Non Application Command method of banning a user
public async messageRun(message: Message, args: Args) {
// Get arguments
let user: User;
try {
user = await args.pick('user');
} catch {
await message.react('❌');
await message.reply('User was not provided!');
return;
}
const arg = args.finished ? null : await args.rest('string');
const mod = message.author;
if (arg === null) {
await message.react('❌');
await message.reply('Ban reason was not provided!');
return;
}
const { duration, reason } = this.findTimeAndReason(arg);
if (Number.isNaN(duration.offset)) {
await message.react('❌');
await message.reply('Invalid time length for ban!');
return;
}
if (reason.length === 0) {
await message.react('❌');
await message.reply('Reason was not provided!');
return;
}
const { guild } = message;
if (guild === null) {
await message.react('❌');
await message.reply('Guild not found! Try again or contact a developer!');
return;
}
if (message.channel.id !== IDs.channels.restricted.moderators) {
await message.react('❌');
await message.reply(
`You can only run this command in <#${IDs.channels.restricted.moderators}> ` +
'or alternatively use the slash command!',
);
return;
}
const ban = await this.ban(user.id, mod.id, duration, reason, guild);
await message.reply(ban.message);
await message.react(ban.success ? '✅' : '❌');
}
private findTimeAndReason(args: string) {
const info = {
duration: new Duration(''),
reason: '',
};
if (Number.isNaN(new Duration(args).offset)) {
return info;
}
const spliced = args.split(' ');
let time = '';
for (let i = 0; i < spliced.length; i += 1) {
if (!Number.isNaN(new Duration(spliced[i]).offset)) {
time += spliced[i];
} else {
info.reason = args.slice(args.indexOf(spliced[i]));
break;
}
}
if (time.length === 0) {
return info;
}
info.duration = new Duration(time);
return info;
}
private async ban(
userId: Snowflake,
modId: Snowflake,
time: Duration,
reason: string,
guild: Guild,
) {
const info = {
message: '',
success: false,
};
const banLength = new DurationFormatter().format(time.offset);
let user = guild.client.users.cache.get(userId);
if (user === undefined) {
user = (await guild.client.users.fetch(userId)) as User;
}
// Gets mod's GuildMember
const mod = guild.members.cache.get(modId);
// Checks if guildMember is null
if (mod === undefined) {
info.message = 'Error fetching mod!';
return info;
}
if (await checkTempBan(userId)) {
info.message = `${user} is already temp banned!`;
return info;
}
// Check if mod is in database
await updateUser(mod);
// Gets guildMember
let member = guild.members.cache.get(userId);
if (member === undefined) {
member = await guild.members.fetch(userId).catch(() => undefined);
}
if (member !== undefined) {
// Checks if the user is not restricted
if (member.roles.cache.has(IDs.roles.vegan.vegan)) {
info.message = 'You need to restrict the user first!';
return info;
}
await updateUser(member);
// Send DM for reason of ban
await member
.send(
`You have been temporarily banned from ARA for ${banLength}. Reason: ${reason}` +
'\n\nhttps://vbcamp.org/ARA',
)
.catch(() => {});
// Ban the user
await member.ban({ reason });
} else {
await addEmptyUser(userId);
}
// Add ban to database
await addTempBan(userId, modId, time.fromNow, reason);
// Create scheduled task to unban
await this.container.tasks.create(
{
name: 'tempBan',
payload: {
userId: user.id,
guildId: guild.id,
},
},
time.offset,
);
info.message = `${user} has been temporarily banned for ${banLength}.`;
info.success = true;
// Log the ban
let logChannel = guild.channels.cache.get(IDs.channels.logs.restricted) as
| TextChannel
| undefined;
if (logChannel === undefined) {
logChannel = (await guild.channels.fetch(
IDs.channels.logs.restricted,
)) as TextChannel | undefined;
if (logChannel === undefined) {
this.container.logger.error(
'Temp Ban Error: Could not fetch log channel',
);
info.message =
`${user} has been temporarily banned for ${banLength}. ` +
"This hasn't been logged in a text channel as log channel could not be found";
return info;
}
}
const log = new EmbedBuilder()
.setColor('#FF0000')
.setAuthor({
name: `Temp Banned ${user.tag}`,
iconURL: `${user.displayAvatarURL()}`,
})
.addFields(
{ name: 'User', value: `${user}`, inline: true },
{ name: 'Moderator', value: `${mod}`, inline: true },
{ name: 'Duration', value: banLength },
{ name: 'Reason', value: reason },
)
.setTimestamp()
.setFooter({ text: `ID: ${user.id}` });
await logChannel.send({ embeds: [log] });
return info;
}
}

View File

@@ -1,226 +0,0 @@
// SPDX-License-Identifier: GPL-3.0-or-later
/*
Animal Rights Advocates Discord Bot
Copyright (C) 2023 Anthony Berg
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { Args, Command, RegisterBehavior } from '@sapphire/framework';
import type {
User,
Message,
Snowflake,
TextChannel,
Guild,
GuildBan,
} from 'discord.js';
import { EmbedBuilder } from 'discord.js';
import IDs from '#utils/ids';
import { removeBan, checkBan, addBan } from '#utils/database/moderation/ban';
import {
checkTempBan,
removeTempBan,
} from '#utils/database/moderation/tempBan';
import { addEmptyUser, addExistingUser } from '#utils/database/dbExistingUser';
export class UnbanCommand extends Command {
public constructor(context: Command.LoaderContext, options: Command.Options) {
super(context, {
...options,
name: 'unban',
description: 'Unbans a user',
preconditions: ['RestrictedAccessOnly'],
});
}
// Registers that this is a slash command
public override registerApplicationCommands(registry: Command.Registry) {
registry.registerChatInputCommand(
(builder) =>
builder
.setName(this.name)
.setDescription(this.description)
.addUserOption((option) =>
option
.setName('user')
.setDescription('User to unban')
.setRequired(true),
),
{
behaviorWhenNotIdentical: RegisterBehavior.Overwrite,
},
);
}
// Command run
public async chatInputRun(interaction: Command.ChatInputCommandInteraction) {
// Get the arguments
const user = interaction.options.getUser('user', true);
const mod = interaction.user;
const { guild } = interaction;
// Checks if all the variables are of the right type
if (guild === null) {
await interaction.reply({
content: 'Error fetching guild!',
ephemeral: true,
fetchReply: true,
});
return;
}
await interaction.deferReply();
const unban = await this.unban(user.id, mod.id, guild);
await interaction.editReply({ content: unban.message });
}
// Non Application Command method of banning a user
public async messageRun(message: Message, args: Args) {
// Get arguments
let user: User;
try {
user = await args.pick('user');
} catch {
await message.react('❌');
await message.reply('User was not provided!');
return;
}
const mod = message.author;
const { guild } = message;
if (guild === null) {
await message.react('❌');
await message.reply('Guild not found! Try again or contact a developer!');
return;
}
const unban = await this.unban(user.id, mod.id, guild);
await message.reply(unban.message);
await message.react(unban.success ? '✅' : '❌');
}
private async unban(userId: Snowflake, modId: Snowflake, guild: Guild) {
const info = {
message: '',
success: false,
};
// Gets mod's GuildMember
const mod = guild.members.cache.get(modId);
// Checks if guildMember is null
if (mod === undefined) {
info.message = 'Error fetching mod!';
return info;
}
// Check if mod is in database
await addExistingUser(mod);
let user = guild.client.users.cache.get(userId);
if (user === undefined) {
user = await guild.client.users.fetch(userId);
if (user === undefined) {
info.message = 'Could not fetch the user!';
return info;
}
}
let dbBan = await checkBan(userId);
const dbTempBan = await checkTempBan(userId);
if (!dbBan && !dbTempBan) {
let ban: GuildBan;
try {
ban = await guild.bans.fetch(userId);
} catch {
try {
ban = await guild.bans.fetch({ user, force: true });
} catch {
info.message = `${user} is not banned.`;
return info;
}
}
let { reason } = ban;
if (reason === null || reason === undefined) {
reason = '';
}
// Check if user and mod are on the database
await addEmptyUser(user.id);
// Add missing ban
await addBan(
userId,
modId,
`(Mod who banned is not accurate) - ${reason}`,
);
dbBan = true;
}
// Unban the user
await guild.members.unban(user).catch(() => {});
if (dbBan) {
// Add unban to database
await removeBan(user.id, mod.user.id);
} else if (dbTempBan) {
await removeTempBan(user.id, mod.user.id);
}
info.message = `${user} has been unbanned.`;
info.success = true;
// Log unban
let logChannel = guild.channels.cache.get(IDs.channels.logs.restricted) as
| TextChannel
| undefined;
if (logChannel === undefined) {
logChannel = (await guild.channels.fetch(
IDs.channels.logs.restricted,
)) as TextChannel | undefined;
if (logChannel === undefined) {
this.container.logger.error('Ban Error: Could not fetch log channel');
info.message = `${user} has been banned. This hasn't been logged in a text channel as log channel could not be found`;
return info;
}
}
const log = new EmbedBuilder()
.setColor('#28A745')
.setAuthor({
name: `Unbanned ${user.tag}`,
iconURL: `${user.displayAvatarURL()}`,
})
.addFields(
{ name: 'User', value: `${user}`, inline: true },
{ name: 'Moderator', value: `${mod}`, inline: true },
)
.setTimestamp()
.setFooter({ text: `ID: ${user.id}` });
await logChannel.send({ embeds: [log] });
return info;
}
}

View File

@@ -1,288 +0,0 @@
// SPDX-License-Identifier: GPL-3.0-or-later
/*
Animal Rights Advocates Discord Bot
Copyright (C) 2022 Anthony Berg
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
// TODO This file needs a MASSIVE refactor
import { Args, container, RegisterBehavior } from '@sapphire/framework';
import { Subcommand } from '@sapphire/plugin-subcommands';
import {
ChannelType,
GuildMember,
Message,
PermissionsBitField,
} from 'discord.js';
import type { TextChannel, Snowflake } from 'discord.js';
import IDs from '#utils/ids';
export class DiversityCommand extends Subcommand {
public constructor(
context: Subcommand.LoaderContext,
options: Subcommand.Options,
) {
super(context, {
...options,
name: 'diversity',
aliases: ['di', 'div'],
subcommands: [
{
name: 'role',
default: true,
chatInputRun: 'roleCommand',
messageRun: 'roleMessage',
},
{
name: 'toggleopen',
chatInputRun: 'toggleOpen',
},
],
description: 'Commands for the Diversity Coordinators',
preconditions: ['DiversityCoordinatorOnly'],
});
}
// Registers that this is a slash command
public override registerApplicationCommands(registry: Subcommand.Registry) {
registry.registerChatInputCommand(
(builder) =>
builder
.setName(this.name)
.setDescription(this.description)
.addSubcommand((command) =>
command
.setName('role')
.setDescription('Gives/removes the diversity role')
.addUserOption((option) =>
option
.setName('user')
.setDescription('User to give/remove diversity to')
.setRequired(true),
),
)
.addSubcommand((command) =>
command
.setName('toggleopen')
.setDescription(
'Toggles read-only for vegans in diversity section',
),
),
{
behaviorWhenNotIdentical: RegisterBehavior.Overwrite,
},
);
}
// Command run
public async toggleOpen(interaction: Subcommand.ChatInputCommandInteraction) {
// Check if guild is not null
if (interaction.guild === null) {
await interaction.reply({
content: 'Guild not found!',
ephemeral: true,
fetchReply: true,
});
return;
}
// Get the channel
const channel = interaction.guild.channels.cache.get(interaction.channelId);
// Check if channel is not undefined
if (channel === undefined) {
await interaction.reply({
content: 'Channel not found!',
ephemeral: true,
fetchReply: true,
});
return;
}
// Check if channel is text
if (channel.type !== ChannelType.GuildText) {
await interaction.reply({
content: 'Channel is not a text channel!',
ephemeral: true,
fetchReply: true,
});
return;
}
// Converts GuildBasedChannel to TextChannel
const channelText = channel as TextChannel;
// Check if the command was run in the diversity section
if (channel.parentId !== IDs.categories.diversity) {
await interaction.reply({
content: 'Command was not run in the Diversity section!',
ephemeral: true,
fetchReply: true,
});
return;
}
// Checks if the channel is open
const open = channel
.permissionsFor(IDs.roles.vegan.vegan)!
.has([PermissionsBitField.Flags.SendMessages]);
// Toggle send message in channel
await channelText.permissionOverwrites.edit(IDs.roles.vegan.vegan, {
SendMessages: !open,
});
await interaction.reply({
content: `${!open ? 'Opened' : 'Closed'} this channel.`,
fetchReply: true,
});
}
public async roleCommand(
interaction: Subcommand.ChatInputCommandInteraction,
) {
// TODO add database updates
// Get the arguments
const user = interaction.options.getUser('user');
const mod = interaction.member;
const { guild } = interaction;
// Checks if all the variables are of the right type
if (user === null || guild === null || mod === null) {
await interaction.reply({
content: 'Error fetching user!',
ephemeral: true,
fetchReply: true,
});
return;
}
// Gets guildMember whilst removing the ability of each other variables being null
const guildMember = guild.members.cache.get(user.id);
const diversity = guild.roles.cache.get(IDs.roles.staff.diversity);
// Checks if guildMember is null
if (guildMember === undefined || diversity === undefined) {
await interaction.reply({
content: 'Error fetching user!',
ephemeral: true,
fetchReply: true,
});
return;
}
// Checks if the user has Diversity and to give them or remove them based on if they have it
if (guildMember.roles.cache.has(IDs.roles.staff.diversity)) {
// Remove the Diversity role from the user
await guildMember.roles.remove(diversity);
await this.threadManager(guildMember.id, false);
await interaction.reply({
content: `Removed the ${diversity.name} role from ${user}`,
fetchReply: true,
});
return;
}
// Add Diversity Team role to the user
await guildMember.roles.add(diversity);
await this.threadManager(guildMember.id, true);
await interaction.reply({
content: `Gave ${user} the ${diversity.name} role!`,
fetchReply: true,
});
await user
.send(`You have been given the ${diversity.name} role by ${mod}!`)
.catch(() => {});
}
public async roleMessage(message: Message, args: Args) {
// Get arguments
let user: GuildMember;
try {
user = await args.pick('member');
} catch {
await message.react('❌');
await message.reply('User was not provided!');
return;
}
const mod = message.member;
if (mod === null) {
await message.react('❌');
await message.reply(
'Diversity coordinator not found! Try again or contact a developer!',
);
return;
}
const { guild } = message;
if (guild === null) {
await message.react('❌');
await message.reply('Guild not found! Try again or contact a developer!');
return;
}
const diversity = guild.roles.cache.get(IDs.roles.staff.diversity);
if (diversity === undefined) {
await message.react('❌');
await message.reply('Role not found! Try again or contact a developer!');
return;
}
// Checks if the user has Diversity and to give them or remove them based on if they have it
if (user.roles.cache.has(IDs.roles.staff.diversity)) {
// Remove the Diversity Team role from the user
await user.roles.remove(diversity);
await this.threadManager(user.id, false);
await message.reply({
content: `Removed the ${diversity.name} role from ${user}`,
});
} else {
// Give Diversity Team role to the user
await user.roles.add(diversity);
await this.threadManager(user.id, true);
await message.reply({
content: `Gave ${user} the ${diversity.name} role!`,
});
await user
.send(`You have been given the ${diversity.name} role by ${mod}!`)
.catch(() => {});
}
await message.react('✅');
}
private async threadManager(member: Snowflake, add: boolean) {
const thread = await container.client.channels.fetch(
IDs.channels.diversity.diversity,
);
if (thread === null) {
return;
}
if (!thread.isThread()) {
return;
}
if (add) {
await thread.members.add(member);
return;
}
await thread.members.remove(member);
}
}

View File

@@ -0,0 +1,129 @@
// SPDX-License-Identifier: GPL-3.0-or-later
/*
Animal Rights Advocates Discord Bot
Copyright (C) 2022 Anthony Berg
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { Command, RegisterBehavior } from '@sapphire/framework';
import type { TextChannel } from 'discord.js';
import IDs from '../../utils/ids';
class ToggleOpenCommand extends Command {
public constructor(context: Command.Context, options: Command.Options) {
super(context, {
...options,
name: 'diversity',
description: 'Commands for the Diversity Coordinators',
preconditions: ['DiversityCoordinatorOnly'],
});
}
// Registers that this is a slash command
public override registerApplicationCommands(registry: Command.Registry) {
registry.registerChatInputCommand(
(builder) => builder
.setName(this.name)
.setDescription(this.description)
.addSubcommand((command) => command.setName('toggleopen')
.setDescription('Toggles read-only for vegans in diversity section')),
{
behaviorWhenNotIdentical: RegisterBehavior.Overwrite,
},
);
}
// Command run
public async chatInputRun(interaction: Command.ChatInputInteraction) {
const subcommand = interaction.options.getSubcommand(true);
// Checks what subcommand was run
switch (subcommand) {
case 'toggleopen': {
await this.toggleOpen(interaction);
return;
}
default: {
// If subcommand is invalid
await interaction.reply({
content: 'Invalid sub command!',
ephemeral: true,
fetchReply: true,
});
}
}
}
// Command run
public async toggleOpen(interaction: Command.ChatInputInteraction) {
// Check if guild is not null
if (interaction.guild === null) {
await interaction.reply({
content: 'Guild not found!',
ephemeral: true,
fetchReply: true,
});
return;
}
// Get the channel
const channel = interaction.guild.channels.cache.get(interaction.channelId);
// Check if channel is not undefined
if (channel === undefined) {
await interaction.reply({
content: 'Channel not found!',
ephemeral: true,
fetchReply: true,
});
return;
}
// Check if channel is text
if (!channel.isText()) {
await interaction.reply({
content: 'Channel is not a text channel!',
ephemeral: true,
fetchReply: true,
});
return;
}
// Converts GuildBasedChannel to TextChannel
const channelText = channel as TextChannel;
// Check if the command was run in the diversity section
if (channel.parentId !== IDs.categories.diversity) {
await interaction.reply({
content: 'Command was not run in the Diversity section!',
ephemeral: true,
fetchReply: true,
});
return;
}
// Checks if the channel is open
const open = channel.permissionsFor(IDs.roles.vegan.vegan)!.has(['SEND_MESSAGES']);
// Toggle send message in channel
await channelText.permissionOverwrites.edit(IDs.roles.vegan.vegan, { SEND_MESSAGES: !open });
await interaction.reply({
content: `${!open ? 'Opened' : 'Closed'} this channel.`,
fetchReply: true,
});
}
}
export default ToggleOpenCommand;

View File

@@ -1,176 +0,0 @@
// SPDX-License-Identifier: GPL-3.0-or-later
/*
Animal Rights Advocates Discord Bot
Copyright (C) 2023 Anthony Berg
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
I created this command on the 13:30 Newcastle - Kings Cross train.
Idk why I wanted to say that, but I felt like it was a cool fact
*/
import { Args, Command, RegisterBehavior } from '@sapphire/framework';
import type { Message } from 'discord.js';
import { ChannelType } from 'discord.js';
export class MoveAllCommand extends Command {
public constructor(context: Command.LoaderContext, options: Command.Options) {
super(context, {
...options,
name: 'moveall',
aliases: ['mvall'],
description: 'Moves everyone from one voice channel to the specified one',
preconditions: [['CoordinatorOnly', 'ModOnly']],
});
}
// Registers that this is a slash command
public override registerApplicationCommands(registry: Command.Registry) {
registry.registerChatInputCommand(
(builder) =>
builder
.setName(this.name)
.setDescription(this.description)
.addChannelOption((option) =>
option
.setName('channel')
.setDescription('The channel to move everyone to')
.setRequired(true),
),
{
behaviorWhenNotIdentical: RegisterBehavior.Overwrite,
},
);
}
// Command run
public async chatInputRun(interaction: Command.ChatInputCommandInteraction) {
// Get the arguments
const channel = interaction.options.getChannel('channel', true);
const { member } = interaction;
const { guild } = interaction;
await interaction.deferReply({ ephemeral: true });
if (
channel.type !== ChannelType.GuildVoice &&
channel.type !== ChannelType.GuildStageVoice
) {
await interaction.editReply({
content: 'The channel you provided is not a voice channel!',
});
return;
}
// Checks if all the variables are of the right type
if (guild === null) {
await interaction.editReply({
content: 'Error fetching guild!',
});
return;
}
if (member === null) {
await interaction.editReply({
content: 'Error fetching your user',
});
return;
}
const mod = guild.members.cache.get(member.user.id);
if (mod === undefined) {
await interaction.editReply({
content: 'Error fetching user from guild',
});
return;
}
if (mod.voice.channelId === null) {
await interaction.editReply({
content: 'You need to be in a voice channel to run this command!',
});
return;
}
const voice = guild.channels.cache.get(mod.voice.channelId);
if (voice === undefined || !voice.isVoiceBased()) {
await interaction.editReply({
content: 'Error fetching your current voice channel!',
});
return;
}
voice.members.forEach((memberVC) => {
memberVC.voice.setChannel(channel.id);
});
await interaction.editReply({
content: `Successfully moved ${voice.members.size} members to <#${channel.id}>!`,
});
}
public async messageRun(message: Message, args: Args) {
// Get arguments
const channel = await args.pick('channel');
if (!channel.isVoiceBased()) {
await message.react('❌');
await message.reply('You did not provide a voice based channel!');
return;
}
const mod = message.member;
const { guild } = message;
if (mod === null) {
await message.react('❌');
await message.reply('Could not find your user!');
return;
}
if (mod.voice.channelId === null) {
await message.react('❌');
await message.reply(
'You need to be in a voice channel to run this command!',
);
return;
}
if (guild === null) {
await message.react('❌');
await message.reply('Could not find guild!');
return;
}
const voice = guild.channels.cache.get(mod.voice.channelId);
if (voice === undefined || !voice.isVoiceBased()) {
await message.react('❌');
await message.reply('Could not fetch current voice channel!');
return;
}
voice.members.forEach((member) => {
member.voice.setChannel(channel.id);
});
await message.reply(
`Successfully moved ${voice.members.size} members to <#${channel.id}>!`,
);
await message.react('✅');
}
}

View File

@@ -1,139 +0,0 @@
// SPDX-License-Identifier: GPL-3.0-or-later
/*
Animal Rights Advocates Discord Bot
Copyright (C) 2023 Anthony Berg
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { Args, Command, RegisterBehavior } from '@sapphire/framework';
import type { GuildMember, Message } from 'discord.js';
export class RenameUserCommand extends Command {
public constructor(context: Command.LoaderContext, options: Command.Options) {
super(context, {
...options,
name: 'rename',
aliases: ['ru', 'nick'],
description: 'Changes the nickname for the user',
preconditions: [['CoordinatorOnly', 'ModOnly']],
});
}
// Registers that this is a slash command
public override registerApplicationCommands(registry: Command.Registry) {
registry.registerChatInputCommand(
(builder) =>
builder
.setName(this.name)
.setDescription(this.description)
.addUserOption((option) =>
option
.setName('user')
.setDescription('User to change nickname')
.setRequired(true),
)
.addStringOption((option) =>
option
.setName('nickname')
.setDescription('The nickname to give the user')
.setMaxLength(32),
),
{
behaviorWhenNotIdentical: RegisterBehavior.Overwrite,
},
);
}
// Command run
public async chatInputRun(interaction: Command.ChatInputCommandInteraction) {
// TODO add database updates
// Get the arguments
const user = interaction.options.getUser('user', true);
const nickname = interaction.options.getString('nickname');
const { guild } = interaction;
// Checks if all the variables are of the right type
if (guild === null) {
await interaction.reply({
content: 'Error fetching guild!',
ephemeral: true,
fetchReply: true,
});
return;
}
// Gets guildMember whilst removing the ability of each other variables being null
const member = guild.members.cache.get(user.id);
// Checks if guildMember is null
if (member === undefined) {
await interaction.reply({
content: 'Error fetching user!',
ephemeral: true,
fetchReply: true,
});
return;
}
// Change nickname
try {
await member.setNickname(nickname);
} catch {
await interaction.reply({
content: "Bot doesn't have permission to change the user's name!",
ephemeral: true,
fetchReply: true,
});
return;
}
await interaction.reply({
content: `Changed ${user}'s nickname`,
fetchReply: true,
ephemeral: true,
});
}
public async messageRun(message: Message, args: Args) {
// Get arguments
let member: GuildMember;
try {
member = await args.pick('member');
} catch {
await message.react('❌');
await message.reply('User was not provided!');
return;
}
const nickname = args.finished ? null : await args.rest('string');
if (nickname != null && nickname.length > 32) {
await message.react('❌');
await message.reply('Nickname is too long!');
return;
}
try {
await member.setNickname(nickname);
} catch {
await message.react('❌');
await message.reply(
"Bot doesn't have permission to change the user's name!",
);
return;
}
await message.react('✅');
}
}

View File

@@ -1,375 +0,0 @@
// SPDX-License-Identifier: GPL-3.0-or-later
/*
Animal Rights Advocates Discord Bot
Copyright (C) 2023 Anthony Berg
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import {
Args,
Command,
RegisterBehavior,
container,
} from '@sapphire/framework';
import {
ChannelType,
EmbedBuilder,
PermissionsBitField,
time,
} from 'discord.js';
import type { User, Message, TextChannel, Guild, Snowflake } from 'discord.js';
import IDs from '#utils/ids';
import {
addEmptyUser,
updateUser,
fetchRoles,
} from '#utils/database/dbExistingUser';
import { restrict, checkActive } from '#utils/database/moderation/restriction';
import { randint } from '#utils/maths';
import { blockedRolesAfterRestricted } from '#utils/blockedRoles';
export async function restrictRun(
userId: Snowflake,
modId: Snowflake,
reason: string,
guild: Guild,
tolerance = false,
) {
const info = {
message: '',
success: false,
};
let user = guild.client.users.cache.get(userId);
if (user === undefined) {
user = await guild.client.users.fetch(userId);
if (user === undefined) {
info.message = 'Error fetching user';
return info;
}
}
// Gets mod's GuildMember
const mod = guild.members.cache.get(modId);
// Checks if guildMember is null
if (mod === undefined) {
info.message = 'Error fetching mod';
return info;
}
// Check if mod is in database
await updateUser(mod);
if (await checkActive(userId)) {
info.message = `<@${userId}> is already restricted!`;
return info;
}
// Gets guildMember
let member = guild.members.cache.get(userId);
if (member === undefined) {
member = await guild.members.fetch(userId).catch(() => undefined);
}
const restrictRoles = IDs.roles.restrictions.restricted;
let section = tolerance ? randint(3, 4) : randint(1, 2);
if (member !== undefined) {
// Checks if the user is not restricted
if (member.roles.cache.hasAny(...restrictRoles)) {
info.message = `${member} is already restricted!`;
return info;
}
// Check if user and mod are on the database
await updateUser(member);
if (member.roles.cache.has(IDs.roles.vegan.vegan)) {
section = 5;
}
await member.roles.add(restrictRoles[section - 1]);
if (member.roles.cache.has(IDs.roles.vegan.vegan)) {
const voiceChannel = await guild.channels.create({
name: 'Restricted Voice Channel',
type: ChannelType.GuildVoice,
parent: IDs.categories.restricted,
permissionOverwrites: [
{
id: guild.roles.everyone,
deny: [PermissionsBitField.Flags.ViewChannel],
},
{
id: member.id,
allow: [PermissionsBitField.Flags.ViewChannel],
},
{
id: IDs.roles.staff.restricted,
allow: [
PermissionsBitField.Flags.SendMessages,
PermissionsBitField.Flags.ViewChannel,
PermissionsBitField.Flags.Connect,
PermissionsBitField.Flags.MuteMembers,
],
},
],
});
let restrictedChannel: TextChannel;
let bannedName = false;
try {
restrictedChannel = await guild.channels.create({
name: `⛔┃${member.user.username}-restricted`,
type: ChannelType.GuildText,
topic: `Restricted channel. ${member.id} ${voiceChannel.id} (Please do not change this)`,
parent: IDs.categories.restricted,
permissionOverwrites: [
{
id: guild.roles.everyone,
allow: [PermissionsBitField.Flags.ReadMessageHistory],
deny: [PermissionsBitField.Flags.ViewChannel],
},
{
id: member.id,
allow: [PermissionsBitField.Flags.ViewChannel],
},
{
id: IDs.roles.staff.restricted,
allow: [
PermissionsBitField.Flags.SendMessages,
PermissionsBitField.Flags.ViewChannel,
],
},
],
});
} catch {
restrictedChannel = await guild.channels.create({
name: `⛔┃${member.user.id}-restricted`,
type: ChannelType.GuildText,
topic: `Restricted channel. ${member.id} ${voiceChannel.id} (Please do not change this)`,
parent: IDs.categories.restricted,
permissionOverwrites: [
{
id: guild.roles.everyone,
allow: [PermissionsBitField.Flags.ReadMessageHistory],
deny: [PermissionsBitField.Flags.ViewChannel],
},
{
id: member.id,
allow: [PermissionsBitField.Flags.ViewChannel],
},
{
id: IDs.roles.staff.restricted,
allow: [
PermissionsBitField.Flags.SendMessages,
PermissionsBitField.Flags.ViewChannel,
],
},
],
});
bannedName = true;
}
if (!bannedName) {
await voiceChannel.setName(`${member.user.username}-restricted`);
} else {
await voiceChannel.setName(`${member.user.id}-restricted`);
}
const joinTime = time(member.joinedAt!);
const registerTime = time(member.user.createdAt);
const embed = new EmbedBuilder()
.setColor(member.displayHexColor)
.setTitle(`Restricted channel for ${member.user.username}`)
.setDescription(`${member}`)
.setThumbnail(member.user.displayAvatarURL())
.addFields(
{ name: 'Joined:', value: `${joinTime}`, inline: true },
{ name: 'Created:', value: `${registerTime}`, inline: true },
);
await restrictedChannel.send({ embeds: [embed] });
}
await member.roles.remove(blockedRolesAfterRestricted);
} else {
await addEmptyUser(userId);
const dbRoles = await fetchRoles(userId);
if (dbRoles.includes(IDs.roles.vegan.vegan)) {
section = 5;
}
}
if (member !== undefined && member.voice.channelId !== null) {
await member.voice.disconnect();
}
// Restrict the user on the database
await restrict(userId, modId, reason, section);
info.message = `Restricted ${user}`;
info.success = true;
// DM the reason
const dmEmbed = new EmbedBuilder()
.setColor('#FF6700')
.setAuthor({
name: "You've been restricted!",
iconURL: `${user.displayAvatarURL()}`,
})
.addFields({ name: 'Reason', value: reason })
.setTimestamp();
await user.send({ embeds: [dmEmbed] }).catch(() => {});
// Log the ban
let logChannel = guild.channels.cache.get(IDs.channels.logs.restricted) as
| TextChannel
| undefined;
if (logChannel === undefined) {
logChannel = (await guild.channels.fetch(IDs.channels.logs.restricted)) as
| TextChannel
| undefined;
if (logChannel === undefined) {
container.logger.error('Restrict Error: Could not fetch log channel');
info.message = `Restricted ${user} but could not find the log channel. This has been logged to the database.`;
return info;
}
}
const message = new EmbedBuilder()
.setColor('#FF6700')
.setAuthor({
name: `Restricted ${user.tag}`,
iconURL: `${user.displayAvatarURL()}`,
})
.addFields(
{ name: 'User', value: `${user}`, inline: true },
{ name: 'Moderator', value: `${mod}`, inline: true },
{ name: 'Reason', value: reason },
)
.setTimestamp()
.setFooter({ text: `ID: ${userId}` });
await logChannel.send({ embeds: [message] });
return info;
}
export class RestrictCommand extends Command {
public constructor(context: Command.LoaderContext, options: Command.Options) {
super(context, {
...options,
name: 'restrict',
aliases: ['r', 'rest', 'rr', 'rv'],
description: 'Restricts a user',
preconditions: ['ModOnly'],
});
}
// Registers that this is a slash command
public override registerApplicationCommands(registry: Command.Registry) {
registry.registerChatInputCommand(
(builder) =>
builder
.setName(this.name)
.setDescription(this.description)
.addUserOption((option) =>
option
.setName('user')
.setDescription('User to restrict')
.setRequired(true),
)
.addStringOption((option) =>
option
.setName('reason')
.setDescription('Reason for restricting the user')
.setRequired(true),
),
{
behaviorWhenNotIdentical: RegisterBehavior.Overwrite,
},
);
}
// Command run
public async chatInputRun(interaction: Command.ChatInputCommandInteraction) {
// Get the arguments
const user = interaction.options.getUser('user', true);
const reason = interaction.options.getString('reason', true);
const mod = interaction.user;
const { guild } = interaction;
// Checks if all the variables are of the right type
if (guild === null) {
await interaction.reply({
content: 'Error fetching guild!',
ephemeral: true,
fetchReply: true,
});
return;
}
await interaction.deferReply({ ephemeral: true });
const info = await restrictRun(user?.id, mod.id, reason, guild);
await interaction.editReply({
content: info.message,
});
}
// Non Application Command method of banning a user
public async messageRun(message: Message, args: Args) {
// Get arguments
let user: User;
try {
user = await args.pick('user');
} catch {
await message.react('❌');
await message.reply('User was not provided!');
return;
}
const reason = args.finished ? null : await args.rest('string');
const mod = message.author;
if (reason === null) {
await message.react('❌');
await message.reply('Restrict reason was not provided!');
return;
}
const { guild } = message;
if (guild === null) {
await message.react('❌');
await message.reply('Guild not found! Try again or contact a developer!');
return;
}
const info = await restrictRun(user?.id, mod.id, reason, guild);
await message.reply(info.message);
await message.react(info.success ? '✅' : '❌');
}
}

View File

@@ -1,242 +0,0 @@
// SPDX-License-Identifier: GPL-3.0-or-later
/*
Animal Rights Advocates Discord Bot
Copyright (C) 2023 Anthony Berg
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { Args, Command, RegisterBehavior } from '@sapphire/framework';
import { ChannelType, EmbedBuilder } from 'discord.js';
import type { Message, TextChannel, Guild, Snowflake } from 'discord.js';
import IDs from '#utils/ids';
import { getRestrictions } from '#utils/database/moderation/restriction';
import { checkStaff } from '#utils/checker';
export class RestrictLogsCommand extends Command {
public constructor(context: Command.LoaderContext, options: Command.Options) {
super(context, {
...options,
name: 'restrictlogs',
description: 'Shows restriction history for a user',
preconditions: ['ModOnly'],
});
}
// Registers that this is a slash command
public override registerApplicationCommands(registry: Command.Registry) {
registry.registerChatInputCommand(
(builder) =>
builder
.setName(this.name)
.setDescription(this.description)
.addUserOption((option) =>
option
.setName('user')
.setDescription('User to check restriction logs for'),
),
{
behaviorWhenNotIdentical: RegisterBehavior.Overwrite,
},
);
}
// Command run
public async chatInputRun(interaction: Command.ChatInputCommandInteraction) {
// Get the arguments
const user = interaction.options.getUser('user');
let { channel } = interaction;
const { guild } = interaction;
// Checks if all the variables are of the right type
if (guild === null || channel === null) {
await interaction.reply({
content: 'Error fetching guild or channel!',
ephemeral: true,
fetchReply: true,
});
return;
}
let userId: Snowflake | null = null;
if (user !== undefined && user !== null) {
userId = user.id;
}
const staffChannel = checkStaff(channel);
if (staffChannel) {
channel = channel as TextChannel;
if (userId === null) {
let topic: string[];
if (channel.parentId === IDs.categories.modMail) {
// Checks if the channel topic has the user's snowflake
if (channel.topic !== null) {
topic = channel.topic.split(' ');
// eslint-disable-next-line prefer-destructuring
userId = topic[2];
}
}
}
if (userId === null) {
await interaction.reply({
content: 'User could not be found or was not provided!',
ephemeral: true,
fetchReply: true,
});
return;
}
const info = await this.unRestrictRun(userId, guild);
await interaction.reply({
embeds: info.embeds,
content: info.message,
fetchReply: true,
ephemeral: !staffChannel,
});
}
}
// Non Application Command method of banning a user
public async messageRun(message: Message, args: Args) {
// Get arguments
let userId: Snowflake | null;
try {
const user = await args.pick('user');
userId = user.id;
} catch {
userId = null;
}
const { guild } = message;
if (guild === null) {
await message.react('❌');
await message.reply('Guild not found! Try again or contact a developer!');
return;
}
if (userId === null) {
const { channel } = message;
if (channel.type !== ChannelType.GuildText) {
await message.react('❌');
await message.reply('User was not provided!');
return;
}
let topic: string[];
if (channel.parentId === IDs.categories.modMail) {
// Checks if the channel topic has the user's snowflake
if (channel.topic !== null) {
topic = channel.topic.split(' ');
// eslint-disable-next-line prefer-destructuring
userId = topic[2];
}
}
}
if (userId === null) {
await message.react('❌');
await message.reply('User was not provided!');
return;
}
const info = await this.unRestrictRun(userId, guild);
await message.reply({ content: info.message, embeds: info.embeds });
if (!info.success) {
await message.react('❌');
}
}
private async unRestrictRun(userId: Snowflake, guild: Guild) {
const info = {
message: '',
embeds: [] as EmbedBuilder[],
success: false,
};
let user = guild.client.users.cache.get(userId);
if (user === undefined) {
user = await guild.client.users.fetch(userId);
if (user === undefined) {
info.message = 'Error fetching user';
return info;
}
}
const restrictions = await getRestrictions(userId);
if (restrictions.length === 0) {
info.message = `${user} user has no restrict logs on them.`;
return info;
}
// Creates the embed to display the restrictions
const embed = new EmbedBuilder()
.setColor('#FF6700')
.setTitle(`${restrictions.length} restrictions for ${user.tag}`)
.setThumbnail(user.displayAvatarURL())
.setFooter({ text: `ID: ${userId}` });
// Add up to 10 of the latest restrictions to the embed
for (
let i = restrictions.length > 10 ? restrictions.length - 10 : 0;
i < restrictions.length;
i += 1
) {
// Get mod names
let restMod = restrictions[i].modId;
const restModMember = guild.members.cache.get(restMod);
if (restModMember !== undefined) {
restMod = restModMember.displayName;
}
let endRestMod = restrictions[i].endModId;
if (endRestMod !== null) {
const endRestModMember = guild.members.cache.get(endRestMod);
if (endRestModMember !== undefined) {
endRestMod = endRestModMember.displayName;
}
}
let restTitle = `Restriction: ${i + 1} | Restricted by: ${restMod} | `;
if (endRestMod !== null) {
restTitle += `Unrestricted by: ${endRestMod} | `;
} else {
restTitle += 'Currently Restricted | ';
}
restTitle += `Date: <t:${Math.floor(
restrictions[i].startTime.getTime() / 1000,
)}>`;
embed.addFields({
name: restTitle,
value: restrictions[i].reason,
});
}
info.embeds.push(embed);
info.success = true;
return info;
}
}

View File

@@ -1,120 +0,0 @@
// SPDX-License-Identifier: GPL-3.0-or-later
/*
Animal Rights Advocates Discord Bot
Copyright (C) 2023 Anthony Berg
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { Args, Command, RegisterBehavior } from '@sapphire/framework';
import type { User, Message } from 'discord.js';
import { restrictRun } from './restrict';
export class RestrictToleranceCommand extends Command {
public constructor(context: Command.LoaderContext, options: Command.Options) {
super(context, {
...options,
name: 'restricttolerance',
aliases: ['rt'],
description: 'Restricts a user for bigoted reasons',
preconditions: ['ModOnly'],
});
}
// Registers that this is a slash command
public override registerApplicationCommands(registry: Command.Registry) {
registry.registerChatInputCommand(
(builder) =>
builder
.setName(this.name)
.setDescription(this.description)
.addUserOption((option) =>
option
.setName('user')
.setDescription('User to restrict')
.setRequired(true),
)
.addStringOption((option) =>
option
.setName('reason')
.setDescription('Reason for restricting the user')
.setRequired(true),
),
{
behaviorWhenNotIdentical: RegisterBehavior.Overwrite,
},
);
}
// Command run
public async chatInputRun(interaction: Command.ChatInputCommandInteraction) {
// Get the arguments
const user = interaction.options.getUser('user', true);
const reason = interaction.options.getString('reason', true);
const mod = interaction.user;
const { guild } = interaction;
// Checks if all the variables are of the right type
if (guild === null) {
await interaction.reply({
content: 'Error fetching guild!',
ephemeral: true,
fetchReply: true,
});
return;
}
await interaction.deferReply({ ephemeral: true });
const info = await restrictRun(user?.id, mod.id, reason, guild, true);
await interaction.editReply({
content: info.message,
});
}
// Non Application Command method of banning a user
public async messageRun(message: Message, args: Args) {
// Get arguments
let user: User;
try {
user = await args.pick('user');
} catch {
await message.react('❌');
await message.reply('User was not provided!');
return;
}
const reason = args.finished ? null : await args.rest('string');
const mod = message.author;
if (reason === null) {
await message.react('❌');
await message.reply('Restrict reason was not provided!');
return;
}
const { guild } = message;
if (guild === null) {
await message.react('❌');
await message.reply('Guild not found! Try again or contact a developer!');
return;
}
const info = await restrictRun(user?.id, mod.id, reason, guild, true);
await message.reply(info.message);
await message.react(info.success ? '✅' : '❌');
}
}

View File

@@ -1,181 +0,0 @@
// SPDX-License-Identifier: GPL-3.0-or-later
/*
Animal Rights Advocates Discord Bot
Copyright (C) 2023 Anthony Berg
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { RegisterBehavior } from '@sapphire/framework';
import { Subcommand } from '@sapphire/plugin-subcommands';
import type { TextChannel } from 'discord.js';
import { CategoryChannel, ChannelType } from 'discord.js';
import IDs from '#utils/ids';
export class RestrictToolsCommand extends Subcommand {
public constructor(
context: Subcommand.LoaderContext,
options: Subcommand.Options,
) {
super(context, {
...options,
name: 'restricttools',
subcommands: [
{
name: 'channel',
type: 'group',
entries: [{ name: 'delete', chatInputRun: 'deleteChannel' }],
},
],
description: 'Tools for managing restrictions',
preconditions: ['RestrictedAccessOnly'],
});
}
// Registers that this is a slash command
public override registerApplicationCommands(registry: Subcommand.Registry) {
registry.registerChatInputCommand(
(builder) =>
builder
.setName(this.name)
.setDescription(this.description)
.addSubcommandGroup((group) =>
group
.setName('channel')
.setDescription('Manages restricted channels')
.addSubcommand((command) =>
command
.setName('delete')
.setDescription('Deletes a restricted channel')
.addUserOption((option) =>
option
.setName('user')
.setDescription("The user's channel to delete"),
),
),
),
{
behaviorWhenNotIdentical: RegisterBehavior.Overwrite,
},
);
}
public async deleteChannel(
interaction: Subcommand.ChatInputCommandInteraction,
) {
// Get the arguments
const user = interaction.options.getUser('user');
const { guild, channel } = interaction;
await interaction.deferReply({ ephemeral: true });
// Checks if all the variables are of the right type
if (guild === null || channel === null) {
await interaction.editReply({
content: 'Error fetching user!',
});
return;
}
let topic: string[];
if (user === null) {
if (channel.type !== ChannelType.GuildText) {
await interaction.editReply({
content:
'Please make sure you ran this command in the original restricted text channel!',
});
return;
}
if (channel.parentId !== IDs.categories.restricted) {
await interaction.editReply({
content:
'Please make sure you ran this command in the original restricted text channel!',
});
return;
}
if (
channel.id === IDs.channels.restricted.welcome ||
channel.id === IDs.channels.restricted.moderators ||
channel.id === IDs.channels.restricted.restricted ||
channel.id === IDs.channels.restricted.tolerance
) {
await interaction.editReply({
content: "You can't run this command these channels!",
});
return;
}
if (channel.topic === null) {
await interaction.editReply({
content: "There was an error with this channel's topic!",
});
return;
}
topic = channel.topic.split(' ');
await channel.delete();
const vcId = topic[3];
const voiceChannel = guild.channels.cache.get(vcId);
if (
voiceChannel !== undefined &&
voiceChannel.parentId === IDs.categories.restricted
) {
await voiceChannel.delete();
}
return;
}
const category = guild.channels.cache.get(IDs.categories.restricted) as
| CategoryChannel
| undefined;
if (category === undefined) {
await interaction.editReply({
content: 'Could not find category!',
});
return;
}
const textChannels = category.children.cache.filter(
(c) => c.type === ChannelType.GuildText,
);
textChannels.forEach((c) => {
const textChannel = c as TextChannel;
// Checks if the channel topic has the user's snowflake
if (textChannel.topic?.includes(user?.id)) {
topic = textChannel.topic.split(' ');
const vcId = topic[topic.indexOf(user?.id) + 1];
const voiceChannel = guild.channels.cache.get(vcId);
if (
voiceChannel !== undefined &&
voiceChannel.parentId === IDs.categories.restricted
) {
voiceChannel.delete();
}
textChannel.delete();
}
});
await interaction.editReply({
content: `Successfully deleted the channel for ${user}`,
});
}
}

View File

@@ -1,272 +0,0 @@
// SPDX-License-Identifier: GPL-3.0-or-later
/*
Animal Rights Advocates Discord Bot
Copyright (C) 2023 Anthony Berg
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { Args, Command, RegisterBehavior } from '@sapphire/framework';
import { CategoryChannel, ChannelType, EmbedBuilder } from 'discord.js';
import type { User, Message, TextChannel, Guild, Snowflake } from 'discord.js';
import IDs from '#utils/ids';
import { fetchRoles, addExistingUser } from '#utils/database/dbExistingUser';
import {
unRestrict,
checkActive,
unRestrictLegacy,
} from '#utils/database/moderation/restriction';
export class UnRestrictCommand extends Command {
public constructor(context: Command.LoaderContext, options: Command.Options) {
super(context, {
...options,
name: 'unrestrict',
aliases: ['ur', 'urv'],
description: 'Unrestricts a user',
preconditions: ['ModOnly'],
});
}
// Registers that this is a slash command
public override registerApplicationCommands(registry: Command.Registry) {
registry.registerChatInputCommand(
(builder) =>
builder
.setName(this.name)
.setDescription(this.description)
.addUserOption((option) =>
option
.setName('user')
.setDescription('User to unrestrict')
.setRequired(true),
),
{
behaviorWhenNotIdentical: RegisterBehavior.Overwrite,
},
);
}
// Command run
public async chatInputRun(interaction: Command.ChatInputCommandInteraction) {
// Get the arguments
const user = interaction.options.getUser('user', true);
const mod = interaction.user;
const { guild } = interaction;
// Checks if all the variables are of the right type
if (guild === null) {
await interaction.reply({
content: 'Error fetching guild!',
ephemeral: true,
});
return;
}
await interaction.deferReply();
const info = await this.unRestrictRun(user?.id, mod.id, guild);
await interaction.editReply({
content: info.message,
});
}
// Non Application Command method of banning a user
public async messageRun(message: Message, args: Args) {
// Get arguments
let user: User;
try {
user = await args.pick('user');
} catch {
await message.react('❌');
await message.reply('User was not provided!');
return;
}
const mod = message.author;
const { guild } = message;
if (guild === null) {
await message.react('❌');
await message.reply('Guild not found! Try again or contact a developer!');
return;
}
const channelRun = message.channel;
const info = await this.unRestrictRun(
user?.id,
mod.id,
guild,
channelRun.id,
);
if (!info.runInVeganRestrict) {
await message.reply(info.message);
await message.react(info.success ? '✅' : '❌');
}
}
private async unRestrictRun(
userId: Snowflake,
modId: Snowflake,
guild: Guild,
channelRun: Snowflake | null = null,
) {
const info = {
message: '',
success: false,
runInVeganRestrict: false,
};
let user = guild.client.users.cache.get(userId);
if (user === undefined) {
user = await guild.client.users.fetch(userId);
if (user === undefined) {
info.message = 'Error fetching user';
return info;
}
}
// Gets mod's GuildMember
const mod = guild.members.cache.get(modId);
// Checks if guildMember is null
if (mod === undefined) {
info.message = 'Error fetching mod';
return info;
}
// Check if mod is in database
await addExistingUser(mod);
// Gets guildMember
let member = guild.members.cache.get(userId);
if (member === undefined) {
member = await guild.members.fetch(userId).catch(() => undefined);
}
if (member === undefined) {
info.message = "Can't unrestrict the user as they are not on this server";
return info;
}
// Check if user is in database
await addExistingUser(member);
const restrictRoles = IDs.roles.restrictions.restricted;
// Checks if the user is not restricted
if (!member.roles.cache.hasAny(...restrictRoles)) {
info.message = `${user} is not restricted!`;
return info;
}
if (await checkActive(userId)) {
const roles = await fetchRoles(userId);
await member.roles.add(roles);
// Unrestricts the user on the database
await unRestrict(userId, modId);
} else {
let section = 1;
for (let i = 0; i < restrictRoles.length; i += 1) {
if (member.roles.cache.has(restrictRoles[i])) {
section = i + 1;
}
}
await member.roles.add(IDs.roles.nonvegan.nonvegan);
// Unrestricts the user on the database but for restricts done on the old bot
await unRestrictLegacy(userId, modId, section);
}
await member.roles.remove(restrictRoles);
// Remove vegan restrict channels
if (member.roles.cache.has(IDs.roles.vegan.vegan)) {
const category = guild.channels.cache.get(IDs.categories.restricted) as
| CategoryChannel
| undefined;
let topic: string[];
if (category !== undefined) {
const textChannels = category.children.cache.filter(
(c) => c.type === ChannelType.GuildText,
);
textChannels.forEach((c) => {
const textChannel = c as TextChannel;
// Checks if the channel topic has the user's snowflake
if (textChannel.topic?.includes(userId)) {
if (textChannel.id === channelRun) {
info.runInVeganRestrict = true;
}
topic = textChannel.topic.split(' ');
const vcId = topic[topic.indexOf(userId) + 1];
const voiceChannel = guild.channels.cache.get(vcId);
if (
voiceChannel !== undefined &&
voiceChannel.parentId === IDs.categories.restricted
) {
voiceChannel.delete();
}
textChannel.delete();
}
});
}
}
info.success = true;
// Log the ban
let logChannel = guild.channels.cache.get(IDs.channels.logs.restricted) as
| TextChannel
| undefined;
if (logChannel === undefined) {
logChannel = (await guild.channels.fetch(
IDs.channels.logs.restricted,
)) as TextChannel | undefined;
if (logChannel === undefined) {
this.container.logger.error(
'Restrict Error: Could not fetch log channel',
);
info.message = `Unrestricted ${user} but could not find the log channel. This has been logged to the database.`;
return info;
}
}
const message = new EmbedBuilder()
.setColor('#28A745')
.setAuthor({
name: `Unrestricted ${user.tag}`,
iconURL: `${user.displayAvatarURL()}`,
})
.addFields(
{ name: 'User', value: `${user}`, inline: true },
{ name: 'Moderator', value: `${mod}`, inline: true },
)
.setTimestamp()
.setFooter({ text: `ID: ${userId}` });
await logChannel.send({ embeds: [message] });
info.message = `Unrestricted ${user}`;
return info;
}
}

View File

@@ -1,134 +0,0 @@
// SPDX-License-Identifier: GPL-3.0-or-later
/*
Animal Rights Advocates Discord Bot
Copyright (C) 2023 Anthony Berg
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { Args, Command, RegisterBehavior } from '@sapphire/framework';
import type { Message, TextBasedChannel } from 'discord.js';
import { ChannelType } from 'discord.js';
import { Duration, DurationFormatter } from '@sapphire/time-utilities';
import { isNumber } from '#utils/maths';
export class SlowmodeCommand extends Command {
public constructor(context: Command.LoaderContext, options: Command.Options) {
super(context, {
...options,
name: 'slowmode',
description: 'Sets slowmode for a channel',
preconditions: ['ModOnly'],
});
}
// Registers that this is a slash command
public override registerApplicationCommands(registry: Command.Registry) {
registry.registerChatInputCommand(
(builder) =>
builder
.setName(this.name)
.setDescription(this.description)
.addStringOption((option) =>
option
.setName('duration')
.setDescription('Set the slowmode time')
.setRequired(true),
),
{
behaviorWhenNotIdentical: RegisterBehavior.Overwrite,
},
);
}
// Command run
public async chatInputRun(interaction: Command.ChatInputCommandInteraction) {
// Get the arguments
const duration = interaction.options.getString('duration', true);
const { channel } = interaction;
if (channel === null) {
await interaction.reply({
content: 'Could not fetch channel!',
ephemeral: true,
fetchReply: true,
});
return;
}
const slowmode = await this.slowmode(duration, channel);
await interaction.reply({ content: slowmode.message });
}
public async messageRun(message: Message, args: Args) {
// Get arguments
const duration = args.finished ? null : await args.rest('string');
const { channel } = message;
if (duration === null) {
await message.react('❌');
await message.reply('Slowmode length was not provided!');
return;
}
const slowmode = await this.slowmode(duration, channel);
await message.reply(slowmode.message);
await message.react(slowmode.success ? '✅' : '❌');
}
private async slowmode(duration: string, channel: TextBasedChannel) {
const info = {
message: '',
success: false,
};
if (channel.type !== ChannelType.GuildText) {
info.message = 'Channel is not a text channel!';
return info;
}
let durationCheck = duration;
if (isNumber(durationCheck)) {
durationCheck += 's';
}
const durationParsed = new Duration(durationCheck);
let time = 0;
if (Number.isNaN(durationParsed.offset)) {
if (duration !== 'off') {
info.message = 'Invalid time format!';
return info;
}
time = 0;
} else {
time = durationParsed.offset;
}
await channel.setRateLimitPerUser(time / 1000);
info.success = true;
if (time === 0) {
info.message = `${channel} is no longer in slowmode.`;
return info;
}
info.message = `${channel} has now been set to a post every ${new DurationFormatter().format(
time,
)}.`;
return info;
}
}

View File

@@ -1,124 +0,0 @@
// SPDX-License-Identifier: GPL-3.0-or-later
/*
Animal Rights Advocates Discord Bot
Copyright (C) 2023 Anthony Berg
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { Args, Command, RegisterBehavior } from '@sapphire/framework';
import type { GuildMember, Message } from 'discord.js';
import IDs from '#utils/ids';
export class SoftMuteCommand extends Command {
public constructor(context: Command.LoaderContext, options: Command.Options) {
super(context, {
...options,
name: 'softmute',
aliases: ['sm'],
description:
'Prevent a user from reacting to a message by giving ' +
'the soft mute role',
preconditions: ['ModOnly'],
});
}
// Registers that this is a slash command
public override registerApplicationCommands(registry: Command.Registry) {
registry.registerChatInputCommand(
(builder) =>
builder
.setName(this.name)
.setDescription(this.description)
.addUserOption((option) =>
option
.setName('user')
.setDescription('User to soft mute')
.setRequired(true),
),
{
behaviorWhenNotIdentical: RegisterBehavior.Overwrite,
},
);
}
// Command run
public async chatInputRun(interaction: Command.ChatInputCommandInteraction) {
// TODO add database updates
// Get the arguments
const user = interaction.options.getUser('user', true);
const { guild } = interaction;
// Checks if all the variables are of the right type
if (guild === null) {
await interaction.reply({
content: 'Error fetching the guild!',
ephemeral: true,
fetchReply: true,
});
return;
}
// Gets guildMember whilst removing the ability of each other variables being null
const guildMember = guild.members.cache.get(user.id);
// Checks if guildMember is null
if (guildMember === undefined) {
await interaction.reply({
content: 'Error fetching user!',
ephemeral: true,
fetchReply: true,
});
return;
}
if (guildMember.roles.cache.has(IDs.roles.restrictions.softMute)) {
await guildMember.roles.remove(IDs.roles.restrictions.softMute);
await interaction.reply({
content: `Removed soft muted for ${user}`,
fetchReply: true,
});
return;
}
await guildMember.roles.add(IDs.roles.restrictions.softMute);
await interaction.reply({
content: `Soft muted ${user}`,
fetchReply: true,
});
}
public async messageRun(message: Message, args: Args) {
// Get arguments
let user: GuildMember;
try {
user = await args.pick('member');
} catch {
await message.react('❌');
await message.reply('User was not provided!');
return;
}
if (user.roles.cache.has(IDs.roles.restrictions.softMute)) {
await user.roles.remove(IDs.roles.restrictions.softMute);
await message.reply(`Removed soft mute for ${user}`);
} else {
await user.roles.add(IDs.roles.restrictions.softMute);
await message.reply(`Soft muted ${user}`);
}
await message.react('✅');
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,172 +0,0 @@
// SPDX-License-Identifier: GPL-3.0-or-later
/*
Animal Rights Advocates Discord Bot
Copyright (C) 2023 Anthony Berg
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { Args, Command, RegisterBehavior } from '@sapphire/framework';
import type { GuildMember, Message } from 'discord.js';
import {
addMute,
removeMute,
checkActive,
} from '#utils/database/moderation/vcMute';
import { addExistingUser } from '#utils/database/dbExistingUser';
export class VCMuteCommand extends Command {
public constructor(context: Command.LoaderContext, options: Command.Options) {
super(context, {
...options,
name: 'vcmute',
aliases: ['vmute'],
description: 'Persists a server mute if a user is trying to bypass mute',
preconditions: [['CoordinatorOnly', 'ModOnly']],
});
}
// Registers that this is a slash command
public override registerApplicationCommands(registry: Command.Registry) {
registry.registerChatInputCommand(
(builder) =>
builder
.setName(this.name)
.setDescription(this.description)
.addUserOption((option) =>
option
.setName('user')
.setDescription('User to persistently mute')
.setRequired(true),
)
.addStringOption((option) =>
option
.setName('reason')
.setDescription('Reason for persistently muting the user'),
),
{
behaviorWhenNotIdentical: RegisterBehavior.Overwrite,
},
);
}
// Command run
public async chatInputRun(interaction: Command.ChatInputCommandInteraction) {
// Get the arguments
const user = interaction.options.getUser('user', true);
const reason = interaction.options.getString('reason');
const modUser = interaction.user;
const { guild } = interaction;
// Checks if all the variables are of the right type
if (guild === null) {
await interaction.reply({
content: 'Error fetching guild!',
ephemeral: true,
fetchReply: true,
});
return;
}
// Gets guildMember whilst removing the ability of each other variables being null
const member = guild.members.cache.get(user.id);
const mod = guild.members.cache.get(modUser.id);
// Checks if guildMember is null
if (member === undefined || mod === undefined) {
await interaction.reply({
content: 'Error fetching user!',
ephemeral: true,
fetchReply: true,
});
return;
}
// Check if removing VC Mute
if (await checkActive(member.id)) {
await removeMute(member.id);
if (member.voice.channel !== null) {
await member.voice.setMute(false, reason === null ? undefined : reason);
}
await interaction.reply({
content: `Removed server mute from ${user}`,
fetchReply: true,
ephemeral: true,
});
return;
}
// Check if mod is in database
await addExistingUser(mod);
// Add VC Mute
if (member.voice.channel !== null) {
await member.voice.setMute(true, reason === null ? undefined : reason);
}
await addMute(member.id, mod.id, reason);
await interaction.reply({
content: `Server muted ${user}`,
fetchReply: true,
ephemeral: true,
});
}
public async messageRun(message: Message, args: Args) {
// Get arguments
let member: GuildMember;
try {
member = await args.pick('member');
} catch {
await message.react('❌');
await message.reply('User was not provided!');
return;
}
const reason = args.finished ? null : await args.rest('string');
const mod = message.member;
if (mod === null) {
await message.react('❌');
await message.reply(
'Moderator not found! Try again or contact a developer!',
);
return;
}
// Check if removing VC Mute
if (await checkActive(member.id)) {
await removeMute(member.id);
if (member.voice.channel !== null) {
await member.voice.setMute(false, reason === null ? undefined : reason);
}
await message.reply(`Removed server mute from ${member}`);
await message.react('✅');
return;
}
// Check if mod is in database
await addExistingUser(mod);
// Add VC Mute
if (member.voice.channel !== null) {
await member.voice.setMute(true, reason === null ? undefined : reason);
}
await addMute(member.id, mod.id, reason);
await message.reply(`Server muted ${member}`);
await message.react('✅');
}
}

View File

@@ -1,186 +0,0 @@
// SPDX-License-Identifier: GPL-3.0-or-later
/*
Animal Rights Advocates Discord Bot
Copyright (C) 2024 Anthony Berg
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { Args, Command, RegisterBehavior } from '@sapphire/framework';
import { EmbedBuilder, TextChannel } from 'discord.js';
import type { Message, Guild, User } from 'discord.js';
import IDs from '#utils/ids';
import {
deleteWarning,
fetchWarning,
} from '#utils/database/moderation/warnings';
import { checkStaff } from '#utils/checker';
export class DeleteWarningCommand extends Command {
public constructor(context: Command.LoaderContext, options: Command.Options) {
super(context, {
...options,
name: 'deletewarning',
aliases: ['delwarn', 'removewarning'],
description: 'Deletes a warning',
preconditions: ['ModOnly'],
});
}
// Registers that this is a slash command
public override registerApplicationCommands(registry: Command.Registry) {
registry.registerChatInputCommand(
(builder) =>
builder
.setName(this.name)
.setDescription(this.description)
.addIntegerOption((option) =>
option
.setName('id')
.setDescription('ID for the warning')
.setRequired(true),
),
{
behaviorWhenNotIdentical: RegisterBehavior.Overwrite,
},
);
}
// Command run
public async chatInputRun(interaction: Command.ChatInputCommandInteraction) {
// Get the arguments
const warningId = interaction.options.getInteger('id', true);
const mod = interaction.user;
const { guild } = interaction;
// Checks if all the variables are of the right type
if (guild === null) {
await interaction.reply({
content: 'Error fetching guild!',
ephemeral: true,
fetchReply: true,
});
return;
}
const staffChannel = checkStaff(interaction.channel);
await interaction.deferReply({ ephemeral: !staffChannel });
const info = await this.deleteWarning(warningId, mod, guild);
await interaction.editReply({
content: info.message,
embeds: info.embeds,
});
}
// Non Application Command method for removing a warning
public async messageRun(message: Message, args: Args) {
// Get arguments
let warningId: number;
try {
warningId = await args.pick('integer');
} catch {
await message.react('❌');
await message.react('Correct warning ID not provided!');
return;
}
const mod = message.author;
const { guild } = message;
if (guild === null) {
await message.react('❌');
await message.reply('Guild not found! Try again or contact a developer!');
return;
}
const info = await this.deleteWarning(warningId, mod, guild);
await message.reply({ content: info.message, embeds: info.embeds });
if (!info.success) {
await message.react('❌');
}
}
private async deleteWarning(warningId: number, mod: User, guild: Guild) {
const info = {
message: '',
embeds: [] as EmbedBuilder[],
success: false,
};
const warning = await fetchWarning(warningId);
if (warning === null) {
info.message = `Warning ID \`${warningId}\` not found!`;
return info;
}
await deleteWarning(warningId);
info.success = true;
const userId = warning.userId;
let user = guild.client.users.cache.get(userId);
if (user === undefined) {
user = await guild.client.users.fetch(userId);
if (user === undefined) {
info.message = `Deleted warning ID \`${warningId}\`, but the user could not be found!`;
return info;
}
}
// Log the warnings deletion
let logChannel = guild.channels.cache.get(IDs.channels.logs.sus) as
| TextChannel
| undefined;
if (logChannel === undefined) {
logChannel = (await guild.channels.fetch(IDs.channels.logs.sus)) as
| TextChannel
| undefined;
if (logChannel === undefined) {
this.container.logger.error(
'Delete Warning Error: Could not fetch log channel',
);
info.message =
`Deleted warning for ${user} (Warning ID: ${warningId} but ` +
'could not find the log channel.';
return info;
}
}
const message = new EmbedBuilder()
.setColor('#28A745')
.setAuthor({
name: `Removed warning for ${user.tag}`,
iconURL: `${user.displayAvatarURL()}`,
})
.addFields(
{ name: 'User', value: `${user}`, inline: true },
{ name: 'Moderator', value: `${mod}`, inline: true },
{ name: 'Warning ID', value: `${warningId}`, inline: true },
)
.setTimestamp()
.setFooter({ text: `ID: ${userId}` });
await logChannel.send({ embeds: [message] });
info.message = `Deleted warning for ${user}`;
return info;
}
}

View File

@@ -1,226 +0,0 @@
// SPDX-License-Identifier: GPL-3.0-or-later
/*
Animal Rights Advocates Discord Bot
Copyright (C) 2023, 2024 Anthony Berg
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import {
Args,
Command,
container,
RegisterBehavior,
} from '@sapphire/framework';
import type { User, Message, Snowflake, Guild, TextChannel } from 'discord.js';
import { updateUser } from '#utils/database/dbExistingUser';
import { addWarn } from '#utils/database/moderation/warnings';
import { EmbedBuilder } from 'discord.js';
import IDs from '#utils/ids';
export class WarnCommand extends Command {
public constructor(context: Command.LoaderContext, options: Command.Options) {
super(context, {
...options,
name: 'warn',
description: 'Warns a user',
preconditions: [['CoordinatorOnly', 'ModOnly']],
});
}
// Registers that this is a slash command
public override registerApplicationCommands(registry: Command.Registry) {
registry.registerChatInputCommand(
(builder) =>
builder
.setName(this.name)
.setDescription(this.description)
.addUserOption((option) =>
option
.setName('user')
.setDescription('User to warn')
.setRequired(true),
)
.addStringOption((option) =>
option
.setName('reason')
.setDescription('Reason for the warning')
.setRequired(true),
),
{
behaviorWhenNotIdentical: RegisterBehavior.Overwrite,
},
);
}
// Command run
public async chatInputRun(interaction: Command.ChatInputCommandInteraction) {
// Get the arguments
const user = interaction.options.getUser('user', true);
const reason = interaction.options.getString('reason', true);
const mod = interaction.user;
const { guild } = interaction;
// Checks if all the variables are of the right type
if (guild === null) {
await interaction.reply({
content: 'Error fetching guild!',
ephemeral: true,
fetchReply: true,
});
return;
}
await interaction.deferReply();
const info = await this.warn(user.id, mod.id, reason, guild);
await interaction.editReply({
content: info.message,
});
}
// Non Application Command method for warning a user
public async messageRun(message: Message, args: Args) {
// Get arguments
let user: User;
try {
user = await args.pick('user');
} catch {
await message.react('❌');
await message.reply('User was not provided!');
return;
}
const reason = args.finished ? null : await args.rest('string');
const mod = message.member;
if (reason === null) {
await message.react('❌');
await message.reply('Warn reason was not provided!');
return;
}
if (mod === null) {
await message.react('❌');
await message.reply(
'Moderator not found! Try again or contact a developer!',
);
return;
}
const { guild } = message;
if (guild === null) {
await message.react('❌');
await message.reply('Guild not found! Try again or contact a developer!');
return;
}
const warn = await this.warn(user.id, mod.id, reason, guild);
if (!warn.success) {
await message.react('❌');
return;
}
await message.react('✅');
}
private async warn(
userId: Snowflake,
modId: Snowflake,
reason: string,
guild: Guild,
) {
const info = {
message: '',
success: false,
};
// Gets mod's GuildMember
const mod = guild.members.cache.get(modId);
// Checks if guildMember is null
if (mod === undefined) {
info.message = 'Error fetching mod!';
return info;
}
// Check if mod is in database
await updateUser(mod);
// Gets User for person being restricted
let user = guild.client.users.cache.get(userId);
if (user === undefined) {
user = await guild.client.users.fetch(userId);
if (user === undefined) {
info.message = 'Error fetching user';
return info;
}
}
await addWarn(userId, modId, reason);
info.message = `Warned ${user}`;
info.success = true;
// DM the reason
const dmEmbed = new EmbedBuilder()
.setColor('#FF6700')
.setAuthor({
name: "You've been warned!",
iconURL: `${user.displayAvatarURL()}`,
})
.addFields({ name: 'Reason', value: reason })
.setTimestamp();
await user.send({ embeds: [dmEmbed] }).catch(() => {});
// Log the ban
let logChannel = guild.channels.cache.get(IDs.channels.logs.sus) as
| TextChannel
| undefined;
if (logChannel === undefined) {
logChannel = (await guild.channels.fetch(IDs.channels.logs.sus)) as
| TextChannel
| undefined;
if (logChannel === undefined) {
container.logger.error('Warn Error: Could not fetch log channel');
info.message = `Warned ${user} but could not find the log channel. This has been logged to the database.`;
return info;
}
}
const message = new EmbedBuilder()
.setColor('#FF6700')
.setAuthor({
name: `Warned ${user.tag}`,
iconURL: `${user.displayAvatarURL()}`,
})
.addFields(
{ name: 'User', value: `${user}`, inline: true },
{ name: 'Moderator', value: `${mod}`, inline: true },
{ name: 'Reason', value: reason },
)
.setTimestamp()
.setFooter({ text: `ID: ${userId}` });
await logChannel.send({ embeds: [message] });
return info;
}
}

View File

@@ -1,161 +0,0 @@
// SPDX-License-Identifier: GPL-3.0-or-later
/*
Animal Rights Advocates Discord Bot
Copyright (C) 2024 Anthony Berg
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { Args, Command, RegisterBehavior } from '@sapphire/framework';
import { ChannelType, EmbedBuilder } from 'discord.js';
import type { Message, Guild, User } from 'discord.js';
import IDs from '#utils/ids';
import { fetchWarnings } from '#utils/database/moderation/warnings';
import { checkStaff } from '#utils/checker';
import { createWarningsEmbed } from '#utils/embeds';
export class WarningsCommand extends Command {
public constructor(context: Command.LoaderContext, options: Command.Options) {
super(context, {
...options,
name: 'warnings',
aliases: ['warninglog', 'warnlog'],
description: 'Shows all the warnings for the user',
preconditions: ['ModOnly'],
});
}
// Registers that this is a slash command
public override registerApplicationCommands(registry: Command.Registry) {
registry.registerChatInputCommand(
(builder) =>
builder
.setName(this.name)
.setDescription(this.description)
.addUserOption((option) =>
option
.setName('user')
.setDescription('User to check the warnings for')
.setRequired(true),
),
{
behaviorWhenNotIdentical: RegisterBehavior.Overwrite,
},
);
}
// Command run
public async chatInputRun(interaction: Command.ChatInputCommandInteraction) {
// Get the arguments
const user = interaction.options.getUser('user', true);
const { guild } = interaction;
// Checks if all the variables are of the right type
if (guild === null) {
await interaction.reply({
content: 'Error fetching guild!',
ephemeral: true,
fetchReply: true,
});
return;
}
const staffChannel = checkStaff(interaction.channel);
await interaction.deferReply({ ephemeral: !staffChannel });
const info = await this.warnings(user, guild);
await interaction.editReply({
content: info.message,
embeds: info.embeds,
});
}
// Non Application Command method for fetching warnings
public async messageRun(message: Message, args: Args) {
// Get arguments
let user: User | undefined;
try {
user = await args.pick('user');
} catch {
user = undefined;
}
const { guild } = message;
if (guild === null) {
await message.react('❌');
await message.reply('Guild not found! Try again or contact a developer!');
return;
}
if (user === undefined) {
const { channel } = message;
if (channel.type !== ChannelType.GuildText) {
await message.react('❌');
await message.reply('User was not provided!');
return;
}
let topic: string[];
if (channel.parentId === IDs.categories.modMail) {
// Checks if the channel topic has the user's snowflake
if (channel.topic !== null) {
topic = channel.topic.split(' ');
// eslint-disable-next-line prefer-destructuring
const userId = topic[2];
user = guild.client.users.cache.get(userId);
if (user === undefined) {
user = await guild.client.users.fetch(userId);
}
}
}
}
if (user === undefined) {
await message.react('❌');
await message.reply('User was not provided!');
return;
}
const info = await this.warnings(user, guild);
await message.reply({ content: info.message, embeds: info.embeds });
}
private async warnings(user: User, guild: Guild) {
const info = {
message: '',
embeds: [] as EmbedBuilder[],
};
const warnings = await fetchWarnings(user.id);
if (warnings.length === 0) {
info.message = `${user} user has no warnings.`;
return info;
}
// Creates an embed to display the warnings
const embed = createWarningsEmbed(warnings, user, guild);
info.embeds.push(embed);
return info;
}
}

View File

@@ -1,596 +0,0 @@
// SPDX-License-Identifier: GPL-3.0-or-later
/*
Animal Rights Advocates Discord Bot
Copyright (C) 2023 Anthony Berg
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { Subcommand } from '@sapphire/plugin-subcommands';
import { RegisterBehavior } from '@sapphire/framework';
import { ChannelType, PermissionsBitField, Snowflake } from 'discord.js';
import { updateUser } from '#utils/database/dbExistingUser';
import {
addStatUser,
checkActiveEvent,
createEvent,
createStat,
endEvent,
getCurrentEvent,
getStatFromLeader,
getStatFromRole,
getStatGroups,
updateStats,
userInStats,
} from '#utils/database/outreach';
import IDs from '#utils/ids';
import { EmbedBuilder } from 'discord.js';
export class OutreachCommand extends Subcommand {
public constructor(
context: Subcommand.LoaderContext,
options: Subcommand.Options,
) {
super(context, {
...options,
name: 'outreach',
description: 'Tools for doing outreach',
subcommands: [
{
name: 'event',
type: 'group',
entries: [
{ name: 'create', chatInputRun: 'eventCreate' },
// { name: 'start', chatInputRun: 'eventStart' },
{ name: 'end', chatInputRun: 'eventEnd' },
],
},
{
name: 'group',
type: 'group',
entries: [
{ name: 'create', chatInputRun: 'groupCreate' },
{ name: 'add', chatInputRun: 'groupAdd' },
{ name: 'update', chatInputRun: 'groupUpdate' },
],
},
],
});
}
// Registers that this is a slash command
public override registerApplicationCommands(registry: Subcommand.Registry) {
registry.registerChatInputCommand(
(builder) =>
builder
.setName(this.name)
.setDescription(this.description)
.addSubcommandGroup((group) =>
group
.setName('event')
.setDescription('Commands to do with outreach events')
.addSubcommand((command) =>
command
.setName('create')
.setDescription('Start an outreach event'),
)
/*
TODO add this back at a later date
.addBooleanOption((option) => option.setName('start')
.setDescription('Start the event immediately'))
.addSubcommand((command) => command.setName('start')
.setDescription('Start an outreach event'))
*/
.addSubcommand((command) =>
command.setName('end').setDescription('End an outreach event'),
),
)
.addSubcommandGroup((group) =>
group
.setName('group')
.setDescription('Commands to do with groups')
.addSubcommand((command) =>
command
.setName('create')
.setDescription('Create a group for people doing activism')
.addUserOption((option) =>
option
.setName('leader')
.setDescription('This is the person leading the group')
.setRequired(true),
),
)
.addSubcommand((command) =>
command
.setName('add')
.setDescription('Add a person to the group')
.addUserOption((option) =>
option
.setName('user')
.setDescription('User to add to the group')
.setRequired(true),
)
.addRoleOption((option) =>
option
.setName('group')
.setDescription('Group to add the user to'),
),
)
.addSubcommand((command) =>
command
.setName('update')
.setDescription('Update the statistics for the group')
.addIntegerOption((option) =>
option
.setName('vegan')
.setDescription('How many said would go vegan?'),
)
.addIntegerOption((option) =>
option
.setName('considered')
.setDescription(
'How many seriously considered being vegan?',
),
)
.addIntegerOption((option) =>
option
.setName('anti-vegan')
.setDescription(
'How many people had anti-vegan viewpoints?',
),
)
.addIntegerOption((option) =>
option
.setName('thanked')
.setDescription(
'How many thanked you for the conversation?',
),
)
.addIntegerOption((option) =>
option
.setName('documentary')
.setDescription(
'How many said they would watch a vegan documentary?',
),
)
.addIntegerOption((option) =>
option
.setName('educated')
.setDescription(
'How many got educated on veganism or the animal industry?',
),
),
),
),
{
behaviorWhenNotIdentical: RegisterBehavior.Overwrite,
},
);
}
public async eventCreate(
interaction: Subcommand.ChatInputCommandInteraction,
) {
// const start = interaction.options.getBoolean('start');
const modUser = interaction.user;
const { guild } = interaction;
if (guild === null) {
await interaction.reply({
content: 'Mod or guild was not found!',
ephemeral: true,
});
return;
}
const mod = guild.members.cache.get(modUser.id);
if (mod === undefined) {
await interaction.reply({
content: 'Outreach Leader was not found!',
ephemeral: true,
});
return;
}
if (!mod.roles.cache.has(IDs.roles.staff.outreachLeader)) {
await interaction.reply({
content: 'You need to be an Outreach Leader to run this command!',
ephemeral: true,
});
return;
}
if (await checkActiveEvent()) {
await interaction.reply({
content: 'There is already an active event!',
ephemeral: true,
});
return;
}
await updateUser(mod);
await createEvent(modUser.id);
await interaction.reply({
content: 'Created the event!',
ephemeral: true,
});
}
public async eventEnd(interaction: Subcommand.ChatInputCommandInteraction) {
const modUser = interaction.user;
const { guild } = interaction;
if (guild === null) {
await interaction.reply({
content: 'Guild not found!',
ephemeral: true,
});
return;
}
const mod = guild.members.cache.get(modUser.id);
if (mod === undefined) {
await interaction.reply({
content: 'Your guild member was not found!',
ephemeral: true,
});
return;
}
if (!mod.roles.cache.has(IDs.roles.staff.outreachLeader)) {
await interaction.reply({
content: 'You need to be an Outreach Leader to run this command!',
ephemeral: true,
});
return;
}
await interaction.deferReply({ ephemeral: true });
const event = await getCurrentEvent();
if (event === null) {
await interaction.editReply('There is currently no event!');
return;
}
const [stat] = await Promise.all([getStatGroups(event.id)]);
stat.forEach(({ role }) => {
if (role !== null) {
guild.roles.delete(role.roleId); // Delete role
guild.channels.delete(role.channelId); // Delete VC
}
});
await endEvent(event.id);
// Statistics shown at the end
let vegan = 0;
let considered = 0;
let antiVegan = 0;
let thanked = 0;
let documentary = 0;
let educated = 0;
stat.forEach((group) => {
vegan += group.vegan;
considered += group.considered;
antiVegan += group.antivegan;
thanked += group.thanked;
documentary += group.documentary;
educated += group.educated;
});
const activist = guild.channels.cache.get(IDs.channels.activism.activism);
if (activist === undefined || !activist.isTextBased()) {
await interaction.editReply(
'Event has now ended, but could not post statistics!',
);
return;
}
const embed = new EmbedBuilder()
.setColor('#0099FF')
.setAuthor({ name: 'Stats for Discord Outreach' })
.addFields(
{ name: 'How many said would go vegan?', value: `${vegan}` },
{
name: 'How many seriously considered being vegan?',
value: `${considered}`,
},
{
name: 'How many people had anti-vegan viewpoints?',
value: `${antiVegan}`,
},
{
name: 'How many thanked you for the conversation?',
value: `${thanked}`,
},
{
name: 'How many said they would watch a vegan documentary?',
value: `${documentary}`,
},
{
name: 'How many got educated on veganism or the animal industry?',
value: `${educated}`,
},
)
.setTimestamp()
.setFooter({ text: `Outreach Event: ${event.id}` });
await activist.send({ embeds: [embed] });
await interaction.editReply('Event has now ended!');
}
public async groupCreate(
interaction: Subcommand.ChatInputCommandInteraction,
) {
const leader = interaction.options.getUser('leader', true);
const { guild } = interaction;
if (guild === null) {
await interaction.reply({
content: 'Guild not found!',
ephemeral: true,
});
return;
}
await interaction.deferReply({ ephemeral: true });
if ((await getStatFromLeader(leader.id)) !== null) {
await interaction.editReply(
`${leader} is already a leader for another group!`,
);
return;
}
const event = await getCurrentEvent();
if (event === null) {
await interaction.editReply({
content: 'There is no current event!',
});
return;
}
const statGroups = await getStatGroups(event.id);
const groupNo = statGroups.length + 1;
const leaderMember = await guild.members.cache.get(leader.id);
if (leaderMember === undefined) {
await interaction.editReply({
content: `Could not find ${leader}'s guild member.`,
});
return;
}
await updateUser(leaderMember);
// Create role for group
const role = await guild.roles.create({
name: `Outreach Group ${groupNo}`,
mentionable: true,
});
// Create a voice channel for group
const channel = await guild.channels.create({
name: `Outreach Group ${groupNo}`,
type: ChannelType.GuildVoice,
parent: IDs.categories.activism,
permissionOverwrites: [
{
id: guild.roles.everyone,
deny: [
PermissionsBitField.Flags.SendMessages,
PermissionsBitField.Flags.Connect,
PermissionsBitField.Flags.ViewChannel,
],
},
{
id: IDs.roles.vegan.activist,
allow: [PermissionsBitField.Flags.ViewChannel],
},
{
id: role.id, // Permissions for the specific group
allow: [
PermissionsBitField.Flags.SendMessages,
PermissionsBitField.Flags.Connect,
],
},
{
id: IDs.roles.staff.outreachLeader,
allow: [
PermissionsBitField.Flags.SendMessages,
PermissionsBitField.Flags.Connect,
],
},
],
});
// Create stats in database
await createStat(event.id, leader.id, role.id, channel.id);
// Give group leader role
await leaderMember.roles.add(role);
// Send message in VC with a welcome and reminder
await channel.send(
`Welcome ${role}, ${leaderMember} is going to be the leader of your group!\n\n` +
'Remember to keep track of stats during activism with `/outreach group update` and' +
'to have these questions in mind whilst doing activism:\n' +
'- How many said would go vegan?\n' +
'- How many seriously considered being vegan?\n' +
'- How many people had anti-vegan viewpoints?\n' +
'- How many thanked you for the conversation?\n' +
'- How many said they would watch a vegan documentary?\n' +
'- How many got educated on veganism or the animal industry?',
);
await interaction.editReply({
content: `Created a group with the leader being ${leader}`,
});
}
public async groupAdd(interaction: Subcommand.ChatInputCommandInteraction) {
const user = interaction.options.getUser('user', true);
const group = interaction.options.getRole('group');
const leader = interaction.user;
const { guild } = interaction;
if (guild === null) {
await interaction.reply({
content: 'Could not find guild!',
ephemeral: true,
});
return;
}
await interaction.deferReply({ ephemeral: true });
let statId: number;
let roleId: Snowflake | undefined;
// Find group from role
if (group !== null) {
const [stat] = await Promise.all([getStatFromRole(group.id)]);
if (stat === null) {
await interaction.editReply({
content: `Could not find the group for role ${group}`,
});
return;
}
const leaderMember = guild.members.cache.get(leader.id);
if (leaderMember === undefined) {
await interaction.editReply({
content: 'Could not find your GuildMember in cache!',
});
return;
}
if (
leader.id !== stat.stat.leaderId &&
!leaderMember.roles.cache.has(IDs.roles.staff.outreachLeader)
) {
await interaction.editReply({
content: `You are not the leader for ${group}`,
});
return;
}
statId = stat.statId;
roleId = stat.roleId;
} else {
// Find group from person who ran the command
const [stat] = await Promise.all([getStatFromLeader(leader.id)]);
if (stat === null) {
await interaction.editReply({
content: "You're not a group leader!",
});
return;
}
statId = stat.id;
roleId = stat.role?.roleId;
}
if (await userInStats(statId, user.id)) {
await interaction.editReply({
content: `${user} is already in this group!`,
});
return;
}
const member = guild.members.cache.get(user.id);
if (member === undefined) {
await interaction.editReply({
content: 'Could not fetch the member!',
});
return;
}
await updateUser(member);
await addStatUser(statId, user.id);
if (roleId !== undefined) {
await member.roles.add(roleId);
}
await interaction.editReply({
content: `Added ${user} to the group!`,
});
}
public async groupUpdate(
interaction: Subcommand.ChatInputCommandInteraction,
) {
const vegan = interaction.options.getInteger('vegan');
const considered = interaction.options.getInteger('considered');
const antiVegan = interaction.options.getInteger('anti-vegan');
const thanked = interaction.options.getInteger('thanked');
const documentary = interaction.options.getInteger('documentary');
const educated = interaction.options.getInteger('educated');
const leader = interaction.user;
const stats = {
vegan: vegan !== null ? vegan : 0,
considered: considered !== null ? considered : 0,
antiVegan: antiVegan !== null ? antiVegan : 0,
thanked: thanked !== null ? thanked : 0,
documentary: documentary !== null ? documentary : 0,
educated: educated !== null ? educated : 0,
};
if (leader === null) {
await interaction.reply({
content: 'Could not find your user!',
ephemeral: true,
});
return;
}
await interaction.deferReply({ ephemeral: true });
const stat = await getStatFromLeader(leader.id);
if (stat === null) {
await interaction.editReply({
content: "You're not the leader of a group!",
});
return;
}
await updateStats(stat.id, stats);
await interaction.editReply({
content: 'Updated the database with the stats!',
});
}
}

View File

@@ -1,152 +0,0 @@
// SPDX-License-Identifier: GPL-3.0-or-later
/*
Animal Rights Advocates Discord Bot
Copyright (C) 2023 Anthony Berg
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { Args, Command, RegisterBehavior } from '@sapphire/framework';
import type { Guild, User, Message } from 'discord.js';
import IDs from '#utils/ids';
import { roleAddLog, roleRemoveLog } from '#utils/logging/role';
export class BookClubCommand extends Command {
public constructor(context: Command.LoaderContext, options: Command.Options) {
super(context, {
...options,
name: 'bookclub',
description: 'Gives the Book Club role',
preconditions: ['EventCoordinatorOnly'],
});
}
// Registers that this is a slash command
public override registerApplicationCommands(registry: Command.Registry) {
registry.registerChatInputCommand(
(builder) =>
builder
.setName(this.name)
.setDescription(this.description)
.addUserOption((option) =>
option
.setName('user')
.setDescription('User to give Book Club to')
.setRequired(true),
),
{
behaviorWhenNotIdentical: RegisterBehavior.Overwrite,
},
);
}
// Command run
public async chatInputRun(interaction: Command.ChatInputCommandInteraction) {
// Get the arguments
const user = interaction.options.getUser('user', true);
const mod = interaction.user;
const { guild } = interaction;
// Checks if all the variables are of the right type
if (guild === null) {
await interaction.reply({
content: 'Error fetching guild!',
ephemeral: true,
fetchReply: true,
});
return;
}
await interaction.deferReply({ ephemeral: true });
const info = await this.manageBookClub(user, mod, guild);
await interaction.editReply(info.message);
}
public async messageRun(message: Message, args: Args) {
// Get arguments
let user: User;
try {
user = await args.pick('user');
} catch {
await message.react('❌');
await message.reply('User was not provided!');
return;
}
const mod = message.author;
if (mod === null) {
await message.react('❌');
await message.reply(
'Event coordinator not found! Try again or contact a developer!',
);
return;
}
const { guild } = message;
if (guild === null) {
await message.react('❌');
await message.reply('Guild not found! Try again or contact a developer!');
return;
}
const info = await this.manageBookClub(user, mod, guild);
await message.reply(info.message);
await message.react(info.success ? '✅' : '❌');
}
private async manageBookClub(user: User, mod: User, guild: Guild) {
const info = {
message: '',
success: false,
};
const member = guild.members.cache.get(user.id);
const bookClub = guild.roles.cache.get(IDs.roles.bookClub);
// Checks if user's GuildMember was found in cache
if (member === undefined) {
info.message = 'Error fetching guild member for the user!';
return info;
}
if (bookClub === undefined) {
info.message = 'Error fetching book club role from cache!';
return info;
}
// Checks if the user has Book Club and to give them or remove them based on if they have it
if (member.roles.cache.has(IDs.roles.bookClub)) {
// Remove the Book Club role from the user
await member.roles.remove(bookClub);
await roleRemoveLog(user.id, mod.id, bookClub);
info.message = `Removed the ${bookClub.name} role from ${user}`;
info.success = true;
return info;
}
// Add Book Club role to the user
await member.roles.add(bookClub);
await roleAddLog(user.id, mod.id, bookClub);
info.message = `Gave ${user} the ${bookClub.name} role!`;
await user
.send(`You have been given the ${bookClub.name} role by ${mod}!`)
.catch(() => {});
info.success = true;
return info;
}
}

View File

@@ -1,153 +0,0 @@
// SPDX-License-Identifier: GPL-3.0-or-later
/*
Animal Rights Advocates Discord Bot
Copyright (C) 2023 Anthony Berg
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { Args, Command, RegisterBehavior } from '@sapphire/framework';
import type { Guild, User, Message } from 'discord.js';
import IDs from '#utils/ids';
import { roleAddLog, roleRemoveLog } from '#utils/logging/role';
export class DebateHostCommand extends Command {
public constructor(context: Command.LoaderContext, options: Command.Options) {
super(context, {
...options,
name: 'debatehost',
description: 'Gives the Debate Host role',
preconditions: ['EventCoordinatorOnly'],
});
}
// Registers that this is a slash command
public override registerApplicationCommands(registry: Command.Registry) {
registry.registerChatInputCommand(
(builder) =>
builder
.setName(this.name)
.setDescription(this.description)
.addUserOption((option) =>
option
.setName('user')
.setDescription('User to give Debate Host role to')
.setRequired(true),
),
{
behaviorWhenNotIdentical: RegisterBehavior.Overwrite,
},
);
}
// Command run
public async chatInputRun(interaction: Command.ChatInputCommandInteraction) {
// TODO add database updates
// Get the arguments
const user = interaction.options.getUser('user', true);
const mod = interaction.user;
const { guild } = interaction;
// Checks if all the variables are of the right type
if (guild === null) {
await interaction.reply({
content: 'Error fetching guild!',
ephemeral: true,
fetchReply: true,
});
return;
}
await interaction.deferReply({ ephemeral: true });
const info = await this.manageDebateHost(user, mod, guild);
await interaction.editReply(info.message);
}
public async messageRun(message: Message, args: Args) {
// Get arguments
let user: User;
try {
user = await args.pick('user');
} catch {
await message.react('❌');
await message.reply('User was not provided!');
return;
}
const mod = message.author;
if (mod === null) {
await message.react('❌');
await message.reply(
'Event coordinator not found! Try again or contact a developer!',
);
return;
}
const { guild } = message;
if (guild === null) {
await message.react('❌');
await message.reply('Guild not found! Try again or contact a developer!');
return;
}
const info = await this.manageDebateHost(user, mod, guild);
await message.reply(info.message);
await message.react(info.success ? '✅' : '❌');
}
private async manageDebateHost(user: User, mod: User, guild: Guild) {
const info = {
message: '',
success: false,
};
const member = guild.members.cache.get(user.id);
const debateHost = guild.roles.cache.get(IDs.roles.debateHost);
// Checks if user's GuildMember was found in cache
if (member === undefined) {
info.message = 'Error fetching guild member for the user!';
return info;
}
if (debateHost === undefined) {
info.message = 'Error fetching debate host role from cache!';
return info;
}
// Checks if the user has Debate Host and to give them or remove them based on if they have it
if (member.roles.cache.has(IDs.roles.debateHost)) {
// Remove the Debate Host role from the user
await member.roles.remove(debateHost);
await roleRemoveLog(user.id, mod.id, debateHost);
info.message = `Removed the ${debateHost.name} role from ${user}`;
info.success = true;
return info;
}
// Add Debate Host role to the user
await member.roles.add(debateHost);
await roleAddLog(user.id, mod.id, debateHost);
info.message = `Gave ${user} the ${debateHost.name} role!`;
await user
.send(`You have been given the ${debateHost.name} role by ${mod}!`)
.catch(() => {});
info.success = true;
return info;
}
}

View File

@@ -1,152 +0,0 @@
// SPDX-License-Identifier: GPL-3.0-or-later
/*
Animal Rights Advocates Discord Bot
Copyright (C) 2023 Anthony Berg
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { Args, Command, RegisterBehavior } from '@sapphire/framework';
import type { Guild, User, Message } from 'discord.js';
import IDs from '#utils/ids';
import { roleAddLog, roleRemoveLog } from '#utils/logging/role';
export class GameNightHostCommand extends Command {
public constructor(context: Command.LoaderContext, options: Command.Options) {
super(context, {
...options,
name: 'gamenight',
description: 'Gives the Game Night Host role',
preconditions: ['EventCoordinatorOnly'],
});
}
// Registers that this is a slash command
public override registerApplicationCommands(registry: Command.Registry) {
registry.registerChatInputCommand(
(builder) =>
builder
.setName(this.name)
.setDescription(this.description)
.addUserOption((option) =>
option
.setName('user')
.setDescription('User to give Game Night Host role to')
.setRequired(true),
),
{
behaviorWhenNotIdentical: RegisterBehavior.Overwrite,
},
);
}
// Command run
public async chatInputRun(interaction: Command.ChatInputCommandInteraction) {
// Get the arguments
const user = interaction.options.getUser('user', true);
const mod = interaction.user;
const { guild } = interaction;
// Checks if all the variables are of the right type
if (guild === null) {
await interaction.reply({
content: 'Error fetching guild!',
ephemeral: true,
fetchReply: true,
});
return;
}
await interaction.deferReply({ ephemeral: true });
const info = await this.manageGameNight(user, mod, guild);
await interaction.editReply(info.message);
}
public async messageRun(message: Message, args: Args) {
// Get arguments
let user: User;
try {
user = await args.pick('user');
} catch {
await message.react('❌');
await message.reply('User was not provided!');
return;
}
const mod = message.author;
if (mod === null) {
await message.react('❌');
await message.reply(
'Event coordinator not found! Try again or contact a developer!',
);
return;
}
const { guild } = message;
if (guild === null) {
await message.react('❌');
await message.reply('Guild not found! Try again or contact a developer!');
return;
}
const info = await this.manageGameNight(user, mod, guild);
await message.reply(info.message);
await message.react(info.success ? '✅' : '❌');
}
private async manageGameNight(user: User, mod: User, guild: Guild) {
const info = {
message: '',
success: false,
};
const member = guild.members.cache.get(user.id);
const gameNightHost = guild.roles.cache.get(IDs.roles.gameNightHost);
// Checks if user's GuildMember was found in cache
if (member === undefined) {
info.message = 'Error fetching guild member for the user!';
return info;
}
if (gameNightHost === undefined) {
info.message = 'Error fetching game night host role from cache!';
return info;
}
// Checks if the user has Game Night and to give them or remove them based on if they have it
if (member.roles.cache.has(IDs.roles.gameNightHost)) {
// Remove the Game Night Host role from the user
await member.roles.remove(gameNightHost);
await roleRemoveLog(user.id, mod.id, gameNightHost);
info.message = `Removed the ${gameNightHost.name} role from ${user}`;
info.success = true;
return info;
}
// Add Game Night Host role to the user
await member.roles.add(gameNightHost);
await roleAddLog(user.id, mod.id, gameNightHost);
info.message = `Gave ${user} the ${gameNightHost.name} role!`;
await user
.send(`You have been given the ${gameNightHost.name} role by ${mod}!`)
.catch(() => {});
info.success = true;
return info;
}
}

View File

@@ -1,152 +0,0 @@
// SPDX-License-Identifier: GPL-3.0-or-later
/*
Animal Rights Advocates Discord Bot
Copyright (C) 2023 Anthony Berg
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { Args, Command, RegisterBehavior } from '@sapphire/framework';
import type { Guild, User, Message } from 'discord.js';
import IDs from '#utils/ids';
import { roleAddLog, roleRemoveLog } from '#utils/logging/role';
export class GuestCommand extends Command {
public constructor(context: Command.LoaderContext, options: Command.Options) {
super(context, {
...options,
name: 'guest',
description: 'Gives the Guest role',
preconditions: ['EventCoordinatorOnly'],
});
}
// Registers that this is a slash command
public override registerApplicationCommands(registry: Command.Registry) {
registry.registerChatInputCommand(
(builder) =>
builder
.setName(this.name)
.setDescription(this.description)
.addUserOption((option) =>
option
.setName('user')
.setDescription('User to give Guest role to')
.setRequired(true),
),
{
behaviorWhenNotIdentical: RegisterBehavior.Overwrite,
},
);
}
// Command run
public async chatInputRun(interaction: Command.ChatInputCommandInteraction) {
// Get the arguments
const user = interaction.options.getUser('user', true);
const mod = interaction.user;
const { guild } = interaction;
// Checks if all the variables are of the right type
if (guild === null) {
await interaction.reply({
content: 'Error fetching guild!',
ephemeral: true,
fetchReply: true,
});
return;
}
await interaction.deferReply({ ephemeral: true });
const info = await this.manageGuest(user, mod, guild);
await interaction.editReply(info.message);
}
public async messageRun(message: Message, args: Args) {
// Get arguments
let user: User;
try {
user = await args.pick('user');
} catch {
await message.react('❌');
await message.reply('User was not provided!');
return;
}
const mod = message.author;
if (mod === null) {
await message.react('❌');
await message.reply(
'Event coordinator not found! Try again or contact a developer!',
);
return;
}
const { guild } = message;
if (guild === null) {
await message.react('❌');
await message.reply('Guild not found! Try again or contact a developer!');
return;
}
const info = await this.manageGuest(user, mod, guild);
await message.reply(info.message);
await message.react(info.success ? '✅' : '❌');
}
private async manageGuest(user: User, mod: User, guild: Guild) {
const info = {
message: '',
success: false,
};
const member = guild.members.cache.get(user.id);
const guest = guild.roles.cache.get(IDs.roles.guest);
// Checks if user's GuildMember was found in cache
if (member === undefined) {
info.message = 'Error fetching guild member for the user!';
return info;
}
if (guest === undefined) {
info.message = 'Error fetching guest role from cache!';
return info;
}
// Checks if the user has Guest and to give them or remove them based on if they have it
if (member.roles.cache.has(IDs.roles.guest)) {
// Remove the Guest role from the user
await member.roles.remove(guest);
await roleRemoveLog(user.id, mod.id, guest);
info.message = `Removed the ${guest.name} role from ${user}`;
info.success = true;
return info;
}
// Add Guest role to the user
await member.roles.add(guest);
await roleAddLog(user.id, mod.id, guest);
info.message = `Gave ${user} the ${guest.name} role!`;
await user
.send(`You have been given the ${guest.name} role by ${mod}!`)
.catch(() => {});
info.success = true;
return info;
}
}

View File

@@ -1,153 +0,0 @@
// SPDX-License-Identifier: GPL-3.0-or-later
/*
Animal Rights Advocates Discord Bot
Copyright (C) 2023 Anthony Berg
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { Args, Command, RegisterBehavior } from '@sapphire/framework';
import type { Guild, User, Message } from 'discord.js';
import IDs from '#utils/ids';
import { roleAddLog, roleRemoveLog } from '#utils/logging/role';
export class MentorCommand extends Command {
public constructor(context: Command.LoaderContext, options: Command.Options) {
super(context, {
...options,
name: 'mentor',
aliases: ['vegs'],
description: 'Gives/removes the mentor role',
preconditions: ['MentorCoordinatorOnly'],
});
}
// Registers that this is a slash command
public override registerApplicationCommands(registry: Command.Registry) {
registry.registerChatInputCommand(
(builder) =>
builder
.setName(this.name)
.setDescription(this.description)
.addUserOption((option) =>
option
.setName('user')
.setDescription('User to give/remove mentor role')
.setRequired(true),
),
{
behaviorWhenNotIdentical: RegisterBehavior.Overwrite,
},
);
}
// Command run
public async chatInputRun(interaction: Command.ChatInputCommandInteraction) {
// Get the arguments
const user = interaction.options.getUser('user', true);
const mod = interaction.user;
const { guild } = interaction;
// Checks if all the variables are of the right type
if (guild === null) {
await interaction.reply({
content: 'Error fetching guild!',
ephemeral: true,
fetchReply: true,
});
return;
}
await interaction.deferReply({ ephemeral: true });
const info = await this.manageMentor(user, mod, guild);
await interaction.editReply(info.message);
}
public async messageRun(message: Message, args: Args) {
// Get arguments
let user: User;
try {
user = await args.pick('user');
} catch {
await message.react('❌');
await message.reply('User was not provided!');
return;
}
const mod = message.author;
if (mod === null) {
await message.react('❌');
await message.reply(
'Mentor coordinator not found! Try again or contact a developer!',
);
return;
}
const { guild } = message;
if (guild === null) {
await message.react('❌');
await message.reply('Guild not found! Try again or contact a developer!');
return;
}
const info = await this.manageMentor(user, mod, guild);
await message.reply(info.message);
await message.react(info.success ? '✅' : '❌');
}
private async manageMentor(user: User, mod: User, guild: Guild) {
const info = {
message: '',
success: false,
};
const member = guild.members.cache.get(user.id);
const mentor = guild.roles.cache.get(IDs.roles.staff.mentor);
// Checks if user's GuildMember was found in cache
if (member === undefined) {
info.message = 'Error fetching guild member for the user!';
return info;
}
if (mentor === undefined) {
info.message = 'Error fetching mentor role from cache!';
return info;
}
// Checks if the user has Mentor and to give them or remove them based on if they have it
if (member.roles.cache.has(IDs.roles.staff.mentor)) {
// Remove the Mentor role from the user
await member.roles.remove(mentor);
await roleRemoveLog(user.id, mod.id, mentor, true);
info.message = `Removed the ${mentor.name} role from ${user}`;
info.success = true;
return info;
}
// Add Mentor role to the user
await member.roles.add(mentor);
await roleAddLog(user.id, mod.id, mentor, true);
info.message = `Gave ${user} the ${mentor.name} role!`;
await user
.send(`You have been given the ${mentor.name} role by ${mod}!`)
.catch(() => {});
info.success = true;
return info;
}
}

Some files were not shown because too many files have changed in this diff Show More