Compare commits

..

36 commits

Author SHA1 Message Date
2ca9a40458
Add setup command for lobby (#123) 2022-04-10 13:29:57 +01:00
1b1a070cfd
Feature/66 add different commands per server (#122)
* Add ability for server exclusive commands

* Add MankBot server-exclusive commands

* Add lobby entity to database

* Add documentation
2022-04-09 14:11:06 +01:00
6a00c49ef3
Feature/48 database (#114)
* Add database and default values

* Add ability to save a setting to the database

* Get commands and events to use database

* Setup and config command

* Update commands to check roles per server

* Different rules per server

Signed-off-by: Ethan Lane <ethan@vylpes.com>

* Different prefix per server

Signed-off-by: Ethan Lane <ethan@vylpes.com>

* Add verification system

Signed-off-by: Ethan Lane <ethan@vylpes.com>

* Disabled commands per server

* Add devmode for default prefix

* Update embeds

* Fix broken tests
2022-03-29 18:19:54 +01:00
c8edd1b4c5
Add moderator names to audit reason (#108) 2022-02-05 21:09:35 +00:00
de236dfd30
Containerise bot (#107) 2022-02-05 21:09:27 +00:00
97633451ed
Update rules with blog website and event spoilers rule" (#106)
Signed-off-by: Ethan Lane <ethan@vylpes.com>
2022-02-05 09:55:40 +00:00
f61c4c728a
Feature/12 create tests (#102)
* Fix tests

* Update coverage

* Remove unrequired mock files

* Add about command test

* Update about tests

* Ban command tests

* eval command tests

* Start help command tests

* Add help command tests

* Add kick command tests

* Mute command tests

* Poll command tests

* Add role command tests

Signed-off-by: Ethan Lane <ethan@vylpes.com>

* Add rules command tests

* Add unmute command tests

* Add warn command tests

* Add MemberEvents tests

* Add GuildMemberUpdate tests

Signed-off-by: Ethan Lane <ethan@vylpes.com>

* Add MessageEvents tests

* Add StringTools test

Signed-off-by: Ethan Lane <ethan@vylpes.com>

* Add embed tests

Signed-off-by: Ethan Lane <ethan@vylpes.com>

* Add GitHub Actions

Signed-off-by: Ethan Lane <ethan@vylpes.com>

* Move to tslint

Signed-off-by: Ethan Lane <ethan@vylpes.com>

* Remove tslint

Signed-off-by: Ethan Lane <ethan@vylpes.com>

* Remove linting script

Signed-off-by: Ethan Lane <ethan@vylpes.com>
2022-01-30 17:03:36 +00:00
2cc12d91be
Feature/74 merge vylbot core (#80)
* Merge VylBot-Core

* Update commands to new system

* Fix issue where events would not load
2021-12-24 14:55:28 +00:00
45d871fbf7
Merge pull request #79 from Vylpes/bug/16-event-doesnt-ignore-bots
Update messageDelete event to ignore bots
2021-12-04 16:51:51 +00:00
783c3a013d
Update messageDelete event to ignore bots 2021-12-04 16:47:43 +00:00
598a0b5a44
Merge pull request #73 from Vylpes/feature/23-migrate-to-typescript
Feature/23 migrate to typescript
2021-12-04 15:51:41 +00:00
68b9ed34e4
Update ErrorMessage to ChannelNotFound 2021-12-04 15:46:47 +00:00
ba51cbb28c
Update about command to use the PublicEmbed class 2021-12-04 15:42:58 +00:00
44571d735a
Migrate events to typescript 2021-12-02 15:38:38 +00:00
24818bcb44
Migrate rules command 2021-12-02 14:36:24 +00:00
6c90307754
Migrate help command 2021-12-02 14:09:05 +00:00
acedbffdad
Migrate eval command 2021-12-02 13:15:08 +00:00
c62488aa63
Migrate warn command 2021-12-02 13:02:13 +00:00
4ff88d0694
Migrate unmute command 2021-12-02 12:45:25 +00:00
07c7155027
Migrate role command 2021-12-02 11:45:02 +00:00
90ef4317cc
Update required roles checker 2021-12-02 11:44:56 +00:00
35f7210b6e
Migrate bunny command 2021-12-02 11:11:46 +00:00
e7169d960a
Migrate poll command 2021-11-29 15:34:57 +00:00
0d3134bf45
Migrate mute command 2021-11-29 15:06:13 +00:00
019966f25f
Migrate kick command 2021-11-29 11:51:54 +00:00
be329d709f
Migrate clear command 2021-11-29 11:46:16 +00:00
ecf9c5e4fc
Migrate ban command 2021-11-29 11:27:44 +00:00
bb433749f8
Migrate about command 2021-11-28 14:25:00 +00:00
c7417cf7a5
Migrate entry point 2021-11-28 14:24:53 +00:00
6fb2da2b18
Install packges and setup typescript 2021-11-28 14:24:37 +00:00
19065dc3e6
Merge pull request #63 from Vylpes/bug/28-role-configs
Add role configs to config template
2021-11-23 14:43:58 +00:00
ee7fe3fd19
Add role configs to config template 2021-11-23 11:41:40 +00:00
5b9aac22d3
Merge pull request #62 from Vylpes/feature/61-migrate-to-yarn
Migrate to yarn
2021-11-22 21:10:53 +00:00
38a5f6fb29
Migrate to yarn 2021-11-22 20:59:49 +00:00
Ethan Lane
021c495769 Merge branch 'main' into develop 2021-05-11 18:36:37 +01:00
Vylpes
9854f3c60f
Change rules.txt to rules.json (#31) 2021-04-25 16:50:27 +01:00
190 changed files with 11349 additions and 8624 deletions

View file

@ -1,72 +0,0 @@
---
kind: pipeline
name: deployment
steps:
- name: deploy
image: appleboy/drone-ssh
settings:
host: 192.168.1.115
username: vylpes
password:
from_secret: ssh_password
port: 22
script:
- sh /home/vylpes/scripts/vylbot/deploy_prod.sh
trigger:
event:
- tag
---
kind: pipeline
name: staging
steps:
- name: stage
image: appleboy/drone-ssh
settings:
host: 192.168.1.115
username: vylpes
password:
from_secret: ssh_password
port: 22
script:
- sh /home/vylpes/scripts/vylbot/deploy_stage.sh
trigger:
branch:
- develop
event:
- push
---
kind: pipeline
name: integration
steps:
- name: build
image: node
commands:
- yarn install --frozen-lockfile
- yarn build
# - name: test
# image: node
# commands:
# - yarn install --frozen-lockfile
# - yarn test
trigger:
branch:
- main
- develop
- hotfix/*
- feature/*
- renovate/*
event:
- push
- pull_request

View file

@ -1,28 +0,0 @@
# Security Warning! Do not commit this file to any VCS!
# This is a local file to speed up development process,
# so you don't have to change your environment variables.
#
# This is not applied to `.env.template`!
# Template files must be committed to the VCS, but must not contain
# any secret values.
BOT_TOKEN=
BOT_VER=3.3.0
BOT_AUTHOR=Vylpes
BOT_OWNERID=147392775707426816
BOT_CLIENTID=682942374040961060
ABOUT_FUNDING=https://ko-fi.com/vylpes
ABOUT_REPO=https://gitea.vylpes.xyz/RabbitLabs/vylbot-app
CACHE_INTERVAL=1800000 # 30 minutes
DB_HOST=127.0.0.1
DB_PORT=3101
DB_NAME=vylbot
DB_AUTH_USER=dev
DB_AUTH_PASS=dev
DB_SYNC=true
DB_LOGGING=true
DB_ROOT_HOST=0.0.0.0
DB_DATA_LOCATION=./.temp/database

13
.env.template Normal file
View file

@ -0,0 +1,13 @@
# Security Warning! Do not commit this file to any VCS!
# This is a local file to speed up development process,
# so you don't have to change your environment variables.
#
# This is not applied to `.env.template`!
# Template files must be committed to the VCS, but must not contain
# any secret values.
BOT_TOKEN=
BOT_VER=3.0
BOT_AUTHOR=Vylpes
BOT_DATE=28 Nov 2021
BOT_OWNERID=147392775707426816

View file

@ -1,67 +0,0 @@
name: Deploy To Production
on:
push:
branches:
- main
jobs:
build:
environment: prod
runs-on: node
steps:
- uses: actions/checkout@v4
- name: Use Node.js
uses: actions/setup-node@v4
with:
node-version: 20.x
- run: yarn install --frozen-lockfile
- run: yarn build
- run: yarn test
- name: "Copy files over to location"
run: rsync -rvzP --delete . ${{ secrets.PROD_REPO_PATH }}
deploy:
environment: prod
needs: build
runs-on: node
steps:
- uses: https://github.com/appleboy/ssh-action@v1.1.0
env:
DB_NAME: ${{ secrets.PROD_DB_NAME }}
DB_AUTH_USER: ${{ secrets.PROD_DB_AUTH_USER }}
DB_AUTH_PASS: ${{ secrets.PROD_DB_AUTH_PASS }}
DB_HOST: ${{ secrets.PROD_DB_HOST }}
DB_PORT: ${{ secrets.PROD_DB_PORT }}
DB_ROOT_HOST: ${{ secrets.PROD_DB_ROOT_HOST }}
DB_SYNC: ${{ secrets.PROD_DB_SYNC }}
DB_LOGGING: ${{ secrets.PROD_DB_LOGGING }}
DB_DATA_LOCATION: ${{ secrets.PROD_DB_DATA_LOCATION }}
SERVER_PATH: ${{ secrets.PROD_SSH_SERVER_PATH }}
BOT_TOKEN: ${{ secrets.PROD_BOT_TOKEN }}
BOT_VER: ${{ vars.PROD_BOT_VER }}
BOT_AUTHOR: ${{ vars.PROD_BOT_AUTHOR }}
BOT_OWNERID: ${{ vars.PROD_BOT_OWNERID }}
BOT_CLIENTID: ${{ vars.PROD_BOT_CLIENTID }}
ABOUT_FUNDING: ${{ vars.PROD_ABOUT_FUNDING }}
ABOUT_REPO: ${{ vars.PROD_ABOUT_REPO }}
CACHE_INTERVAL: ${{ vars.PROD_CACHE_INTERVAL }}
with:
host: ${{ secrets.PROD_SSH_HOST }}
username: ${{ secrets.PROD_SSH_USER }}
key: ${{ secrets.PROD_SSH_KEY }}
port: ${{ secrets.PROD_SSH_PORT }}
envs: DB_NAME,DB_AUTH_USER,DB_AUTH_PASS,DB_HOST,DB_PORT,DB_ROOT_HOST,DB_SYNC,DB_LOGGING,DB_DATA_LOCATION,BOT_TOKEN,BOT_VER,BOT_AUTHOR,BOT_OWNERID,BOT_CLIENTID,ABOUT_FUNDING,ABOUT_REPO,CACHE_INTERVAL
script: |
source .sshrc \
&& cd /home/vylpes/apps/vylbot/vylbot_prod \
&& docker compose down \
&& (pm2 stop vylbot_prod || true) \
&& (pm2 delete vylbot_prod || true) \
&& docker compose up -d \
&& sleep 10 \
&& yarn db:up \
&& pm2 start --name vylbot_prod dist/vylbot.js

View file

@ -1,69 +0,0 @@
name: Deploy To Stage
on:
push:
branches:
- develop
- hotfix/*
- release/*
jobs:
build:
environment: prod
runs-on: node
steps:
- uses: actions/checkout@v4
- name: Use Node.js
uses: actions/setup-node@v4
with:
node-version: 20.x
- run: yarn install --frozen-lockfile
- run: yarn build
- run: yarn test
- name: "Copy files over to location"
run: rsync -rvzP --delete . ${{ secrets.STAGE_REPO_PATH }}
deploy:
environment: prod
needs: build
runs-on: node
steps:
- uses: https://github.com/appleboy/ssh-action@v1.1.0
env:
DB_NAME: ${{ secrets.STAGE_DB_NAME }}
DB_AUTH_USER: ${{ secrets.STAGE_DB_AUTH_USER }}
DB_AUTH_PASS: ${{ secrets.STAGE_DB_AUTH_PASS }}
DB_HOST: ${{ secrets.STAGE_DB_HOST }}
DB_PORT: ${{ secrets.STAGE_DB_PORT }}
DB_ROOT_HOST: ${{ secrets.STAGE_DB_ROOT_HOST }}
DB_SYNC: ${{ secrets.STAGE_DB_SYNC }}
DB_LOGGING: ${{ secrets.STAGE_DB_LOGGING }}
DB_DATA_LOCATION: ${{ secrets.STAGE_DB_DATA_LOCATION }}
SERVER_PATH: ${{ secrets.STAGE_SSH_SERVER_PATH }}
BOT_TOKEN: ${{ secrets.STAGE_BOT_TOKEN }}
BOT_VER: ${{ github.ref_name }}
BOT_AUTHOR: ${{ vars.STAGE_BOT_AUTHOR }}
BOT_OWNERID: ${{ vars.STAGE_BOT_OWNERID }}
BOT_CLIENTID: ${{ vars.STAGE_BOT_CLIENTID }}
ABOUT_FUNDING: ${{ vars.STAGE_ABOUT_FUNDING }}
ABOUT_REPO: ${{ vars.STAGE_ABOUT_REPO }}
CACHE_INTERVAL: ${{ vars.STAGE_CACHE_INTERVAL }}
with:
host: ${{ secrets.STAGE_SSH_HOST }}
username: ${{ secrets.STAGE_SSH_USER }}
key: ${{ secrets.STAGE_SSH_KEY }}
port: ${{ secrets.STAGE_SSH_PORT }}
envs: DB_NAME,DB_AUTH_USER,DB_AUTH_PASS,DB_HOST,DB_PORT,DB_ROOT_HOST,DB_SYNC,DB_LOGGING,DB_DATA_LOCATION,BOT_TOKEN,BOT_VER,BOT_AUTHOR,BOT_OWNERID,BOT_CLIENTID,ABOUT_FUNDING,ABOUT_REPO,CACHE_INTERVAL
script: |
source .sshrc \
&& cd /home/vylpes/apps/vylbot/vylbot_stage \
&& docker compose down \
&& (pm2 stop vylbot_stage || true) \
&& (pm2 delete vylbot_stage || true) \
&& docker compose up -d \
&& sleep 10 \
&& yarn db:up \
&& pm2 start --name vylbot_stage dist/vylbot.js

View file

@ -1,23 +0,0 @@
name: Test
on:
push:
branches:
- feature/*
- renovate/*
jobs:
build:
environment: stage
runs-on: node
steps:
- uses: actions/checkout@v4
- name: Use Node.js
uses: actions/setup-node@v4
with:
node-version: 20.x
- run: yarn install --frozen-lockfile
- run: yarn build
- run: yarn test

View file

@ -1,18 +0,0 @@
Epic: \
Story Points:
---
*No description*
## Acceptance Criteria
*No acceptance criteria*
## Subtasks
*No subtasks*
## Notes
*No notes*

27
.github/workflows/testing.yml vendored Normal file
View file

@ -0,0 +1,27 @@
name: Testing
on:
pull_request:
branches:
- main
- develop
jobs:
build:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [12.x, 14.x, 16.x]
steps:
- uses: actions/checkout@v2
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v1
with:
node-version: ${{ matrix.node-version }}
- run: yarn install
- run: yarn build
- run: yarn test --coverage

3
.gitignore vendored
View file

@ -105,5 +105,4 @@ dist
config.json
.DS_Store
ormconfig.json
.temp/
ormconfig.json

0
.gitlab/.gitkeep Normal file
View file

View file

View file

@ -8,14 +8,14 @@ Fixes # (issue)
Please delete options that are not relevant.
- [ ] Bug fix (non-breaking change which fixes an issue)
- [ ] New feature (non-breaking change which adds functionality)
- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected)
- [ ] This change requires a documentation update
- Bug fix (non-breaking change which fixes an issue)
- New feature (non-breaking change which adds functionality)
- Breaking change (fix or feature that would cause existing functionality to not work as expected)
- This change requires a documentation update
# How Has This Been Tested?
Please describe the tests that you ran to verify the changes. Provide instructions so we can reproduce. Please also list any relevant details to your test configuration.
Please describe the tests that you ran to verify your changes. Provide instructions so we can reproduce. Please also list any relevant details for your test configuration
# Checklist
@ -24,6 +24,6 @@ Please describe the tests that you ran to verify the changes. Provide instructio
- [ ] I have commented my code, particularly in hard-to-understand areas
- [ ] I have made corresponding changes to the documentation
- [ ] My changes generate no new warnings
- [ ] I have added tests that provide my fix is effective or that my feature works
- [ ] I have added tests that prove my fix is effective or that my feature works
- [ ] New and existing unit tests pass locally with my changes
- [ ] Any dependent changes have been merged and published in downstream modules
- [ ] Any dependant changes have been merged and published in downstream modules

View file

@ -1,128 +0,0 @@
# Contributor Covenant Code of Conduct
## Our Pledge
We as members, contributors, and leaders pledge to make participation in our
community a harassment-free experience for everyone, regardless of age, body
size, visible or invisible disability, ethnicity, sex characteristics, gender
identity and expression, level of experience, education, socio-economic status,
nationality, personal appearance, race, religion, or sexual identity
and orientation.
We pledge to act and interact in ways that contribute to an open, welcoming,
diverse, inclusive, and healthy community.
## Our Standards
Examples of behavior that contributes to a positive environment for our
community include:
* Demonstrating empathy and kindness toward other people
* Being respectful of differing opinions, viewpoints, and experiences
* Giving and gracefully accepting constructive feedback
* Accepting responsibility and apologizing to those affected by our mistakes,
and learning from the experience
* Focusing on what is best not just for us as individuals, but for the
overall community
Examples of unacceptable behavior include:
* The use of sexualized language or imagery, and sexual attention or
advances of any kind
* Trolling, insulting or derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or email
address, without their explicit permission
* Other conduct which could reasonably be considered inappropriate in a
professional setting
## Enforcement Responsibilities
Community leaders are responsible for clarifying and enforcing our standards of
acceptable behavior and will take appropriate and fair corrective action in
response to any behavior that they deem inappropriate, threatening, offensive,
or harmful.
Community leaders have the right and responsibility to remove, edit, or reject
comments, commits, code, wiki edits, issues, and other contributions that are
not aligned to this Code of Conduct, and will communicate reasons for moderation
decisions when appropriate.
## Scope
This Code of Conduct applies within all community spaces, and also applies when
an individual is officially representing the community in public spaces.
Examples of representing our community include using an official e-mail address,
posting via an official social media account, or acting as an appointed
representative at an online or offline event.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported to the community leaders responsible for enforcement at
[helpdesk@vylpes.com](mailto:helpdesk@vylpes.com).
All complaints will be reviewed and investigated promptly and fairly.
All community leaders are obligated to respect the privacy and security of the
reporter of any incident.
## Enforcement Guidelines
Community leaders will follow these Community Impact Guidelines in determining
the consequences for any action they deem in violation of this Code of Conduct:
### 1. Correction
**Community Impact**: Use of inappropriate language or other behavior deemed
unprofessional or unwelcome in the community.
**Consequence**: A private, written warning from community leaders, providing
clarity around the nature of the violation and an explanation of why the
behavior was inappropriate. A public apology may be requested.
### 2. Warning
**Community Impact**: A violation through a single incident or series
of actions.
**Consequence**: A warning with consequences for continued behavior. No
interaction with the people involved, including unsolicited interaction with
those enforcing the Code of Conduct, for a specified period of time. This
includes avoiding interactions in community spaces as well as external channels
like social media. Violating these terms may lead to a temporary or
permanent ban.
### 3. Temporary Ban
**Community Impact**: A serious violation of community standards, including
sustained inappropriate behavior.
**Consequence**: A temporary ban from any sort of interaction or public
communication with the community for a specified period of time. No public or
private interaction with the people involved, including unsolicited interaction
with those enforcing the Code of Conduct, is allowed during this period.
Violating these terms may lead to a permanent ban.
### 4. Permanent Ban
**Community Impact**: Demonstrating a pattern of violation of community
standards, including sustained inappropriate behavior, harassment of an
individual, or aggression toward or disparagement of classes of individuals.
**Consequence**: A permanent ban from any sort of public interaction within
the community.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
version 2.0, available at
https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
Community Impact Guidelines were inspired by [Mozilla's code of conduct
enforcement ladder](https://github.com/mozilla/diversity).
[homepage]: https://www.contributor-covenant.org
For answers to common questions about this code of conduct, see the FAQ at
https://www.contributor-covenant.org/faq. Translations are available at
https://www.contributor-covenant.org/translations.

View file

@ -1,200 +0,0 @@
# Contributing to VylBot
First off, thanks for taking the time to contribute!
The following is a set of guidelines for contributing to VylBot. These are mostly guidelines, not rules. Use your best judgement, and feel free to propose changes to this document in a pull request.
## Code of Conduct
This project and everyone participating in it is governed by the VylBot Code of Conduct. By participating, you are expected to uphold this code.
## Questions about VylBot
> **Note:** Please don't file an issue to ask a question. You'll get faster results by using the resources below.
You can ask a question about the project in the `#development` channel in the [Discord Server](https://go.vylpes.xyz/A6HcA).
You can also email with queries and support if you'd prefer at [helpdesk@vylpes.com](mailto:helpdesk@vylpes.com).
## What you should know
### Javascript and Node
VylBot uses [NodeJS](https://nodejs.org/), and therefore Javascript, as its runtime. You should know how to use this.
## Conventions
There are a few conventions that have developed over time for this project. When you create a pull request a check will be ran making sure that your code follows these conventions.
We won't accept pull requests unless these checks pass. If yours fail, simply fix what the bot says until it passes and then get a repo member to review your code.
* Variable names should use **Camel Case**
* Functions should put **braces on the same line**
* **No comma dangle**, i.e. having a commma after the last item in an object
* **Arrow body style** should have braces around the body only when needed
* **Arrow parameters** should have brackets around them only when needed
* **Arrow spacing** should have a space around the arrow (' => ')
* **No var** should be used, instead use either let or const when appropriate
## How You Can Contribute
### Reporting Bugs
This section guides you through submitting a bug report for VylBot. Following these guidelines helps maintainers and the community understand your report. reproduce the behaviour, and find related reports.
When you are creating a bug report, please include as many details as possible.
> **Note:** If you find a **Closed** issue that seems like it is the same thing that you're experiencing, open a new issue and include a link to the original issue in the body of your new one.
#### Before Submitting A Bug Report
* **Perform a search** to see if the problem has already been reported. If it has **and the issue is still open**, add a comment to the existing issue instead of opening a new one.
#### How You Can Submit A (Good) Bug Report
Bugs are tracked as GitHub issues. After you've determined the bug you're reporting hasn't got a pre-existing **open** issue already, create an issue and provide information from below.
* **Use a clear and descriptive title** for the issue to indentify the problem.
* **Describe the exact steps which reproduce the problem** in as many details as possible. For example, start by explaining how you started VylBot (if you're using your own instance), which command exactly you used, and the output which the bot replied with. If its your own instance, provide information on what the terminal output said, if any.
* **Provide specific examples to demonstrate the steps**. Include links to files or GitHub projects, or copy/pastable snippets, which you use in those examples. If you're providing snippets in the issue, use Markdown code blocks.
* **Describe the behaviour you observed after following the steps** and point out what exactly is the problem with that behaviour.
* **Explain which behaviour you expected to see instead and why.**
* **Include screenshots and animated GIFs** which show you following the described steps and clearly demonstrate the problem.
* **If the problem wasn't triggered by a specific action**, describe what you were doing before the problem happened and share more information using the guidelines below.
Provide more context by answering these questions:
* **Did the problem start happening recently** (e.g. after updating to a new version of VylBot) or was this always a problem?
* If the problem started happening recently, **can you reproduce the problem in an older version of VylBot?** What's the most recently version in which the problem doesn't happen? You can download older versions of VylBot from the releases page.
* **Can you reliably reproduce the issue?** If not, provide details about how often the problem happens and under which conditions it normally happens.
Include details about your configuration and environment:
* **Which version of VylBot are you using?** You can get the exact version by running the `about` command.
* **What's the name and version of the OS you're using?**
* **Are you running VylBot in a virtual machine?** If so, which VM software are you using and which operating systems and versions are used for the host and the guest?
* **What version of node do you have installed?** You can get this version by running the `node -v` command in your terminal.
### Suggesting Enhancements
This section guides you through submitting an enhancement suggestion for VylBot, including completely new features and minor improvements to existing functionality. Following these guidelines helps maintainers and the community understand your suggestion and find related suggestions.
When you are creating an enhancement suggestion, please include as many details as possible. Fill out the suggestion with the steps that you imagine you would take if the feature you're requesting existed.
#### Before Submitting an Enhancement Suggestion
* **Check if the feature already exists.** Make sure to check on the latest version
* **Perform a search** to see if the enhancement has already been suggested. If it has, add a comment to the existing issue instead of opening a new one.
#### How Do I Submit A (Good) Enhancement Suggestion?
Enhancement suggestions are tracked as GitHub issues. After you've determined the feature doesn't already exist or been suggested before, create an issue on that repository and provide the following information:
* **Use a clear and descriptive title** for the issue to identify the suggestion.
* **Provide a step-by-step description of the suggested enhancement** in as many details as possible.
* **Provide specific examples to demonstrate the steps.** Include copy/pastable snippets which you use in those examples, as Markdown code blocks.
* **Describe the current behaviour** and **explain which behaviour you expected to see instead** and why.
* **Include screnshots and animated GIFs** which help you demonstrate the steps or point out the part of VylBot which the suggestion is related to.
* **Explain why this enhancement would be useful** to most VylBot users and isn't something that can or should be implemented as a custom command.
* **List some other bots where this enhancement exists.**
* **Specify which version of VylBot you're using.** You can get the exact version by running the `about` command.
* **Specify the name and version of the OS you're using.**
#### Prerequisites
In order to download necessary tools, clone the repository, and install dependencies via `yarn` you need network access.
You'll need the following tools:
* Git
* NodeJS
* Yarn
Install and build all of the dependencies using `yarn`
```bash
cd vylbot-app
yarn install
```
#### Build and Run
If you want to understand how VylBot works or want to debug an issue, you'll want to get the source, build it, and run the tool locally.
First, fork the VylBot repository so that you can make a pull request. Then, clone your fork locally:
```bash
git clone https://git.vylpes.xyz/<your-gitea-account>/vylbot-app.git
# OR
git clone https://github.com/vylpes/vylbot-app.git
```
Occasionally, you will want to merge changes in the upstream repository (the official code repo) with your fork.
```bash
cd vylbot-app
git checkout master
git pull https://git.vylpes.xyz/rabbitlabs/vylbot-app.git master
```
Manage any merge conflicts, commit them, and then push them to your fork.
Go into `vylbot-app` and build the package using `yarn build`.
#### Pull Requests
The process described here has several goals:
* Maintain VylBot's quality
* Fix problems that are important to users
* Engage the community in working toward the best possible VylBot
* Enable a sustainable system for VylBot's maintainers to review contributions
Please follow these steps to have your contribution considered by maintainers:
* You mention the issue id which this pull request aims to fix
* After you submit your pull request, verify that all status checks are passing.
> **Note**: If a check fails the pull request it is important that you go and fix these issues, or let us know that you no longer want to work on this issue by commenting on the pull request. Doing this will give you a better chance of having your pull request merged.
* When the checks have passed a maintainer will review your code and ask for any improvements or questions, and will merge it if they are satisifed.
While the prerequesites above must be satisifed prior to having your pull reuqest accepted, the reviewer(s) may ask you to complete additional design ork, tests, or other changes before your pull request can be ultimately accepted.
#### Submitting Changes via Email
If you're not within our forgejo instance and still like to contribute, you can send us your contributions to [git@vylpes.com](mailto:git@vylpes.com).
For more information on how to do this, see the [git documentation](https://git-scm.com/docs/git-send-email).
#### Submitting Changes via GitHub
This code is mirrored on GitHub, although main development is done on my self-hosted Forgejo instance, feel free to clone and create pull requests on there. I will merge it back into Forgejo once accepted.
#### JavaScript Styleguide
All JavaScript code is linted with `eslint`.
* Prefer camelcase for variable names
* Prefer braces `{` to be on the same line
* Prefer no comma `,` dangle
* Prefer arrow function bodies to have brances `{}` only when needed
* Prefer arrow function parameters to have brackets `()` only when needed
* Prefer arrow function arrows `=>` to have a space before and after it
* Prefer `let` and `const` over `var`
As well as eslint's recommended defaults.
Example
```js
function ban (member) {
let reason = "Example reason";
member.ban(reason).then(() => {
// handle then here
}).catch(err => {
// handle error here
});
}
```

15
Dockerfile Normal file
View file

@ -0,0 +1,15 @@
# Create app and work directory
FROM node:16
WORKDIR /vylbot
# Install dependencies
COPY package.json .
COPY yarn.lock .
RUN yarn install
# Bundle app source
COPY . .
RUN yarn build
# Run the app source
CMD [ "yarn", "start" ]

21
LICENSE
View file

@ -1,21 +0,0 @@
MIT License
Copyright (c) 2025 Vylpes
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View file

@ -4,13 +4,13 @@ Discord bot for Vylpes' Den Discord Server.
## Installation
Download the latest version from the [releases page](https://git.vylpes.xyz/rabbitlabs/vylbot-app/releases).
Download the latest version from the [releases page](https://github.com/Vylpes/vylbot-app/releases).
Copy the config template file and fill in the strings.
## Requirements
- NodeJS v20
- NodeJS v16
- Yarn
## Usage
@ -22,10 +22,10 @@ yarn install
yarn build
```
Setup the database (Recommended to use the docker compose file)
Setup the database (Recommended to use the docker-compose file)
```bash
docker compose up -d
docker-compose up -d
```
Copy and edit the settings files
@ -33,6 +33,9 @@ Copy and edit the settings files
```bash
cp .env.template .env
# Edit the .env file
cp ormconfig.json.template ormconfig.json
# Edit the ormconfig.json file
```
> **NOTE:** Make sure you do *not* check in these files! These contain sensitive information and should be treated as private.
@ -41,4 +44,12 @@ Start the bot
```bash
yarn start
```
```
Alternatively, you can start the bot in development mode using:
```bash
yarn start --dev
```
> Dev mode ensures that the default prefix is different to the production mode, in case you have both running in the same server.

22
data/config.txt Normal file
View file

@ -0,0 +1,22 @@
USAGE: <key> <set|reset> [value]
===[ KEYS ]===
bot.prefix: The bot prefix for the server (Default: "v!")
commands.disabled: Disabled commands, separated by commas (Default: "")
role.assignable: List of roles assignable to user (Default: [])
role.moderator: The moderator role name (Default: "Moderator")
role.administrator: The administrator role name (Default: "Administrator")
role.muted: The muted role name (Default: "Muted")
rules.file: The location of the rules file (Default: "data/rules/rules")
channels.logs.message: The channel message events will be logged to (Default: "message-logs")
channels.logs.member: The channel member events will be logged to (Default: "member-logs")
channels.logs.mod: The channel mod events will be logged to (Default: "mod-logs")
verification.enabled: Enables/Disables the verification feature (Default: "false")
verification.channel: The channel to listen to for entry codes (Default: "entry")
verification.role: The server access role (Default: "Entry")
verification.code: The entry code for the channel (Default: "")

8
data/lobbyConfig.txt Normal file
View file

@ -0,0 +1,8 @@
USAGE: config <add|remove> <Channel Name> <Role Name> [cooldown] [Game Name]
===[ EXAMPLE ]===
To add a channel:
- config add game-name role-name 30 Game Name
To remove a channel:
- config remove game-name

View file

@ -6,7 +6,7 @@
"title": "Vylpes' Den",
"description": [
"Welcome to Vylpes' Den! Make sure to say hi!",
"Invite link: https://go.vylpes.xyz/A6HcA"
"Invite link: https://discord.gg/UyAhAVp"
]
},
{
@ -70,17 +70,19 @@
"description": [
"This server uses a bot made by me, VylBot, to help moderate the server.",
"For more information on it, see the GitHub repositories:",
"https://gitea.vylpes.xyz/rabbitlabs/vylbot-app"
"https://github.com/Vylpes/vylbot-core",
"https://github.com/Vylpes/vylbot-app"
]
},
{
"title": "Links",
"description": [
"YouTube: https://www.youtube.com/@vylpes",
"YouTube: https://www.youtube.com/channel/UCwPlzKwCmP5Q9bCX3fHk2BA",
"Patreon: https://www.patreon.com/vylpes",
"Twitch: https://www.twitch.tv/vylpes_",
"Twitter: https://twitter.com/vylpes"
"Twitter: https://twitter.com/vylpes",
"Blog: https://vylpes.xyz"
],
"footer": "Last updated 20/06/2023"
"footer": "Last updated 01/02/2022"
}
]

View file

@ -1,12 +1,88 @@
[
{
"title": "Welcome to Mankalor's Discord Server!",
"image": "https://i.imgur.com/bjH1gza.png"
},
{
"title": "Bot Testing Ground",
"description": [
"*You must follow Discord's TOS, including the rule where", "you must be 13 years or older.",
"If moderators know you're under 13, we will have to ban you!*",
"",
"You need to input a code in *#entry* which is somewhere in this message before you can start chatting, so read the server rules and info below.",
"If you still don't see the other channels after writing this code, message a moderator. For any issues with this bot, message <@147392775707426816>."
"Welcome to Vylpes' Den! Make sure to say hi!",
"Invite link: https://discord.gg/UyAhAVp"
]
},
{
"title": "Discord TOS",
"description": [
"All servers are required to follow the Discord Terms of Service. This includes minimum age requirements (13+). If the moderation team discover a breach of TOS we are required by discord to ban. Make sure you know them!",
"https://discord.com/terms"
]
},
{
"title": "Rules",
"description": [
"**English Only**",
"In order for everyone to understand each other we would like to ask everyone to speak in English only.",
"",
"**No NSFW or Obscene Content**",
"This includes text, images, or links featuring nudity, sex, hard violence, or other graphically disturbing content.",
"",
"**Treat Everyone with Respect**",
"Absolutely no harassment, witch hunting, sexism, racism, or hate speech will be tolerated.",
"",
"**No spam or self promotion**",
"Outside of #self-promo. This includes DMing fellow members.",
"",
"**Keep Politics to #general**",
"And make sure it doesn't become too heated. Debate don't argue.",
"",
"**Drama From Other Servers**",
"Please don't bring up drama from other servers, keep that to DMs",
"",
"**Bot Abuse**",
"Don't abuse the bots or you will be blocked from using them",
"",
"**Event Spoilers**",
"Contents of events and keynotes, such as the Nintendo Direct, must be spoken about in events, this rule applies for up to 24 hours after the event ends. Even though we will only enforce talking there for a set time, please be considerate of those who haven't watched the event yet."
]
},
{
"title": "Moderators Discretion",
"description": [
"Don't argue with a mod's decision. A moderator's choice is final. If you have an issue with a member of the mod team DM me (Vylpes#0001)."
]
},
{
"title": "Supporters",
"description": [
"If you are a Twitch Subscriber or a Patreon Member and have linked your profiles to your discord account you will get exclusive access to the Vylpes Plus channels, including early access to videos!"
]
},
{
"title": "Self-Assignable Roles",
"description": [
"If you want to assign yourself roles, go to #bot-stuff and type v!role <role>. The current roles you can get are:",
"Notify: Get pinged when a new stream or video releases.",
"VotePings: Get pinged when I start a new poll",
"ProjectUpdates: Get pinged when I update my projects as well as new for them"
]
},
{
"title": "VylBot",
"description": [
"This server uses a bot made by me, VylBot, to help moderate the server.",
"For more information on it, see the GitHub repositories:",
"https://github.com/Vylpes/vylbot-core",
"https://github.com/Vylpes/vylbot-app"
]
},
{
"title": "Links",
"description": [
"YouTube: https://www.youtube.com/channel/UCwPlzKwCmP5Q9bCX3fHk2BA",
"Patreon: https://www.patreon.com/vylpes",
"Twitch: https://www.twitch.tv/vylpes_",
"Twitter: https://twitter.com/vylpes",
"Blog: https://vylpes.xyz"
],
"footer": "Last updated 01/02/2022"
}
]

View file

@ -6,7 +6,7 @@
"If moderators know you're under 13, we will have to ban you!*",
"",
"You need to input a code in *#entry* which is somewhere in this message before you can start chatting, so read the server rules and info below.",
"If you still don't see the other channels after writing this code, message a moderator. For any issues with this bot, message <@147392775707426816>."
"If you still don't see the other channels after writing this code, message a moderator. For any issues with this bot, message Vylpes#5725."
]
},
{

View file

@ -1,35 +0,0 @@
USAGE: <key> <set|reset> [value]
===[ KEYS ]===
bot.prefix: The bot prefix for the server (Default: "v!")
commands.disabled: Disabled commands, separated by commas (Default: "")
role.moderator: The moderator role name (Default: "Moderator")
role.administrator: The administrator role name (Default: "Administrator")
rules.file: The location of the rules file (Default: "data/rules/rules")
channels.logs.message: The channel message events will be logged to (Default: "message-logs")
channels.logs.member: The channel member events will be logged to (Default: "member-logs")
channels.logs.mod: The channel mod events will be logged to (Default: "mod-logs")
verification.enabled: Enables/Disables the verification feature (Default: "false")
verification.channel: The channel to listen to for entry codes (Default: "entry")
verification.role: The server access role (Default: "Entry")
verification.code: The entry code for the channel (Default: "")
event.message.delete.enabled: Enables/Disables the message delete log event (Default: "false")
event.message.delete.channel: Sets the channel the bot will log message delete events to (Default: "message-logs")
event.message.update.enabled: Enables/Disables the message delete log event (Default: "false")
event.message.update.channel: Sets the channel the bot will log message delete events to (Default: "message-logs")
event.member.add.enabled: Enables/Disables the member join log event (Default: "false")
event.member.add.channel: Sets the channel the bot will log member join events to (Default: "member-logs")
event.member.remove.enabled: Enables/Disables the member leave log event (Default: "false")
event.member.remove.channel: Sets the channel the bot will log member leave events to (Default: "member-logs")
event.member.update.enabled: Enables/Disables the member update log event (Default: "false")
event.member.update.channel: Sets the channel the bot will log member update events to (Default: "member-logs")

View file

@ -1,8 +0,0 @@
USAGE: config <add|remove> <Channel ID> <Role ID> [cooldown] [Game Name]
===[ EXAMPLE ]===
To add a channel:
- config add 000000000000000000 000000000000000000 30 Game Name
To remove a channel:
- config remove 000000000000000000

View file

@ -1,8 +0,0 @@
USAGE: config <add|remove> <Role ID>
===[ EXAMPLE ]===
To add a role:
- config add 000000000000000000
To remove a role:
- config remove 000000000000000000

View file

@ -1,11 +0,0 @@
CREATE TABLE `audit` (
`Id` varchar(255) NOT NULL,
`WhenCreated` datetime NOT NULL,
`WhenUpdated` datetime NOT NULL,
`AuditId` varchar(255) NOT NULL,
`UserId` varchar(255) NOT NULL,
`AuditType` int NOT NULL,
`Reason` varchar(255) NOT NULL,
`ModeratorId` varchar(255) NOT NULL,
`ServerId` varchar(255) NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;

View file

@ -1,5 +0,0 @@
CREATE TABLE `ignored_channel` (
`Id` varchar(255) NOT NULL,
`WhenCreated` datetime NOT NULL,
`WhenUpdated` datetime NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;

View file

@ -1,10 +0,0 @@
CREATE TABLE `lobby` (
`Id` varchar(255) NOT NULL,
`WhenCreated` datetime NOT NULL,
`WhenUpdated` datetime NOT NULL,
`ChannelId` varchar(255) NOT NULL,
`RoleId` varchar(255) NOT NULL,
`Cooldown` int NOT NULL,
`LastUsed` datetime NOT NULL,
`Name` varchar(255) NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;

View file

@ -1,7 +0,0 @@
CREATE TABLE `role` (
`Id` varchar(255) NOT NULL,
`WhenCreated` datetime NOT NULL,
`WhenUpdated` datetime NOT NULL,
`RoleId` varchar(255) NOT NULL,
`serverId` varchar(255) DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;

View file

@ -1,5 +0,0 @@
CREATE TABLE `server` (
`Id` varchar(255) NOT NULL,
`WhenCreated` datetime NOT NULL,
`WhenUpdated` datetime NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;

View file

@ -1,8 +0,0 @@
CREATE TABLE `setting` (
`Id` varchar(255) NOT NULL,
`WhenCreated` datetime NOT NULL,
`WhenUpdated` datetime NOT NULL,
`Key` varchar(255) NOT NULL,
`Value` varchar(255) NOT NULL,
`serverId` varchar(255) DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;

View file

@ -1,2 +0,0 @@
ALTER TABLE `audit`
ADD PRIMARY KEY (`Id`);

View file

@ -1,2 +0,0 @@
ALTER TABLE `ignored_channel`
ADD PRIMARY KEY (`Id`);

View file

@ -1,2 +0,0 @@
ALTER TABLE `lobby`
ADD PRIMARY KEY (`Id`);

View file

@ -1,3 +0,0 @@
ALTER TABLE `role`
ADD PRIMARY KEY (`Id`),
ADD KEY `FK_d9e438d88cfb64f7f8e1ae593c3` (`serverId`);

View file

@ -1,2 +0,0 @@
ALTER TABLE `server`
ADD PRIMARY KEY (`Id`);

View file

@ -1,3 +0,0 @@
ALTER TABLE `setting`
ADD PRIMARY KEY (`Id`),
ADD KEY `FK_a3623ec541bdb12fa0f58bdfde7` (`serverId`);

View file

@ -1,2 +0,0 @@
ALTER TABLE `role`
ADD CONSTRAINT `FK_d9e438d88cfb64f7f8e1ae593c3` FOREIGN KEY (`serverId`) REFERENCES `server` (`Id`);

View file

@ -1,2 +0,0 @@
ALTER TABLE `setting`
ADD CONSTRAINT `FK_a3623ec541bdb12fa0f58bdfde7` FOREIGN KEY (`serverId`) REFERENCES `server` (`Id`);

View file

@ -1,2 +0,0 @@
ALTER TABLE server
ADD LastCached datetime NOT NULL DEFAULT '2024-03-01 18:10:04';

View file

@ -1 +0,0 @@
DROP TABLE auto_kick_config;

View file

@ -1,10 +0,0 @@
CREATE TABLE auto_kick_config (
Id varchar(255) NOT NULL,
WhenCreated datetime NOT NULL,
WhenUpdated datetime NOT NULL,
ServerId varchar(255) NOT NULL,
RoleId varchar(255) NOT NULL,
KickTime int NOT NULL,
NoticeTime int NULL,
NoticeChannelId varchar(255) NULL
);

View file

@ -1,2 +0,0 @@
ALTER TABLE auto_kick_config
ADD PRIMARY KEY (Id);

View file

@ -1,10 +0,0 @@
CREATE TABLE `moon` (
`Id` varchar(255) NOT NULL,
`WhenCreated` datetime NOT NULL,
`WhenUpdated` datetime NOT NULL,
`MoonNumber` int NOT NULL,
`UserId` varchar(255) NOT NULL,
`Description` varchar(255) NOT NULL,
`WhenArchived` datetime NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;

View file

@ -1,8 +0,0 @@
CREATE TABLE `user_setting` (
`Id` varchar(255) NOT NULL,
`WhenCreated` datetime NOT NULL,
`WhenUpdated` datetime NOT NULL,
`UserId` varchar(255) NOT NULL,
`Key` varchar(255) NOT NULL,
`Value` varchar(255) NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;

View file

@ -1,2 +0,0 @@
ALTER TABLE user_setting
ADD PRIMARY KEY (Id);

View file

@ -1,17 +1,24 @@
version: "3.9"
services:
# discord:
# build: .
database:
image: mysql/mysql-server
command: --default-authentication-plugin=mysql_native_password
restart: always
environment:
- MYSQL_DATABASE=$DB_NAME
- MYSQL_USER=$DB_AUTH_USER
- MYSQL_PASSWORD=$DB_AUTH_PASS
- MYSQL_ROOT_PASSWORD=$DB_AUTH_PASS
- MYSQL_ROOT_HOST=$DB_ROOT_HOST
- MYSQL_DATABASE=vylbot
- MYSQL_USER=dev
- MYSQL_PASSWORD=dev
- MYSQL_ROOT_PASSWORD=root
ports:
- "$DB_PORT:3306"
volumes:
- $DB_DATA_LOCATION:/var/lib/mysql
- 3306:3306
phpmyadmin:
image: phpmyadmin
restart: always
ports:
- 8080:80
environment:
- PMA_ARBITRARY=1

24
ormconfig.json.template Normal file
View file

@ -0,0 +1,24 @@
{
"type": "mysql",
"host": "localhost",
"port": 3306,
"username": "dev",
"password": "dev",
"database": "vylbot",
"synchronize": true,
"logging": false,
"entities": [
"dist/entity/**/*.js"
],
"migrations": [
"dist/migration/**/*.js"
],
"subscribers": [
"dist/subscriber/**/*.js"
],
"cli": {
"entitiesDir": "dist/entity",
"migrationsDir": "dist/migration",
"subscribersDir": "dist/subscriber"
}
}

View file

@ -1,52 +1,38 @@
{
"name": "vylbot-app",
"version": "3.2.4",
"version": "3.0",
"description": "A discord bot made for Vylpes' Den",
"main": "./dist/vylbot",
"typings": "./dist",
"scripts": {
"clean": "rm -rf node_modules/ dist/",
"build": "tsc",
"start": "node ./dist/vylbot",
"test": "jest",
"db:up": "typeorm migration:run -d dist/database/dataSources/appDataSource.js",
"db:down": "typeorm migration:revert -d dist/database/dataSources/appDataSource.js",
"db:create": "typeorm migration:create ./src/database/migrations",
"release": "np --no-publish"
"test": "jest"
},
"repository": {
"type": "git",
"url": "https://github.com/Vylpes/vylbot-app"
"url": "git+https://github.com/Vylpes/vylbot-app.git"
},
"author": "Vylpes",
"license": "MIT",
"bugs": {
"url": "https://github.com/Vylpes/vylbot-app/issues",
"email": "helpdesk@vylpes.com"
},
"bugs": "https://github.com/Vylpes/vylbot-app/issues",
"homepage": "https://github.com/Vylpes/vylbot-app",
"funding": "https://ko-fi.com/vylpes",
"dependencies": {
"@discordjs/rest": "^2.0.0",
"@types/uuid": "^10.0.0",
"cron": "^3.3.1",
"discord.js": "^14.3.0",
"dotenv": "^16.0.0",
"emoji-regex": "^10.0.0",
"minimatch": "10.0.1",
"@types/jest": "^27.0.3",
"@types/uuid": "^8.3.4",
"discord.js": "12.5.3",
"dotenv": "^10.0.0",
"emoji-regex": "^9.2.0",
"jest": "^27.4.5",
"jest-mock-extended": "^2.0.4",
"mysql": "^2.18.1",
"random-bunny": "^2.1.6",
"typeorm": "^0.3.20"
},
"resolutions": {
"random-bunny": "^2.0.0",
"ts-jest": "^27.1.2",
"typeorm": "^0.2.44",
"uuid": "^8.3.2"
},
"devDependencies": {
"@types/jest": "^29.5.12",
"@types/node": "^22.0.0",
"jest": "^29.7.0",
"jest-mock-extended": "^3.0.7",
"np": "^10.0.0",
"ts-jest": "^29.2.4",
"typescript": "^5.0.0"
"@types/node": "^16.11.10",
"typescript": "^4.5.2"
}
}

View file

@ -1,4 +0,0 @@
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
"baseBranches": ["develop"]
}

View file

@ -1,23 +0,0 @@
#! /bin/bash
export PATH="$HOME/.yarn/bin:$PATH"
export PATH="$HOME/.nodeuse/bin:$PATH"
export BOT_TOKEN=$(cat $HOME/scripts/vylbot/prod_key.txt)
cd ~/apps/vylbot/vylbot_prod \
&& git checkout main \
&& git fetch \
&& git pull \
&& docker compose --file docker-compose.prod.yml down \
&& (pm2 stop vylbot_prod || true) \
&& (pm2 delete vylbot_prod || true) \
&& cp .prod.env .env \
&& yarn clean \
&& yarn install --frozen-lockfile \
&& yarn build \
&& docker compose --file docker-compose.prod.yml up -d \
&& echo "Sleeping for 10 seconds to let database load..." \
&& sleep 10 \
&& yarn run db:up \
&& NODE_ENV=production pm2 start --name vylbot_prod dist/vylbot.js

View file

@ -1,23 +0,0 @@
#! /bin/bash
export PATH="$HOME/.yarn/bin:$PATH"
export PATH="$HOME/.nodeuse/bin:$PATH"
export BOT_TOKEN=$(cat $HOME/scripts/vylbot/stage_key.txt)
cd ~/apps/vylbot/vylbot_stage \
&& git checkout develop \
&& git fetch \
&& git pull \
&& docker compose --file docker-compose.stage.yml down \
&& (pm2 stop vylbot_stage || true) \
&& (pm2 delete vylbot_stage || true) \
&& cp .stage.env .env \
&& yarn clean \
&& yarn install --frozen-lockfile \
&& yarn build \
&& docker compose --file docker-compose.stage.yml up -d \
&& echo "Sleeping for 10 seconds to let database load..." \
&& sleep 10 \
&& yarn run db:up \
&& NODE_ENV=production pm2 start --name vylbot_stage dist/vylbot.js

View file

@ -1,14 +0,0 @@
import {ButtonInteraction} from "discord.js";
import {ButtonEvent} from "../../type/buttonEvent";
import List from "./moons/list";
export default class Moons extends ButtonEvent {
public override async execute(interaction: ButtonInteraction): Promise<void> {
const action = interaction.customId.split(" ")[1];
switch (action) {
case "list":
await List(interaction);
}
}
}

View file

@ -1,68 +0,0 @@
import {ActionRowBuilder, ButtonBuilder, ButtonInteraction, ButtonStyle, EmbedBuilder} from "discord.js";
import Moon from "../../../database/entities/304276391837302787/Moon";
import EmbedColours from "../../../constants/EmbedColours";
import UserSetting from "../../../database/entities/UserSetting";
export default async function List(interaction: ButtonInteraction) {
if (!interaction.guild) return;
const userId = interaction.customId.split(" ")[2];
const page = interaction.customId.split(" ")[3];
if (!userId || !page) return;
const pageNumber = Number(page);
const member = interaction.guild.members.cache.find(x => x.user.id == userId) || await interaction.guild.members.fetch(userId);
const pageLength = 10;
const moons = await Moon.FetchPaginatedMoonsByUserId(userId, pageLength, pageNumber);
if (!moons) {
await interaction.reply(`${member?.user.username ?? "This user"} does not have any moons or page is invalid.`);
return;
}
const moonSetting = await UserSetting.FetchOneByKey(userId, "moons");
const totalMoons = moonSetting && Number(moonSetting.Value) ? Number(moonSetting.Value) : 0;
const totalPages = Math.ceil(moons[1] / pageLength);
let description = ["*none*"];
if (moons[0].length > 0) {
description = moons[0].flatMap(x => `**${x.MoonNumber} -** ${x.Description.slice(0, 15)}`);
}
const moonDifference = totalMoons - moons[1];
const isLastPage = pageNumber + 1 == totalPages || moons[0].length == 0;
if (isLastPage && moonDifference > 0) {
description.push(`...plus ${moonDifference} more untracked`);
}
const embed = new EmbedBuilder()
.setTitle(`${member.user.username}'s Moons`)
.setColor(EmbedColours.Ok)
.setDescription(description.join("\n"))
.setFooter({ text: `Page ${pageNumber + 1} of ${totalPages} · ${totalMoons} moons` });
const row = new ActionRowBuilder<ButtonBuilder>()
.addComponents(
new ButtonBuilder()
.setCustomId(`moons list ${userId} ${pageNumber - 1}`)
.setLabel("Previous")
.setStyle(ButtonStyle.Primary)
.setDisabled(pageNumber == 0),
new ButtonBuilder()
.setCustomId(`moons list ${userId} ${pageNumber + 1}`)
.setLabel("Next")
.setStyle(ButtonStyle.Primary)
.setDisabled(isLastPage));
await interaction.update({
embeds: [ embed ],
components: [ row ],
});
}

View file

@ -1,42 +0,0 @@
import { ButtonInteraction, CacheType } from "discord.js";
import { ButtonEvent } from "../type/buttonEvent";
import SettingsHelper from "../helpers/SettingsHelper";
export default class Verify extends ButtonEvent {
public override async execute(interaction: ButtonInteraction<CacheType>) {
if (!interaction.guildId || !interaction.guild) return;
const roleName = await SettingsHelper.GetSetting("verification.role", interaction.guildId);
if (!roleName) return;
const role = interaction.guild.roles.cache.find(x => x.name == roleName);
if (!role) {
await interaction.reply({
content: `Unable to find the role, ${roleName}`,
ephemeral: true,
});
return;
}
const member = interaction.guild.members.cache.find(x => x.id == interaction.user.id);
if (!member || !member.manageable) {
await interaction.reply({
content: "Unable to give role to user",
ephemeral: true,
});
return;
}
await member.roles.add(role);
await interaction.reply({
content: "Given role",
ephemeral: true,
});
}
}

View file

@ -1,56 +1,41 @@
import { Client, Partials } from "discord.js";
import { Client } from "discord.js";
import * as dotenv from "dotenv";
import { EventType } from "../constants/EventType";
import { createConnection } from "typeorm";
import DefaultValues from "../constants/DefaultValues";
import ICommandItem from "../contracts/ICommandItem";
import IEventItem from "../contracts/IEventItem";
import { Command } from "../type/command";
import { Event } from "../type/event";
import { Events } from "./events";
import { Util } from "./util";
import AppDataSource from "../database/dataSources/appDataSource";
import ButtonEventItem from "../contracts/ButtonEventItem";
import { ButtonEvent } from "../type/buttonEvent";
import CacheHelper from "../helpers/CacheHelper";
import TimerHelper from "../helpers/TimerHelper";
import AutoKick from "../timers/AutoKick";
export class CoreClient extends Client {
private static _commandItems: ICommandItem[];
private static _eventItems: IEventItem[];
private static _buttonEvents: ButtonEventItem[];
private static _baseClient: Client;
private _commandItems: ICommandItem[];
private _eventItems: IEventItem[];
private _events: Events;
private _util: Util;
private _timerHelper: TimerHelper;
public static get commandItems(): ICommandItem[] {
public get commandItems(): ICommandItem[] {
return this._commandItems;
}
public static get eventItems(): IEventItem[] {
public get eventItems(): IEventItem[] {
return this._eventItems;
}
public static get buttonEvents(): ButtonEventItem[] {
return this._buttonEvents;
}
public static get baseClient(): Client {
return this._baseClient;
}
constructor(intents: number[], partials: Partials[]) {
super({ intents: intents, partials: partials });
constructor(devmode: boolean = false) {
super();
dotenv.config();
CoreClient._commandItems = [];
CoreClient._eventItems = [];
CoreClient._buttonEvents = [];
DefaultValues.useDevPrefix = devmode;
this._commandItems = [];
this._eventItems = [];
this._events = new Events();
this._util = new Util();
this._timerHelper = new TimerHelper();
}
public async start() {
@ -59,56 +44,34 @@ export class CoreClient extends Client {
return;
}
CoreClient._baseClient = this;
await AppDataSource.initialize()
.then(() => {
console.log("Data Source Initialized");
this._timerHelper.AddTimer("0 * * * *", "Europe/London", AutoKick, false);
this._timerHelper.StartAllTimers();
})
.catch((err) => console.error("Error Initialising Data Source", err));
super.on("interactionCreate", this._events.onInteractionCreate);
super.on("ready", this._events.onReady);
await super.login(process.env.BOT_TOKEN);
this.guilds.cache.forEach(async (guild) => {
await CacheHelper.UpdateServerCache(guild);
await createConnection().catch(e => {
console.error(e);
return;
});
this._util.loadEvents(this, CoreClient._eventItems);
this._util.loadSlashCommands(this);
super.on("message", (message) => this._events.onMessage(message, this._commandItems));
super.on("ready", this._events.onReady);
super.login(process.env.BOT_TOKEN);
this._util.loadEvents(this, this._eventItems);
}
public static RegisterCommand(name: string, command: Command, serverId?: string) {
public RegisterCommand(name: string, command: Command, serverId?: string) {
const item: ICommandItem = {
Name: name,
Command: command,
ServerId: serverId,
};
CoreClient._commandItems.push(item);
this._commandItems.push(item);
}
public static RegisterEvent(eventType: EventType, func: Function) {
public RegisterEvent(event: Event) {
const item: IEventItem = {
EventType: eventType,
ExecutionFunction: func,
};
CoreClient._eventItems.push(item);
}
public static RegisterButtonEvent(buttonId: string, event: ButtonEvent) {
const item: ButtonEventItem = {
ButtonId: buttonId,
Event: event,
};
CoreClient._buttonEvents.push(item);
this._eventItems.push(item);
}
}

View file

@ -1,17 +1,32 @@
import { Interaction } from "discord.js";
import ChatInputCommand from "./interactionCreate/chatInputCommand";
import Button from "./interactionCreate/button";
import { Message } from "discord.js";
import ICommandItem from "../contracts/ICommandItem";
import SettingsHelper from "../helpers/SettingsHelper";
import { Util } from "./util";
export class Events {
public async onInteractionCreate(interaction: Interaction) {
if (!interaction.guildId) return;
private _util: Util;
if (interaction.isChatInputCommand()) {
ChatInputCommand.onChatInput(interaction);
}
constructor() {
this._util = new Util();
}
if (interaction.isButton()) {
Button.onButtonClicked(interaction);
// Emit when a message is sent
// Used to check for commands
public async onMessage(message: Message, commands: ICommandItem[]) {
if (!message.guild) return;
if (message.author.bot) return;
const prefix = await SettingsHelper.GetSetting("bot.prefix", message.guild.id);
if (!prefix) return;
if (message.content.substring(0, prefix.length).toLowerCase() == prefix.toLowerCase()) {
const args = message.content.substring(prefix.length).split(" ");
const name = args.shift();
if (!name) return;
await this._util.loadCommand(name, args, message, commands);
}
}

View file

@ -1,17 +0,0 @@
import { ButtonInteraction } from "discord.js";
import { CoreClient } from "../client";
export default class Button {
public static async onButtonClicked(interaction: ButtonInteraction) {
if (!interaction.isButton) return;
const item = CoreClient.buttonEvents.find(x => x.ButtonId == interaction.customId.split(" ")[0]);
if (!item) {
await interaction.reply("Event not found.");
return;
}
item.Event.execute(interaction);
}
}

View file

@ -1,27 +0,0 @@
import { Interaction } from "discord.js";
import { CoreClient } from "../client";
import ICommandItem from "../../contracts/ICommandItem";
export default class ChatInputCommand {
public static async onChatInput(interaction: Interaction) {
if (!interaction.isChatInputCommand()) return;
const item = CoreClient.commandItems.find(x => x.Name == interaction.commandName && !x.ServerId);
const itemForServer = CoreClient.commandItems.find(x => x.Name == interaction.commandName && x.ServerId == interaction.guildId);
let itemToUse: ICommandItem;
if (!itemForServer) {
if (!item) {
await interaction.reply("Command not found.");
return;
}
itemToUse = item;
} else {
itemToUse = itemForServer;
}
itemToUse.Command.execute(interaction);
}
}

View file

@ -1,95 +1,99 @@
import { Client, REST, Routes, SlashCommandBuilder } from "discord.js";
import { EventType } from "../constants/EventType";
// Required Components
import { Client, Message } from "discord.js";
import { ICommandContext } from "../contracts/ICommandContext";
import ICommandItem from "../contracts/ICommandItem";
import IEventItem from "../contracts/IEventItem";
import { CoreClient } from "./client";
import SettingsHelper from "../helpers/SettingsHelper";
import StringTools from "../helpers/StringTools";
import { CommandResponse } from "../constants/CommandResponse";
import ErrorMessages from "../constants/ErrorMessages";
// Util Class
export class Util {
public loadSlashCommands(client: Client) {
const registeredCommands = CoreClient.commandItems;
public async loadCommand(name: string, args: string[], message: Message, commands: ICommandItem[]) {
if (!message.member) return;
if (!message.guild) return;
const globalCommands = registeredCommands.filter(x => !x.ServerId);
const guildCommands = registeredCommands.filter(x => x.ServerId);
const disabledCommandsString = await SettingsHelper.GetSetting("commands.disabled", message.guild?.id);
const disabledCommands = disabledCommandsString?.split(",");
const globalCommandData: SlashCommandBuilder[] = globalCommands
.filter(x => x.Command.CommandBuilder)
.flatMap(x => x.Command.CommandBuilder);
const guildIds: string[] = [];
for (let command of guildCommands) {
if (!guildIds.find(x => x == command.ServerId)) {
guildIds.push(command.ServerId!);
}
if (disabledCommands?.find(x => x == name)) {
message.reply(process.env.COMMANDS_DISABLED_MESSAGE || "This command is disabled.");
return;
}
const rest = new REST({ version: '10' }).setToken(process.env.BOT_TOKEN!);
const item = commands.find(x => x.Name == name && !x.ServerId);
const itemForServer = commands.find(x => x.Name == name && x.ServerId == message.guild?.id);
rest.put(
Routes.applicationCommands(process.env.BOT_CLIENTID!),
{
body: globalCommandData
let itemToUse: ICommandItem;
if (!itemForServer) {
if (!item) {
message.reply('Command not found');
return;
}
);
for (let guild of guildIds) {
const guildCommandData = guildCommands.filter(x => x.ServerId == guild)
.filter(x => x.Command.CommandBuilder)
.flatMap(x => x.Command.CommandBuilder);
itemToUse = item;
} else {
itemToUse = itemForServer;
}
if (!client.guilds.cache.has(guild)) continue;
const requiredRoles = itemToUse.Command._roles;
rest.put(
Routes.applicationGuildCommands(process.env.BOT_CLIENTID!, guild),
{
body: guildCommandData
for (const i in requiredRoles) {
if (message.guild) {
const setting = await SettingsHelper.GetSetting(`role.${requiredRoles[i]}`, message.guild?.id);
if (!setting) {
message.reply("Unable to verify if you have this role, please contact your bot administrator");
return;
}
)
if (!message.member.roles.cache.find(role => role.name == setting)) {
message.reply(`You require the \`${StringTools.Capitalise(setting)}\` role to run this command`);
return;
}
}
}
const context: ICommandContext = {
name: name,
args: args,
message: message
};
const precheckResponse = itemToUse.Command.precheck(context);
const precheckAsyncResponse = await itemToUse.Command.precheckAsync(context);
if (precheckResponse != CommandResponse.Ok) {
message.reply(ErrorMessages.GetErrorMessage(precheckResponse));
return;
}
if (precheckAsyncResponse != CommandResponse.Ok) {
message.reply(ErrorMessages.GetErrorMessage(precheckAsyncResponse));
return;
}
itemToUse.Command.execute(context);
}
// Load the events
loadEvents(client: Client, events: IEventItem[]) {
events.forEach((e) => {
switch(e.EventType) {
case EventType.ChannelCreate:
client.on('channelCreate', (channel) => e.ExecutionFunction(channel));
break;
case EventType.ChannelDelete:
client.on('channelDelete', (channel) => e.ExecutionFunction(channel));
break;
case EventType.ChannelUpdate:
client.on('channelUpdate', (channel) => e.ExecutionFunction(channel));
break;
case EventType.GuildBanAdd:
client.on('guildBanAdd', (ban) => e.ExecutionFunction(ban));
break;
case EventType.GuildBanRemove:
client.on('guildBanRemove', (ban) => e.ExecutionFunction(ban));
break;
case EventType.GuildCreate:
client.on('guildCreate', (guild) => e.ExecutionFunction(guild));
break;
case EventType.GuildMemberAdd:
client.on('guildMemberAdd', (member) => e.ExecutionFunction(member));
break;
case EventType.GuildMemberRemove:
client.on('guildMemberRemove', (member) => e.ExecutionFunction(member));
break;
case EventType.GuildMemberUpdate:
client.on('guildMemberUpdate', (oldMember, newMember) => e.ExecutionFunction(oldMember, newMember));
break;
case EventType.MessageCreate:
client.on('messageCreate', (message) => e.ExecutionFunction(message));
break;
case EventType.MessageDelete:
client.on('messageDelete', (message) => e.ExecutionFunction(message));
break;
case EventType.MessageUpdate:
client.on('messageUpdate', (oldMessage, newMessage) => e.ExecutionFunction(oldMessage, newMessage));
break;
default:
console.error('Event not implemented.');
}
client.on('channelCreate', e.Event.channelCreate);
client.on('channelDelete', e.Event.channelDelete);
client.on('channelUpdate', e.Event.channelUpdate);
client.on('guildBanAdd', e.Event.guildBanAdd);
client.on('guildBanRemove', e.Event.guildBanRemove);
client.on('guildCreate', e.Event.guildCreate);
client.on('guildMemberAdd', e.Event.guildMemberAdd);
client.on('guildMemberRemove', e.Event.guildMemberRemove);
client.on('guildMemberUpdate', e.Event.guildMemberUpdate);
client.on('message', e.Event.message);
client.on('messageDelete', e.Event.messageDelete);
client.on('messageUpdate', e.Event.messageUpdate);
client.on('ready', e.Event.ready);
});
}
}

View file

@ -1,48 +0,0 @@
import { Command } from "../../type/command";
import { CommandInteraction, SlashCommandBuilder } from "discord.js";
import ListMoons from "./moons/list";
import AddMoon from "./moons/add";
export default class Moons extends Command {
constructor() {
super();
this.CommandBuilder = new SlashCommandBuilder()
.setName("moons")
.setDescription("View and create moons")
.addSubcommand(subcommand =>
subcommand
.setName('list')
.setDescription('List moons you have obtained')
.addUserOption(option =>
option
.setName("user")
.setDescription("The user to view (Defaults to yourself)"))
.addNumberOption(option =>
option
.setName("page")
.setDescription("The page to start with")))
.addSubcommand(subcommand =>
subcommand
.setName('add')
.setDescription('Add a moon to your count!')
.addStringOption(option =>
option
.setName("description")
.setDescription("What deserved a moon?")
.setRequired(true)));
}
public override async execute(interaction: CommandInteraction) {
if (!interaction.isChatInputCommand()) return;
switch (interaction.options.getSubcommand()) {
case "list":
await ListMoons(interaction);
break;
case "add":
await AddMoon(interaction);
break;
}
}
}

View file

@ -1,40 +0,0 @@
import {CommandInteraction, EmbedBuilder} from "discord.js";
import Moon from "../../../database/entities/304276391837302787/Moon";
import EmbedColours from "../../../constants/EmbedColours";
import UserSetting from "../../../database/entities/UserSetting";
export default async function AddMoon(interaction: CommandInteraction) {
const description = interaction.options.get("description", true).value?.toString();
if (!description || description.length > 255) {
await interaction.reply("Name must be less than 255 characters!");
return;
}
let moonSetting = await UserSetting.FetchOneByKey(interaction.user.id, "moons");
const moonCount = moonSetting && Number(moonSetting.Value) ? Number(moonSetting.Value) : 0;
if (moonSetting) {
moonSetting.UpdateValue(`${moonCount + 1}`);
} else {
moonSetting = new UserSetting(interaction.user.id, "moons", `${moonCount + 1}`);
}
await moonSetting.Save(UserSetting, moonSetting);
const allMoons = await Moon.FetchMoonCountByUserId(interaction.user.id);
const moonNumber = allMoons + 1;
const moon = new Moon(moonNumber, description, interaction.user.id);
await moon.Save(Moon, moon);
const embed = new EmbedBuilder()
.setTitle(`${interaction.user.globalName} Got A Moon!`)
.setColor(EmbedColours.Moon)
.setDescription(`**${moon.MoonNumber} -** ${moon.Description}`)
.setThumbnail("https://cdn.discordapp.com/emojis/374131312182689793.webp?size=96&quality=lossless");
await interaction.reply({ embeds: [ embed ] });
}

View file

@ -1,55 +0,0 @@
import {ActionRowBuilder, ButtonBuilder, ButtonStyle, CommandInteraction, EmbedBuilder} from "discord.js";
import Moon from "../../../database/entities/304276391837302787/Moon";
import EmbedColours from "../../../constants/EmbedColours";
import UserSetting from "../../../database/entities/UserSetting";
export default async function ListMoons(interaction: CommandInteraction) {
const user = interaction.options.get("user")?.user ?? interaction.user;
const page = interaction.options.get("page")?.value as number ?? 0;
const pageLength = 10;
const moons = await Moon.FetchPaginatedMoonsByUserId(user.id, pageLength, page);
const moonSetting = await UserSetting.FetchOneByKey(interaction.user.id, "moons");
const totalMoons = moonSetting && Number(moonSetting.Value) ? Number(moonSetting.Value) : 0;
const totalPages = Math.ceil(moons[1] / pageLength);
let description = ["*none*"];
if (moons[0].length > 0) {
description = moons[0].flatMap(x => `**${x.MoonNumber} -** ${x.Description.slice(0, 15)}`);
}
const moonDifference = totalMoons - moons[1];
const isLastPage = page + 1 == totalPages || moons[0].length == 0;
if (isLastPage && moonDifference > 0) {
description.push(`...plus ${moonDifference} more untracked`);
}
const embed = new EmbedBuilder()
.setTitle(`${user.username}'s Moons`)
.setColor(EmbedColours.Ok)
.setDescription(description.join("\n"))
.setFooter({ text: `Page ${page + 1} of ${totalPages} · ${totalMoons} moons` });
const row = new ActionRowBuilder<ButtonBuilder>()
.addComponents(
new ButtonBuilder()
.setCustomId(`moons list ${user.id} ${page - 1}`)
.setLabel("Previous")
.setStyle(ButtonStyle.Primary)
.setDisabled(page == 0),
new ButtonBuilder()
.setCustomId(`moons list ${user.id} ${page + 1}`)
.setLabel("Next")
.setStyle(ButtonStyle.Primary)
.setDisabled(isLastPage));
await interaction.reply({
embeds: [ embed ],
components: [ row ],
});
}

View file

@ -1,58 +0,0 @@
import { CommandInteraction, PermissionsBitField, SlashCommandBuilder } from "discord.js";
import { Command } from "../../../type/command";
import { default as eLobby } from "../../../database/entities/501231711271780357/Lobby";
export default class AddRole extends Command {
constructor() {
super();
this.CommandBuilder = new SlashCommandBuilder()
.setName('addlobby')
.setDescription('Add lobby channel')
.setDefaultMemberPermissions(PermissionsBitField.Flags.ModerateMembers)
.addChannelOption(option =>
option
.setName('channel')
.setDescription('The channel')
.setRequired(true))
.addRoleOption(option =>
option
.setName('role')
.setDescription('The role to ping on request')
.setRequired(true))
.addNumberOption(option =>
option
.setName('cooldown')
.setDescription('The cooldown in minutes')
.setRequired(true))
.addStringOption(option =>
option
.setName('name')
.setDescription('The game name')
.setRequired(true));
}
public override async execute(interaction: CommandInteraction) {
const channel = interaction.options.get('channel');
const role = interaction.options.get('role');
const cooldown = interaction.options.get('cooldown');
const gameName = interaction.options.get('name');
if (!channel || !channel.channel || !role || !role.role || !cooldown || !cooldown.value || !gameName || !gameName.value) {
await interaction.reply('Fields are required.');
return;
}
const lobby = await eLobby.FetchOneByChannelId(channel.channel.id);
if (lobby) {
await interaction.reply('This channel has already been setup.');
return;
}
const entity = new eLobby(channel.channel.id, role.role.id, cooldown.value as number, gameName.value as string);
await entity.Save(eLobby, entity);
await interaction.reply(`Added \`${channel.name}\` as a new lobby channel with a cooldown of \`${cooldown.value} minutes \` and will ping \`${role.name}\` on use`);
}
}

View file

@ -1,48 +0,0 @@
import { CacheType, CommandInteraction, EmbedBuilder, GuildBasedChannel, PermissionsBitField, SlashCommandBuilder } from "discord.js";
import { Command } from "../../../type/command";
import { default as eLobby } from "../../../database/entities/501231711271780357/Lobby";
import EmbedColours from "../../../constants/EmbedColours";
export default class ListLobby extends Command {
constructor() {
super();
this.CommandBuilder = new SlashCommandBuilder()
.setName('listlobby')
.setDescription('Lists all channels set up as lobbies')
.setDefaultMemberPermissions(PermissionsBitField.Flags.ModerateMembers);
}
public override async execute(interaction: CommandInteraction<CacheType>) {
if (!interaction.guild) {
await interaction.reply('Guild not found.');
return;
}
const channels: eLobby[] = [];
for (let channel of interaction.guild.channels.cache.map(x => x)) {
const lobby = await eLobby.FetchOneByChannelId(channel.id);
if (lobby) {
channels.push(lobby);
}
}
const embed = new EmbedBuilder()
.setColor(EmbedColours.Ok)
.setTitle("Lobbies")
.setDescription(`Channels: ${channels.length}`);
for (let lobby of channels) {
embed.addFields([
{
name: `# ${lobby.Name}`,
value: `Last Used: ${lobby.LastUsed}`
}
]);
}
await interaction.reply({ embeds: [ embed ]});
}
}

View file

@ -1,41 +0,0 @@
import { CommandInteraction, SlashCommandBuilder } from "discord.js";
import { Command } from "../../../type/command";
import { default as eLobby } from "../../../database/entities/501231711271780357/Lobby";
export default class Lobby extends Command {
constructor() {
super();
this.CommandBuilder = new SlashCommandBuilder()
.setName('lobby')
.setDescription('Attempt to organise a lobby');
}
public override async execute(interaction: CommandInteraction) {
if (!interaction.channelId) return;
const lobby = await eLobby.FetchOneByChannelId(interaction.channelId);
if (!lobby) {
await interaction.reply('This channel is disabled from using the lobby command.');
return;
}
const timeNow = Date.now();
const timeLength = lobby.Cooldown * 60 * 1000; // x minutes in ms
const timeAgo = timeNow - timeLength;
// If it was less than x minutes ago
if (lobby.LastUsed.getTime() > timeAgo) {
const timeLeft = Math.ceil((timeLength - (timeNow - lobby.LastUsed.getTime())) / 1000 / 60);
await interaction.reply(`Requesting a lobby for this game is on cooldown! Please try again in **${timeLeft} minutes**.`);
return;
}
lobby.MarkAsUsed();
await lobby.Save(eLobby, lobby);
await interaction.reply(`${interaction.user} would like to organise a lobby of **${lobby.Name}**! <@&${lobby.RoleId}>`);
}
}

View file

@ -1,40 +0,0 @@
import { CommandInteraction, PermissionsBitField, SlashCommandBuilder } from "discord.js";
import { Command } from "../../../type/command";
import { default as eLobby } from "../../../database/entities/501231711271780357/Lobby";
import BaseEntity from "../../../contracts/BaseEntity";
export default class RemoveLobby extends Command {
constructor() {
super();
this.CommandBuilder = new SlashCommandBuilder()
.setName('removelobby')
.setDescription('Remove a lobby channel')
.setDefaultMemberPermissions(PermissionsBitField.Flags.ModerateMembers)
.addChannelOption(option =>
option
.setName('channel')
.setDescription('The channel')
.setRequired(true));
}
public override async execute(interaction: CommandInteraction) {
const channel = interaction.options.get('channel');
if (!channel || !channel.channel) {
await interaction.reply('Channel is required.');
return;
}
const entity = await eLobby.FetchOneByChannelId(channel.channel.id);
if (!entity) {
await interaction.reply('Channel not found.');
return;
}
await BaseEntity.Remove<eLobby>(eLobby, entity);
await interaction.reply(`Removed <#${channel.channel.id}> from the list of lobby channels`);
}
}

View file

@ -1,5 +1,5 @@
import { CommandInteraction, EmbedBuilder, PermissionsBitField, SlashCommandBuilder, TextChannel } from "discord.js";
import EmbedColours from "../../constants/EmbedColours";
import { ICommandContext } from "../../contracts/ICommandContext";
import PublicEmbed from "../../helpers/embeds/PublicEmbed";
import SettingsHelper from "../../helpers/SettingsHelper";
import { Command } from "../../type/command";
@ -7,25 +7,19 @@ export default class Entry extends Command {
constructor() {
super();
this.CommandBuilder = new SlashCommandBuilder()
.setName('entry')
.setDescription('Sends the entry embed')
.setDefaultMemberPermissions(PermissionsBitField.Flags.ModerateMembers);
super._category = "Moderation";
super._roles = [
"moderator"
];
}
public override async execute(interaction: CommandInteraction) {
if (!interaction.guildId) return;
if (!interaction.channel) return;
public override async execute(context: ICommandContext) {
if (!context.message.guild) return;
const rulesChannelId = await SettingsHelper.GetSetting("channels.rules", interaction.guildId) || "rules";
const rulesChannelId = await SettingsHelper.GetSetting("channels.rules", context.message.guild.id) || "rules";
const embed = new EmbedBuilder()
.setColor(EmbedColours.Ok)
.setTitle("Welcome")
.setDescription(`Welcome to the server! Please make sure to read the rules in the <#${rulesChannelId}> channel and type the code found there in here to proceed to the main part of the server.`);
const embedInfo = new PublicEmbed(context, "", `Welcome to the server! Please make sure to read the rules in the <#${rulesChannelId}> channel and type the code found there in here to proceed to the main part of the server.`);
const channel = interaction.channel as TextChannel;
await channel.send({ embeds: [ embed ]});
embedInfo.SendToCurrentChannel();
}
}

View file

@ -0,0 +1,143 @@
import { TextChannel } from "discord.js";
import { ICommandContext } from "../../contracts/ICommandContext";
import { Command } from "../../type/command";
import { default as eLobby } from "../../entity/501231711271780357/Lobby";
import SettingsHelper from "../../helpers/SettingsHelper";
import PublicEmbed from "../../helpers/embeds/PublicEmbed";
import { readFileSync } from "fs";
import ErrorEmbed from "../../helpers/embeds/ErrorEmbed";
import BaseEntity from "../../contracts/BaseEntity";
export default class Lobby extends Command {
constructor() {
super();
super._category = "General";
}
public override async execute(context: ICommandContext) {
if (!context.message.guild) return;
switch (context.args[0]) {
case "config":
await this.UseConfig(context);
break;
default:
await this.UseDefault(context);
}
}
// =======
// Default
// =======
private async UseDefault(context: ICommandContext) {
const channel = context.message.channel as TextChannel;
const channelId = channel.id;
const lobby = await eLobby.FetchOneByChannelId(channelId);
if (!lobby) {
this.SendDisabled(context);
return;
}
const timeNow = Date.now();
const timeLength = lobby.Cooldown * 60 * 1000; // x minutes in ms
const timeAgo = timeNow - timeLength;
// If it was less than x minutes ago
if (lobby.LastUsed.getTime() > timeAgo) {
this.SendOnCooldown(context, timeLength, new Date(timeNow), lobby.LastUsed);
return;
}
await this.RequestLobby(context, lobby);
}
private async RequestLobby(context: ICommandContext, lobby: eLobby) {
lobby.MarkAsUsed();
await lobby.Save(eLobby, lobby);
context.message.channel.send(`${context.message.author} would like to organise a lobby of **${lobby.Name}**! <@&${lobby.RoleId}>`);
}
private SendOnCooldown(context: ICommandContext, timeLength: number, timeNow: Date, timeUsed: Date) {
const timeLeft = Math.ceil((timeLength - (timeNow.getTime() - timeUsed.getTime())) / 1000 / 60);
context.message.reply(`Requesting a lobby for this game is on cooldown! Please try again in **${timeLeft} minutes**.`);
}
private SendDisabled(context: ICommandContext) {
context.message.reply("This channel hasn't been setup for lobbies.");
}
// ======
// Config
// ======
private async UseConfig(context: ICommandContext) {
const moderatorRole = await SettingsHelper.GetSetting("role.moderator", context.message.guild!.id);
if (!context.message.member?.roles.cache.find(x => x.name == moderatorRole)) {
const errorEmbed = new ErrorEmbed(context, "Sorry, you must be a moderator to be able to configure this command");
errorEmbed.SendToCurrentChannel();
return;
}
switch (context.args[1]) {
case "add":
await this.AddLobbyConfig(context);
break;
case "remove":
await this.RemoveLobbyConfig(context);
break;
case "help":
default:
this.SendConfigHelp(context);
}
}
private SendConfigHelp(context: ICommandContext) {
const helpText = readFileSync(`${process.cwd()}/data/lobbyConfig.txt`).toString();
const embed = new PublicEmbed(context, "Configure Lobby Command", helpText);
embed.SendToCurrentChannel();
}
private async AddLobbyConfig(context: ICommandContext) {
const channel = context.message.guild!.channels.cache.find(x => x.name == context.args[2]);
const role = context.message.guild!.roles.cache.find(x => x.name == context.args[3]);
const cooldown = context.args[4] || "30";
const gameName = context.args.splice(5).join(" ");
if (!channel || !role) {
this.SendConfigHelp(context);
return;
}
const entity = new eLobby(channel.id, role.id, Number.parseInt(cooldown), gameName);
await entity.Save(eLobby, entity);
const embed = new PublicEmbed(context, "", "Added new lobby channel");
embed.SendToCurrentChannel();
}
private async RemoveLobbyConfig(context: ICommandContext) {
const channel = context.message.guild!.channels.cache.find(x => x.name == context.args[2]);
if (!channel) {
this.SendConfigHelp(context);
return;
}
const entity = await eLobby.FetchOneByChannelId(channel.id);
if (entity) {
await BaseEntity.Remove<eLobby>(eLobby, entity);
}
const embed = new PublicEmbed(context, "", "Removed lobby channel");
embed.SendToCurrentChannel();
}
}

View file

@ -1,54 +0,0 @@
import { CommandInteraction, PermissionsBitField, SlashCommandBuilder } from "discord.js";
import { Command } from "../../type/command";
import { default as eRole } from "../../database/entities/Role";
import Server from "../../database/entities/Server";
export default class ConfigRole extends Command {
constructor() {
super();
this.CommandBuilder = new SlashCommandBuilder()
.setName('configrole')
.setDescription('Toggle your roles')
.setDefaultMemberPermissions(PermissionsBitField.Flags.ManageRoles)
.addRoleOption(option =>
option
.setName('role')
.setDescription('The role name')
.setRequired(true));
}
public override async execute(interaction: CommandInteraction) {
if (!interaction.guildId || !interaction.guild) return;
if (!interaction.member) return;
const role = interaction.options.get('role');
if (!role || !role.role) {
await interaction.reply('Fields are required.');
return;
}
const existingRole = await eRole.FetchOneByRoleId(role.role.id);
if (existingRole) {
await eRole.Remove(eRole, existingRole);
await interaction.reply('Removed role from configuration.');
} else {
const server = await Server.FetchOneById(Server, interaction.guildId);
if (!server) {
await interaction.reply('This server has not been setup.');
return;
}
const newRole = new eRole(role.role.id);
newRole.SetServer(server);
await newRole.Save(eRole, newRole);
await interaction.reply('Added role to configuration.');
}
}
}

View file

@ -1,109 +0,0 @@
import { CommandInteraction, EmbedBuilder, GuildMemberRoleManager, SlashCommandBuilder } from "discord.js";
import { Command } from "../../type/command";
import { default as eRole } from "../../database/entities/Role";
import EmbedColours from "../../constants/EmbedColours";
export default class Role extends Command {
constructor() {
super();
this.CommandBuilder = new SlashCommandBuilder()
.setName('role')
.setDescription('Toggle your roles')
.addSubcommand(subcommand =>
subcommand
.setName('toggle')
.setDescription('Toggle your role')
.addRoleOption(option =>
option
.setName('role')
.setDescription('The role name')
.setRequired(true)))
.addSubcommand(subcommand =>
subcommand
.setName('list')
.setDescription('List togglable roles'));
}
public override async execute(interaction: CommandInteraction) {
if (!interaction.isChatInputCommand()) return;
switch (interaction.options.getSubcommand()) {
case 'toggle':
await this.ToggleRole(interaction);
break;
case 'list':
await this.SendRolesList(interaction);
break;
default:
await interaction.reply('Subcommand not found.');
}
}
private async SendRolesList(interaction: CommandInteraction) {
const roles = await this.GetRolesList(interaction);
const embed = new EmbedBuilder()
.setColor(EmbedColours.Ok)
.setTitle("Roles")
.setDescription(`Roles: ${roles.length}\n\n${roles.join("\n")}`);
await interaction.reply({ embeds: [ embed ]});
}
private async ToggleRole(interaction: CommandInteraction) {
if (!interaction.guild) return;
if (!interaction.member) return;
const roles = await this.GetRolesList(interaction);
const requestedRole = interaction.options.get('role');
if (!requestedRole || !requestedRole.role) {
await interaction.reply('Fields are required.');
return;
}
if (!roles.includes(requestedRole.role.name)) {
await interaction.reply('This role isn\'t marked as assignable.');
return;
}
const roleManager = interaction.member.roles as GuildMemberRoleManager;
const userRole = roleManager.cache.find(x => x.name == requestedRole.role!.name);
const assignRole = interaction.guild.roles.cache.find(x => x.id == requestedRole.role!.id);
if (!assignRole) return;
if (!assignRole.editable) {
await interaction.reply('Insufficient permissions. Please contact a moderator.');
return;
}
if (!userRole) {
await roleManager.add(assignRole);
await interaction.reply(`Gave role: \`${assignRole.name}\``);
} else {
await roleManager.remove(assignRole);
await interaction.reply(`Removed role: \`${assignRole.name}\``);
}
}
private async GetRolesList(interaction: CommandInteraction): Promise<string[]> {
if (!interaction.guildId || !interaction.guild) return [];
const rolesArray = await eRole.FetchAllByServerId(interaction.guildId);
const roles: string[] = [];
for (let i = 0; i < rolesArray.length; i++) {
const serverRole = interaction.guild.roles.cache.find(x => x.id == rolesArray[i].RoleId);
if (serverRole) {
roles.push(serverRole.name);
}
}
return roles;
}
}

View file

@ -1,56 +1,25 @@
import { ActionRowBuilder, ButtonBuilder, ButtonStyle, CommandInteraction, EmbedBuilder, SlashCommandBuilder } from "discord.js";
import EmbedColours from "../constants/EmbedColours";
import { ICommandContext } from "../contracts/ICommandContext";
import ICommandReturnContext from "../contracts/ICommandReturnContext";
import PublicEmbed from "../helpers/embeds/PublicEmbed";
import { Command } from "../type/command";
export default class About extends Command {
constructor() {
super();
this.CommandBuilder = new SlashCommandBuilder()
.setName('about')
.setDescription('About VylBot');
super._category = "General";
}
public override async execute(interaction: CommandInteraction) {
const fundingLink = process.env.ABOUT_FUNDING;
const repoLink = process.env.ABOUT_REPO;
public override execute(context: ICommandContext): ICommandReturnContext {
const embed = new PublicEmbed(context, "About", "")
.addField("Version", process.env.BOT_VER)
.addField("Author", process.env.BOT_AUTHOR)
.addField("Date", process.env.BOT_DATE);
embed.SendToCurrentChannel();
const embed = new EmbedBuilder()
.setColor(EmbedColours.Ok)
.setTitle("About")
.setDescription("Discord Bot made by Vylpes");
embed.addFields([
{
name: "Version",
value: process.env.BOT_VER!,
inline: true,
},
{
name: "Author",
value: process.env.BOT_AUTHOR!,
inline: true,
},
]);
const row = new ActionRowBuilder<ButtonBuilder>();
if (repoLink) {
row.addComponents(
new ButtonBuilder()
.setURL(repoLink)
.setLabel("Repo")
.setStyle(ButtonStyle.Link));
}
if (fundingLink) {
row.addComponents(
new ButtonBuilder()
.setURL(fundingLink)
.setLabel("Funding")
.setStyle(ButtonStyle.Link));
}
await interaction.reply({ embeds: [ embed ], components: row.components.length > 0 ? [ row ] : [] });
return {
commandContext: context,
embeds: [embed]
};
}
}

View file

@ -1,212 +0,0 @@
import Audit from "../database/entities/Audit";
import AuditTools from "../helpers/AuditTools";
import { Command } from "../type/command";
import { CommandInteraction, EmbedBuilder, PermissionsBitField, SlashCommandBuilder } from "discord.js";
import { AuditType } from "../constants/AuditType";
import EmbedColours from "../constants/EmbedColours";
export default class Audits extends Command {
constructor() {
super();
this.CommandBuilder = new SlashCommandBuilder()
.setName("audits")
.setDescription("View audits of a particular user in the server")
.setDefaultMemberPermissions(PermissionsBitField.Flags.ModerateMembers)
.addSubcommand(subcommand =>
subcommand
.setName('user')
.setDescription('View all audits done against a user')
.addUserOption(option =>
option
.setName('target')
.setDescription('The user')
.setRequired(true)))
.addSubcommand(subcommand =>
subcommand
.setName('view')
.setDescription('View a particular audit')
.addStringOption(option =>
option
.setName('auditid')
.setDescription('The audit id in caps')
.setRequired(true)))
.addSubcommand(subcommand =>
subcommand
.setName('clear')
.setDescription('Clears an audit from a user')
.addStringOption(option =>
option
.setName('auditid')
.setDescription('The audit id in caps')
.setRequired(true)))
.addSubcommand(subcommand =>
subcommand
.setName('add')
.setDescription('Manually add an audit')
.addUserOption(option =>
option
.setName('target')
.setDescription('The user')
.setRequired(true))
.addStringOption(option =>
option
.setName('type')
.setDescription('The type of audit')
.setRequired(true)
.addChoices(
{ name: 'General', value: AuditType.General.toString() },
{ name: 'Warn', value: AuditType.Warn.toString() },
{ name: 'Mute', value: AuditType.Mute.toString() },
{ name: 'Kick', value: AuditType.Kick.toString() },
{ name: 'Ban', value: AuditType.Ban.toString() },
)
.setRequired(true))
.addStringOption(option =>
option
.setName('reason')
.setDescription('The reason')));
}
public override async execute(interaction: CommandInteraction) {
if (!interaction.isChatInputCommand()) return;
switch (interaction.options.getSubcommand()) {
case "user":
await this.SendAuditForUser(interaction);
break;
case "view":
await this.SendAudit(interaction);
break;
case "clear":
await this.ClearAudit(interaction);
break;
case "add":
await this.AddAudit(interaction);
break;
default:
await interaction.reply("Subcommand doesn't exist.");
}
}
private async SendAuditForUser(interaction: CommandInteraction) {
if (!interaction.guildId) return;
const user = interaction.options.get('target', true).user!;
if (!user) {
await interaction.reply("User not found.");
return;
}
const audits = await Audit.FetchAuditsByUserId(user.id, interaction.guildId);
if (!audits || audits.length == 0) {
await interaction.reply("There are no audits for this user.");
return;
}
const embed = new EmbedBuilder()
.setColor(EmbedColours.Ok)
.setTitle("Audits")
.setDescription(`Audits: ${audits.length}`);
for (let audit of audits) {
embed.addFields([
{
name: `${audit.AuditId} // ${AuditTools.TypeToFriendlyText(audit.AuditType)}`,
value: audit.WhenCreated.toString(),
}
]);
}
await interaction.reply({ embeds: [ embed ]});
}
private async SendAudit(interaction: CommandInteraction) {
if (!interaction.guildId) return;
const auditId = interaction.options.get('auditid');
if (!auditId || !auditId.value) {
await interaction.reply("AuditId not found.");
return;
}
const audit = await Audit.FetchAuditByAuditId(auditId.value.toString().toUpperCase(), interaction.guildId);
if (!audit) {
await interaction.reply("Audit not found.");
return;
}
const embed = new EmbedBuilder()
.setColor(EmbedColours.Ok)
.setTitle("Audit")
.setDescription(audit.AuditId.toUpperCase())
.addFields([
{
name: "Reason",
value: audit.Reason || "*none*",
inline: true,
},
{
name: "Type",
value: AuditTools.TypeToFriendlyText(audit.AuditType),
inline: true,
},
{
name: "Moderator",
value: `<@${audit.ModeratorId}>`,
inline: true,
},
]);
await interaction.reply({ embeds: [ embed ]});
}
private async ClearAudit(interaction: CommandInteraction) {
if (!interaction.guildId) return;
const auditId = interaction.options.get('auditid');
if (!auditId || !auditId.value) {
await interaction.reply("AuditId not found.");
return;
}
const audit = await Audit.FetchAuditByAuditId(auditId.value.toString().toUpperCase(), interaction.guildId);
if (!audit) {
await interaction.reply("Audit not found.");
return;
}
await Audit.Remove(Audit, audit);
await interaction.reply("Audit cleared.");
}
private async AddAudit(interaction: CommandInteraction) {
if (!interaction.guildId) return;
const user = interaction.options.get('target', true).user!;
const auditType = interaction.options.get('type');
const reasonInput = interaction.options.get('reason');
if (!user || !auditType || !auditType.value) {
await interaction.reply("Invalid input.");
return;
}
const type = auditType.value as AuditType;
const reason = reasonInput && reasonInput.value ? reasonInput.value.toString() : "";
const audit = new Audit(user.id, type, reason, interaction.user.id, interaction.guildId);
await audit.Save(Audit, audit);
await interaction.reply(`Created new audit with ID \`${audit.AuditId}\``);
}
}

View file

@ -1,113 +0,0 @@
import {ChatInputCommandInteraction, CommandInteraction, EmbedBuilder, PermissionFlagsBits, SlashCommandBuilder} from "discord.js";
import {Command} from "../type/command";
import TimeLengthInput from "../helpers/TimeLengthInput";
import AutoKickHelper from "../helpers/AutoKickHelper";
export default class Autokick extends Command {
constructor() {
super();
this.CommandBuilder = new SlashCommandBuilder()
.setName("autokick")
.setDescription("Configure the auto kick functionality")
.setDefaultMemberPermissions(PermissionFlagsBits.KickMembers)
.addSubcommand(x => x
.setName("set")
.setDescription("Set the configuration")
.addRoleOption(y => y
.setName("role")
.setDescription("The role the user needs to be auto kicked")
.setRequired(true))
.addStringOption(y => y
.setName("kicktime")
.setDescription("The time with the role before being kicked (Ex: 2h 30m)")
.setRequired(true))
.addStringOption(y => y
.setName("noticetime")
.setDescription("The time before being kicked when a notification is sent (Ex: 2h 30m)"))
.addChannelOption(y => y
.setName("noticechannel")
.setDescription("The channel to send the notification to")))
.addSubcommand(x => x
.setName("unset")
.setDescription("Unset the current configuration"));
}
public override async execute(interaction: CommandInteraction) {
if (!interaction.isChatInputCommand()) return;
const subcommand = interaction.options.getSubcommand();
switch (subcommand) {
case "set":
await this.set(interaction);
break;
case "unset":
await this.unset(interaction);
break;
}
}
private async set(interaction: ChatInputCommandInteraction) {
if (!interaction.guildId) return;
const roleOption = interaction.options.getRole("role", true);
const kickTimeOption = interaction.options.getString("kicktime", true);
const noticeTimeOption = interaction.options.getString("noticetime");
const noticeChannelOption = interaction.options.getChannel("noticechannel");
const roleId = roleOption.id;
const kickTimeInput = new TimeLengthInput(kickTimeOption);
const noticeTimeInput = noticeTimeOption ? new TimeLengthInput(noticeTimeOption) : undefined;
const noticeChannelId = noticeChannelOption?.id;
if ((noticeTimeInput && !noticeTimeOption) || (!noticeTimeInput && noticeChannelOption)) {
await interaction.reply("Both `noticetime` and `noticechannel` must be set if you want a notification embed");
return;
}
await AutoKickHelper.SetSetting(interaction.guildId, roleId, kickTimeInput.GetMilliseconds(), noticeTimeInput?.GetMilliseconds(), noticeChannelId);
const embed = new EmbedBuilder()
.setTitle("Auto Kick")
.setDescription("Configured auto kick for this server")
.addFields([
{
name: "Role",
value: roleOption.name,
inline: true,
},
{
name: "Kick Time",
value: kickTimeInput.GetLengthShort(),
inline: true,
},
]);
if (noticeTimeInput) {
embed.addFields([
{
name: "Notice Time",
value: noticeTimeInput.GetLengthShort(),
},
{
name: "Notice Channel",
value: noticeChannelOption!.name!,
inline: true,
},
]);
}
await interaction.reply({
embeds: [ embed ],
});
}
private async unset(interaction: ChatInputCommandInteraction) {
if (!interaction.guildId) return;
await AutoKickHelper.UnsetSetting(interaction.guildId);
await interaction.reply("Unset the auto kick configuration for this server");
}
}

View file

@ -1,101 +1,80 @@
import ErrorEmbed from "../helpers/embeds/ErrorEmbed";
import ErrorMessages from "../constants/ErrorMessages";
import LogEmbed from "../helpers/embeds/LogEmbed";
import PublicEmbed from "../helpers/embeds/PublicEmbed";
import { Command } from "../type/command";
import Audit from "../database/entities/Audit";
import { AuditType } from "../constants/AuditType";
import { CommandInteraction, EmbedBuilder, GuildMember, PermissionsBitField, SlashCommandBuilder, TextChannel } from "discord.js";
import EmbedColours from "../constants/EmbedColours";
import SettingsHelper from "../helpers/SettingsHelper";
import { ICommandContext } from "../contracts/ICommandContext";
import ICommandReturnContext from "../contracts/ICommandReturnContext";
export default class Ban extends Command {
constructor() {
super();
this.CommandBuilder = new SlashCommandBuilder()
.setName("ban")
.setDescription("Ban a member from the server with an optional reason")
.setDefaultMemberPermissions(PermissionsBitField.Flags.BanMembers)
.addUserOption(option =>
option
.setName('target')
.setDescription('The user')
.setRequired(true))
.addStringOption(option =>
option
.setName('reason')
.setDescription('The reason'));
super._category = "Moderation";
super._roles = [
"moderator"
];
}
public override async execute(interaction: CommandInteraction) {
if (!interaction.isChatInputCommand()) return;
if (!interaction.guildId) return;
if (!interaction.guild) return;
public override async execute(context: ICommandContext): Promise<ICommandReturnContext> {
const targetUser = context.message.mentions.users.first();
const targetUser = interaction.options.get('target');
const reasonInput = interaction.options.get('reason');
if (!targetUser || !targetUser.user || !targetUser.member) {
await interaction.reply("User not found.");
return;
if (!targetUser) {
const embed = new ErrorEmbed(context, "User does not exist");
embed.SendToCurrentChannel();
return {
commandContext: context,
embeds: [embed],
};
}
const member = targetUser.member as GuildMember;
const reason = reasonInput && reasonInput.value ? reasonInput.value.toString() : "*none*";
const targetMember = context.message.guild?.member(targetUser);
const logEmbed = new EmbedBuilder()
.setColor(EmbedColours.Ok)
.setTitle("Member Banned")
.setDescription(`<@${targetUser.user.id}> \`${targetUser.user.tag}\``)
.setThumbnail(targetUser.user.avatarURL())
.addFields([
{
name: "Moderator",
value: `<@${interaction.user.id}>`,
},
{
name: "Reason",
value: reason,
},
]);
if (!member.bannable) {
await interaction.reply('Insufficient permissions. Please contact a moderator.');
return;
if (!targetMember) {
const embed = new ErrorEmbed(context, "User is not in this server");
embed.SendToCurrentChannel();
return {
commandContext: context,
embeds: [embed],
};
}
await member.ban();
const channelName = await SettingsHelper.GetSetting('channels.logs.mod', interaction.guildId);
if (!channelName) return;
const channel = interaction.guild.channels.cache.find(x => x.name == channelName) as TextChannel;
if (channel) {
await channel.send({ embeds: [ logEmbed ]});
const reasonArgs = context.args;
reasonArgs.splice(0, 1)
const reason = reasonArgs.join(" ");
if (!context.message.guild?.available) {
return {
commandContext: context,
embeds: [],
};
}
const dmEmbed = new EmbedBuilder()
.setColor(EmbedColours.Ok)
.setTitle(interaction.guild.name)
.setDescription("You have been banned by a moderator.")
.addFields([
{
name: "Reason",
value: reason,
},
]);
let replyText = "Successfully banned user.";
try {
const dmChannel = await targetUser.user!.createDM();
await dmChannel.send({ embeds: [ dmEmbed ] });
} catch {
replyText += " *Note: I was unable to DM the user the reason.*";
if (!targetMember.bannable) {
const embed = new ErrorEmbed(context, ErrorMessages.InsufficientBotPermissions);
embed.SendToCurrentChannel();
return {
commandContext: context,
embeds: [embed],
};
}
const audit = new Audit(targetUser.user.id, AuditType.Ban, reason, interaction.user.id, interaction.guildId);
await audit.Save(Audit, audit);
const logEmbed = new LogEmbed(context, "Member Banned");
logEmbed.AddUser("User", targetUser, true);
logEmbed.AddUser("Moderator", context.message.author);
logEmbed.AddReason(reason);
await interaction.reply(replyText);
const publicEmbed = new PublicEmbed(context, "", `${targetUser} has been banned`);
await targetMember.ban({ reason: `Moderator: ${context.message.author.tag}, Reason: ${reason || "*none*"}` });
await logEmbed.SendToModLogsChannel();
publicEmbed.SendToCurrentChannel();
return {
commandContext: context,
embeds: [logEmbed, publicEmbed],
};
}
}

View file

@ -1,47 +0,0 @@
import { Command } from "../type/command";
import randomBunny from "random-bunny";
import { CommandInteraction, EmbedBuilder, SlashCommandBuilder } from "discord.js";
import EmbedColours from "../constants/EmbedColours";
export default class Bunny extends Command {
constructor() {
super();
this.CommandBuilder = new SlashCommandBuilder()
.setName("bunny")
.setDescription("Get a random picture of a rabbit.");
}
public override async execute(interaction: CommandInteraction) {
if (!interaction.isChatInputCommand()) return;
await interaction.deferReply();
const subreddits = [
'rabbits',
'bunnieswithhats',
'buncomfortable',
'bunnytongues',
'dutchbunnymafia',
];
const random = Math.floor(Math.random() * subreddits.length);
const selectedSubreddit = subreddits[random];
const result = await randomBunny(selectedSubreddit, 'hot');
if (result.IsSuccess) {
const embed = new EmbedBuilder()
.setColor(EmbedColours.Ok)
.setTitle(result.Result!.Title)
.setDescription(result.Result!.Permalink)
.setImage(result.Result!.Url)
.setURL(`https://reddit.com${result.Result!.Permalink}`)
.setFooter({ text: `r/${selectedSubreddit} · ${result.Result!.Ups} upvotes`});
await interaction.editReply({ embeds: [ embed ]});
} else {
await interaction.editReply("There was an error running this command.");
}
}
}

View file

@ -1,43 +1,50 @@
import { CommandInteraction, PermissionsBitField, SlashCommandBuilder, TextChannel } from "discord.js";
import ErrorEmbed from "../helpers/embeds/ErrorEmbed";
import { TextChannel } from "discord.js";
import PublicEmbed from "../helpers/embeds/PublicEmbed";
import { Command } from "../type/command";
import { ICommandContext } from "../contracts/ICommandContext";
import ICommandReturnContext from "../contracts/ICommandReturnContext";
export default class Clear extends Command {
constructor() {
super();
this.CommandBuilder = new SlashCommandBuilder()
.setName("clear")
.setDescription("Clears the channel of messages")
.setDefaultMemberPermissions(PermissionsBitField.Flags.ManageMessages)
.addNumberOption(option =>
option
.setName('count')
.setDescription('The amount to delete')
.setRequired(true)
.setMinValue(1)
.setMaxValue(100));
super._category = "Moderation";
super._roles = [
"moderator"
];
}
public override async execute(interaction: CommandInteraction) {
if (!interaction.isChatInputCommand()) return;
if (!interaction.channel) return;
public override async execute(context: ICommandContext): Promise<ICommandReturnContext> {
if (context.args.length == 0) {
const errorEmbed = new ErrorEmbed(context, "Please specify an amount between 1 and 100");
errorEmbed.SendToCurrentChannel();
const totalToClear = interaction.options.getNumber('count');
return {
commandContext: context,
embeds: [errorEmbed]
};
}
const totalToClear = Number.parseInt(context.args[0]);
if (!totalToClear || totalToClear <= 0 || totalToClear > 100) {
await interaction.reply('Please specify an amount between 1 and 100.');
return;
const errorEmbed = new ErrorEmbed(context, "Please specify an amount between 1 and 100");
errorEmbed.SendToCurrentChannel();
return {
commandContext: context,
embeds: [errorEmbed]
};
}
const channel = interaction.channel as TextChannel;
await (context.message.channel as TextChannel).bulkDelete(totalToClear);
if (!channel.manageable) {
await interaction.reply('Insufficient permissions. Please contact a moderator.');
return;
}
const embed = new PublicEmbed(context, "", `${totalToClear} message(s) were removed`);
embed.SendToCurrentChannel();
await channel.bulkDelete(totalToClear);
await interaction.reply(`${totalToClear} message(s) were removed.`);
return {
commandContext: context,
embeds: [embed]
};
}
}

View file

@ -1,4 +1,7 @@
import { CommandInteraction, EmbedBuilder, PermissionsBitField, SlashCommandBuilder, TextChannel } from "discord.js";
import { CommandResponse } from "../constants/CommandResponse";
import { ICommandContext } from "../contracts/ICommandContext";
import ErrorEmbed from "../helpers/embeds/ErrorEmbed";
import PublicEmbed from "../helpers/embeds/PublicEmbed";
import SettingsHelper from "../helpers/SettingsHelper";
import StringTools from "../helpers/StringTools";
import { Command } from "../type/command";
@ -7,60 +10,85 @@ export default class Code extends Command {
constructor() {
super();
this.CommandBuilder = new SlashCommandBuilder()
.setName('code')
.setDescription('Manage the verification code of the server')
.setDefaultMemberPermissions(PermissionsBitField.Flags.ModerateMembers)
.addSubcommand(subcommand =>
subcommand
.setName('randomise')
.setDescription('Regenerates the verification code for this server'))
.addSubcommand(subcommand =>
subcommand
.setName('embed')
.setDescription('Sends the embed with the current code to the current channel'));
super._category = "Moderation";
super._roles = [
"moderator"
];
}
public override async execute(interaction: CommandInteraction) {
if (!interaction.isChatInputCommand()) return;
public override async precheckAsync(context: ICommandContext): Promise<CommandResponse> {
if (!context.message.guild){
return CommandResponse.NotInServer;
}
switch (interaction.options.getSubcommand()) {
const isEnabled = await SettingsHelper.GetSetting("verification.enabled", context.message.guild?.id);
if (!isEnabled) {
return CommandResponse.FeatureDisabled;
}
if (isEnabled.toLocaleLowerCase() != 'true') {
return CommandResponse.FeatureDisabled;
}
return CommandResponse.Ok;
}
public override async execute(context: ICommandContext) {
const action = context.args[0];
switch (action) {
case "randomise":
await this.Randomise(interaction);
await this.Randomise(context);
break;
case "embed":
await this.SendEmbed(interaction);
await this.SendEmbed(context);
break;
default:
await this.SendUsage(context);
}
}
private async Randomise(interaction: CommandInteraction) {
if (!interaction.guildId) return;
const randomCode = StringTools.RandomString(5);
await SettingsHelper.SetSetting("verification.code", interaction.guildId, randomCode);
await interaction.reply(`Entry code has been set to \`${randomCode}\``);
private async SendUsage(context: ICommandContext) {
const description = [
"USAGE: <randomise|embed>",
"",
"randomise: Sets the server's entry code to a random code",
"embed: Sends an embed with the server's entry code"
].join("\n");
const embed = new PublicEmbed(context, "", description);
embed.SendToCurrentChannel();
}
private async SendEmbed(interaction: CommandInteraction) {
if (!interaction.guildId) return;
if (!interaction.channel) return;
const code = await SettingsHelper.GetSetting("verification.code", interaction.guildId);
if (!code || code == "") {
await interaction.reply("There is no code for this server setup.");
private async Randomise(context: ICommandContext) {
if (!context.message.guild) {
return;
}
const embed = new EmbedBuilder()
.setTitle("Entry Code")
.setDescription(code);
const randomCode = StringTools.RandomString(5);
const channel = interaction.channel as TextChannel;
await SettingsHelper.SetSetting("verification.code", context.message.guild.id, randomCode);
await channel.send({ embeds: [ embed ]});
const embed = new PublicEmbed(context, "Code", `Entry code has been set to \`${randomCode}\``);
embed.SendToCurrentChannel();
}
private async SendEmbed(context: ICommandContext) {
if (!context.message.guild) {
return;
}
const code = await SettingsHelper.GetSetting("verification.code", context.message.guild.id);
if (!code || code == "") {
const errorEmbed = new ErrorEmbed(context, "There is no code for this server setup.");
errorEmbed.SendToCurrentChannel();
return;
}
const embed = new PublicEmbed(context, "Entry Code", code!);
embed.SendToCurrentChannel();
}
}

View file

@ -1,192 +1,127 @@
import { CommandInteraction, EmbedBuilder, PermissionsBitField, SlashCommandBuilder } from "discord.js";
import { Guild } from "discord.js";
import { readFileSync } from "fs";
import { CommandResponse } from "../constants/CommandResponse";
import DefaultValues from "../constants/DefaultValues";
import EmbedColours from "../constants/EmbedColours";
import Server from "../database/entities/Server";
import Setting from "../database/entities/Setting";
import { ICommandContext } from "../contracts/ICommandContext";
import ICommandReturnContext from "../contracts/ICommandReturnContext";
import Server from "../entity/Server";
import Setting from "../entity/Setting";
import ErrorEmbed from "../helpers/embeds/ErrorEmbed";
import PublicEmbed from "../helpers/embeds/PublicEmbed";
import { Command } from "../type/command";
export default class Config extends Command {
constructor() {
super();
this.CommandBuilder = new SlashCommandBuilder()
.setName('config')
.setDescription('Configure the current server')
.setDefaultMemberPermissions(PermissionsBitField.Flags.Administrator)
.addSubcommand(subcommand =>
subcommand
.setName('reset')
.setDescription('Reset a setting to the default')
.addStringOption(option =>
option
.setName('key')
.setDescription('The key')
.setRequired(true)))
.addSubcommand(subcommand =>
subcommand
.setName('get')
.setDescription('Gets a setting for the server')
.addStringOption(option =>
option
.setName('key')
.setDescription('The key')
.setRequired(true)))
.addSubcommand(subcommand =>
subcommand
.setName('set')
.setDescription('Sets a setting to a specified value')
.addStringOption(option =>
option
.setName('key')
.setDescription('The key')
.setRequired(true))
.addStringOption(option =>
option
.setName('value')
.setDescription('The value')
.setRequired(true)))
.addSubcommand(subcommand =>
subcommand
.setName('list')
.setDescription('Lists all settings'))
super._category = "Administration";
super._roles = [
"administrator"
]
}
public override async execute(interaction: CommandInteraction) {
if (!interaction.isChatInputCommand()) return;
if (!interaction.guildId) return;
public override async precheckAsync(context: ICommandContext): Promise<CommandResponse> {
if (!context.message.guild) {
return CommandResponse.ServerNotSetup;
}
const server = await Server.FetchOneById<Server>(Server, interaction.guildId, [
const server = await Server.FetchOneById<Server>(Server, context.message.guild?.id, [
"Settings",
]);
if (!server) {
await interaction.reply('Server not setup. Please use the setup command,');
return CommandResponse.ServerNotSetup;
}
return CommandResponse.Ok;
}
public override async execute(context: ICommandContext) {
if (!context.message.guild) {
return;
}
switch (interaction.options.getSubcommand()) {
case 'list':
await this.SendHelpText(interaction);
break;
case 'reset':
await this.ResetValue(interaction);
break;
case 'get':
await this.GetValue(interaction);
break;
case 'set':
await this.SetValue(interaction);
break;
default:
await interaction.reply('Subcommand not found.');
}
}
private async SendHelpText(interaction: CommandInteraction) {
const description = readFileSync(`${process.cwd()}/data/usage/config.txt`).toString();
const embed = new EmbedBuilder()
.setColor(EmbedColours.Ok)
.setTitle("Config")
.setDescription(description);
await interaction.reply({ embeds: [ embed ]});
}
private async GetValue(interaction: CommandInteraction) {
if (!interaction.guildId) return;
const key = interaction.options.get('key');
if (!key || !key.value) {
await interaction.reply('Fields are required.');
return;
}
const server = await Server.FetchOneById<Server>(Server, interaction.guildId, [
const server = await Server.FetchOneById<Server>(Server, context.message.guild?.id, [
"Settings",
]);
if (!server) {
await interaction.reply('Server not found.');
return;
}
const setting = server.Settings.filter(x => x.Key == key.value)[0];
const key = context.args[0];
const action = context.args[1];
const value = context.args.splice(2).join(" ");
if (setting) {
await interaction.reply(`\`${key.value}\`: \`${setting.Value}\``);
if (!key) {
this.SendHelpText(context);
} else if (!action) {
this.GetValue(context, server, key);
} else {
var defaultValue = DefaultValues.GetValue(key.value.toString());
switch(action) {
case 'reset':
this.ResetValue(context, server, key);
break;
case 'set':
if (!value) {
const errorEmbed = new ErrorEmbed(context, "Value is required when setting");
errorEmbed.SendToCurrentChannel();
return;
}
if (defaultValue) {
await interaction.reply(`\`${key.value}\`: \`${defaultValue}\` <DEFAULT>`);
} else {
await interaction.reply(`\`${key.value}\`: <NONE>`);
this.SetValue(context, server, key, value);
break;
default:
const errorEmbed = new ErrorEmbed(context, "Action must be either set or reset");
errorEmbed.SendToCurrentChannel();
return;
}
}
}
private async ResetValue(interaction: CommandInteraction) {
if (!interaction.guildId) return;
private async SendHelpText(context: ICommandContext) {
const description = readFileSync(`${process.cwd()}/data/config.txt`).toString();
const key = interaction.options.get('key');
const embed = new PublicEmbed(context, "Config", description);
if (!key || !key.value) {
await interaction.reply('Fields are required.');
return;
embed.SendToCurrentChannel();
}
private async GetValue(context: ICommandContext, server: Server, key: string) {
const setting = server.Settings.filter(x => x.Key == key)[0];
if (setting) {
const embed = new PublicEmbed(context, "", `${key}: ${setting.Value}`);
embed.SendToCurrentChannel();
} else {
const embed = new PublicEmbed(context, "", `${key}: ${DefaultValues.GetValue(key)} <DEFAULT>`);
embed.SendToCurrentChannel();
}
}
const server = await Server.FetchOneById<Server>(Server, interaction.guildId, [
"Settings",
]);
if (!server) {
await interaction.reply('Server not found.');
return;
}
const setting = server.Settings.filter(x => x.Key == key.value)[0];
private async ResetValue(context: ICommandContext, server: Server, key: string) {
const setting = server.Settings.filter(x => x.Key == key)[0];
if (!setting) {
await interaction.reply('Setting not found.');
const embed = new PublicEmbed(context, "", "Setting has been reset");
embed.SendToCurrentChannel();
return;
}
await Setting.Remove(Setting, setting);
await interaction.reply('The setting has been reset to the default.');
const embed = new PublicEmbed(context, "", "Setting has been reset");
embed.SendToCurrentChannel();
}
private async SetValue(interaction: CommandInteraction) {
if (!interaction.guildId) return;
const key = interaction.options.get('key');
const value = interaction.options.get('value');
if (!key || !key.value || !value || !value.value) {
await interaction.reply('Fields are required.');
return;
}
const server = await Server.FetchOneById<Server>(Server, interaction.guildId, [
"Settings",
]);
if (!server) {
await interaction.reply('Server not found.');
return;
}
const setting = server.Settings.filter(x => x.Key == key.value)[0];
private async SetValue(context: ICommandContext, server: Server, key: string, value: string) {
const setting = server.Settings.filter(x => x.Key == key)[0];
if (setting) {
setting.UpdateBasicDetails(key.value.toString(), value.value.toString());
setting.UpdateBasicDetails(key, value);
await setting.Save(Setting, setting);
} else {
const newSetting = new Setting(key.value.toString(), value.value.toString());
const newSetting = new Setting(key, value);
await newSetting.Save(Setting, newSetting);
@ -195,6 +130,7 @@ export default class Config extends Command {
await server.Save(Server, server);
}
await interaction.reply('Setting has been set.');
const embed = new PublicEmbed(context, "", "Setting has been set");
embed.SendToCurrentChannel();
}
}
}

View file

@ -1,91 +1,96 @@
import { CommandInteraction, PermissionsBitField, SlashCommandBuilder } from "discord.js";
import { CommandResponse } from "../constants/CommandResponse";
import { ICommandContext } from "../contracts/ICommandContext";
import ErrorEmbed from "../helpers/embeds/ErrorEmbed";
import PublicEmbed from "../helpers/embeds/PublicEmbed";
import SettingsHelper from "../helpers/SettingsHelper";
import StringTools from "../helpers/StringTools";
import { Command } from "../type/command";
export default class Disable extends Command {
constructor() {
super();
this.CommandBuilder = new SlashCommandBuilder()
.setName('disable')
.setDescription('Disables a command')
.setDefaultMemberPermissions(PermissionsBitField.Flags.Administrator)
.addSubcommand(subcommand =>
subcommand
.setName('add')
.setDescription('Disables a command for the server')
.addStringOption(option =>
option
.setName('name')
.setDescription('The name of the command')
.setRequired(true)))
.addSubcommand(subcommand =>
subcommand
.setName('remove')
.setDescription('Enables a command for the server')
.addStringOption(option =>
option
.setName('name')
.setDescription('The name of the command')
.setRequired(true)));
super._category = "Moderation";
super._roles = [
"moderator"
];
}
public override async execute(interaction: CommandInteraction) {
if (!interaction.isChatInputCommand()) return;
public override async execute(context: ICommandContext) {
const action = context.args[0];
switch (interaction.options.getSubcommand()) {
switch (action) {
case "add":
await this.Add(interaction);
await this.Add(context);
break;
case "remove":
await this.Remove(interaction);
await this.Remove(context);
break;
default:
await interaction.reply('Subcommand not found.');
await this.SendUsage(context);
}
}
private async Add(interaction: CommandInteraction) {
if (!interaction.guildId) return;
const commandName = interaction.options.get('name');
if (!commandName || !commandName.value) {
await interaction.reply('Fields are required.');
return;
}
const disabledCommandsString = await SettingsHelper.GetSetting("commands.disabled", interaction.guildId);
const disabledCommands = disabledCommandsString != "" ? disabledCommandsString?.split(",") : [];
disabledCommands?.push(commandName.value.toString());
await SettingsHelper.SetSetting("commands.disabled", interaction.guildId, disabledCommands!.join(","));
await interaction.reply(`Disabled command ${commandName.value}`);
private async SendUsage(context: ICommandContext) {
const description = [
"USAGE: <add|remove> <name>",
"",
"add: Adds the command name to the server's disabled command string",
"remove: Removes the command name from the server's disabled command string",
"name: The name of the command to enable/disable"
].join("\n");
const embed = new PublicEmbed(context, "", description);
embed.SendToCurrentChannel();
}
private async Remove(interaction: CommandInteraction) {
if (!interaction.guildId) return;
const commandName = interaction.options.get('name');
if (!commandName || !commandName.value) {
await interaction.reply('Fields are required.');
private async Add(context: ICommandContext) {
if (!context.message.guild) {
return;
}
const disabledCommandsString = await SettingsHelper.GetSetting("commands.disabled", interaction.guildId);
const commandName = context.args[1];
if (!commandName) {
this.SendUsage(context);
return;
}
const disabledCommandsString = await SettingsHelper.GetSetting("commands.disabled", context.message.guild.id);
const disabledCommands = disabledCommandsString != "" ? disabledCommandsString?.split(",") : [];
const disabledCommandsInstance = disabledCommands?.findIndex(x => x == commandName.value!.toString());
disabledCommands?.push(commandName);
await SettingsHelper.SetSetting("commands.disabled", context.message.guild.id, disabledCommands!.join(","));
const embed = new PublicEmbed(context, "", `Disabled command: ${commandName}`);
embed.SendToCurrentChannel();
}
private async Remove(context: ICommandContext) {
if (!context.message.guild) {
return;
}
const commandName = context.args[1];
if (!commandName) {
this.SendUsage(context);
return;
}
const disabledCommandsString = await SettingsHelper.GetSetting("commands.disabled", context.message.guild.id);
const disabledCommands = disabledCommandsString != "" ? disabledCommandsString?.split(",") : [];
const disabledCommandsInstance = disabledCommands?.findIndex(x => x == commandName);
if (disabledCommandsInstance! > -1) {
disabledCommands?.splice(disabledCommandsInstance!, 1);
}
await SettingsHelper.SetSetting("commands.disabled", interaction.guildId, disabledCommands!.join(","));
await SettingsHelper.SetSetting("commands.disabled", context.message.guild.id, disabledCommands!.join(","));
await interaction.reply(`Enabled command ${commandName.value}`);
const embed = new PublicEmbed(context, "", `Enabled command: ${commandName}`);
embed.SendToCurrentChannel();
}
}

47
src/commands/eval.ts Normal file
View file

@ -0,0 +1,47 @@
import { ICommandContext } from "../contracts/ICommandContext";
import ICommandReturnContext from "../contracts/ICommandReturnContext";
import ErrorEmbed from "../helpers/embeds/ErrorEmbed";
import PublicEmbed from "../helpers/embeds/PublicEmbed";
import { Command } from "../type/command";
export default class Evaluate extends Command {
constructor() {
super();
super._category = "Owner";
}
public override execute(context: ICommandContext): ICommandReturnContext {
if (context.message.author.id != process.env.BOT_OWNERID) {
return {
commandContext: context,
embeds: []
};
}
const stmt = context.args;
console.log(`Eval Statement: ${stmt.join(" ")}`);
try {
const result = eval(stmt.join(" "));
const embed = new PublicEmbed(context, "", result);
embed.SendToCurrentChannel();
return {
commandContext: context,
embeds: [embed]
};
}
catch (err: any) {
const errorEmbed = new ErrorEmbed(context, err);
errorEmbed.SendToCurrentChannel();
return {
commandContext: context,
embeds: [errorEmbed]
};
}
}
}

122
src/commands/help.ts Normal file
View file

@ -0,0 +1,122 @@
import { existsSync, readdirSync } from "fs";
import { ICommandContext } from "../contracts/ICommandContext";
import ErrorEmbed from "../helpers/embeds/ErrorEmbed";
import PublicEmbed from "../helpers/embeds/PublicEmbed";
import StringTools from "../helpers/StringTools";
import ICommandReturnContext from "../contracts/ICommandReturnContext";
import { Command } from "../type/command";
export interface ICommandData {
Exists: boolean;
Name?: string;
Category?: string;
Roles?: string[];
}
export default class Help extends Command {
constructor() {
super();
super._category = "General";
}
public override execute(context: ICommandContext): ICommandReturnContext {
if (context.args.length == 0) {
return this.SendAll(context);
} else {
return this.SendSingle(context);
}
}
public SendAll(context: ICommandContext): ICommandReturnContext {
const allCommands = this.GetAllCommandData();
const cateogries = [...new Set(allCommands.map(x => x.Category!))];;
const embed = new PublicEmbed(context, "Commands", "");
cateogries.forEach(category => {
let filtered = allCommands.filter(x => x.Category == category);
embed.addField(StringTools.Capitalise(category), filtered.flatMap(x => x.Name).join(", "));
});
embed.SendToCurrentChannel();
return {
commandContext: context,
embeds: [ embed ]
};
}
public SendSingle(context: ICommandContext): ICommandReturnContext {
const command = this.GetCommandData(context.args[0]);
if (!command.Exists) {
const errorEmbed = new ErrorEmbed(context, "Command does not exist");
errorEmbed.SendToCurrentChannel();
return {
commandContext: context,
embeds: [ errorEmbed ]
};
}
const embed = new PublicEmbed(context, StringTools.Capitalise(command.Name!), "");
embed.addField("Category", StringTools.Capitalise(command.Category!));
embed.addField("Required Roles", StringTools.Capitalise(command.Roles!.join(", ")) || "*none*");
embed.SendToCurrentChannel();
return {
commandContext: context,
embeds: [ embed ]
};
}
public GetAllCommandData(): ICommandData[] {
const result: ICommandData[] = [];
const folder = process.env.FOLDERS_COMMANDS!;
const contents = readdirSync(`${process.cwd()}/${folder}`);
contents.forEach(name => {
const file = require(`${process.cwd()}/${folder}/${name}`).default;
const command = new file() as Command;
const data: ICommandData = {
Exists: true,
Name: name.replace(".ts", ""),
Category: command._category || "none",
Roles: command._roles,
};
result.push(data);
});
return result;
}
public GetCommandData(name: string): ICommandData {
const folder = process.env.FOLDERS_COMMANDS!;
const path = `${process.cwd()}/${folder}/${name}.ts`;
if (!existsSync(path)) {
return {
Exists: false
};
}
const file = require(path).default;
const command = new file() as Command;
const data: ICommandData = {
Exists: true,
Name: name,
Category: command._category || "none",
Roles: command._roles
};
return data;
}
}

View file

@ -1,39 +0,0 @@
import { CommandInteraction, PermissionsBitField, SlashCommandBuilder } from "discord.js";
import IgnoredChannel from "../database/entities/IgnoredChannel";
import { Command } from "../type/command";
export default class Ignore extends Command {
constructor() {
super();
this.CommandBuilder = new SlashCommandBuilder()
.setName('ignore')
.setDescription('Ignore events in this channel')
.setDefaultMemberPermissions(PermissionsBitField.Flags.Administrator);
}
public override async execute(interaction: CommandInteraction) {
if (!interaction.guildId) return;
const isChannelIgnored = await IgnoredChannel.IsChannelIgnored(interaction.guildId);
if (isChannelIgnored) {
const entity = await IgnoredChannel.FetchOneById(IgnoredChannel, interaction.guildId);
if (!entity) {
await interaction.reply('Unable to find channel.');
return;
}
await IgnoredChannel.Remove(IgnoredChannel, entity);
await interaction.reply('This channel will start being logged again.');
} else {
const entity = new IgnoredChannel(interaction.guildId);
await entity.Save(IgnoredChannel, entity);
await interaction.reply('This channel will now be ignored from logging.');
}
}
}

View file

@ -1,101 +1,83 @@
import ErrorMessages from "../constants/ErrorMessages";
import { ICommandContext } from "../contracts/ICommandContext";
import ICommandReturnContext from "../contracts/ICommandReturnContext";
import ErrorEmbed from "../helpers/embeds/ErrorEmbed";
import LogEmbed from "../helpers/embeds/LogEmbed";
import PublicEmbed from "../helpers/embeds/PublicEmbed";
import { Command } from "../type/command";
import Audit from "../database/entities/Audit";
import { AuditType } from "../constants/AuditType";
import { CommandInteraction, EmbedBuilder, GuildMember, PermissionsBitField, SlashCommandBuilder, TextChannel } from "discord.js";
import EmbedColours from "../constants/EmbedColours";
import SettingsHelper from "../helpers/SettingsHelper";
export default class Kick extends Command {
constructor() {
super();
this.CommandBuilder = new SlashCommandBuilder()
.setName("kick")
.setDescription("Kick a member from the server with an optional reason")
.setDefaultMemberPermissions(PermissionsBitField.Flags.KickMembers)
.addUserOption(option =>
option
.setName('target')
.setDescription('The user')
.setRequired(true))
.addStringOption(option =>
option
.setName('reason')
.setDescription('The reason'));
super._category = "Moderation";
super._roles = [
"moderator"
];
}
public override async execute(interaction: CommandInteraction) {
if (!interaction.isChatInputCommand()) return;
if (!interaction.guildId) return;
if (!interaction.guild) return;
public override async execute(context: ICommandContext): Promise<ICommandReturnContext> {
const targetUser = context.message.mentions.users.first();
const targetUser = interaction.options.get('target');
const reasonInput = interaction.options.get('reason');
if (!targetUser) {
const embed = new ErrorEmbed(context, "User does not exist");
embed.SendToCurrentChannel();
if (!targetUser || !targetUser.user || !targetUser.member) {
await interaction.reply("User not found.");
return;
return {
commandContext: context,
embeds: [embed]
};
}
const member = targetUser.member as GuildMember;
const reason = reasonInput && reasonInput.value ? reasonInput.value.toString() : "*none*";
const targetMember = context.message.guild?.member(targetUser);
const logEmbed = new EmbedBuilder()
.setColor(EmbedColours.Ok)
.setTitle("Member Kicked")
.setDescription(`<@${targetUser.user.id}> \`${targetUser.user.tag}\``)
.setThumbnail(targetUser.user.avatarURL())
.addFields([
{
name: "Moderator",
value: `<@${interaction.user.id}>`,
},
{
name: "Reason",
value: reason,
},
]);
if (!member.kickable) {
await interaction.reply('Insufficient permissions. Please contact a moderator.');
return;
if (!targetMember) {
const embed = new ErrorEmbed(context, "User is not in this server");
embed.SendToCurrentChannel();
return {
commandContext: context,
embeds: [embed]
};
}
await member.kick();
const channelName = await SettingsHelper.GetSetting('channels.logs.mod', interaction.guildId);
if (!channelName) return;
const channel = interaction.guild.channels.cache.find(x => x.name == channelName) as TextChannel;
if (channel) {
await channel.send({ embeds: [ logEmbed ]});
const reasonArgs = context.args;
reasonArgs.splice(0, 1)
const reason = reasonArgs.join(" ");
if (!context.message.guild?.available) {
return {
commandContext: context,
embeds: []
};
}
const dmEmbed = new EmbedBuilder()
.setColor(EmbedColours.Ok)
.setTitle(interaction.guild.name)
.setDescription("You have been kicked from the server.")
.addFields([
{
name: "Reason",
value: reason,
},
]);
let replyText = "Successfully kicked user.";
try {
const dmChannel = await targetUser.user!.createDM();
await dmChannel.send({ embeds: [ dmEmbed ] });
} catch {
replyText += " *Note: I was unable to DM the user the reason.*";
if (!targetMember.kickable) {
const embed = new ErrorEmbed(context, ErrorMessages.InsufficientBotPermissions);
embed.SendToCurrentChannel();
return {
commandContext: context,
embeds: [embed]
};
}
const audit = new Audit(targetUser.user.id, AuditType.Kick, reason, interaction.user.id, interaction.guildId);
await audit.Save(Audit, audit);
const logEmbed = new LogEmbed(context, "Member Kicked");
logEmbed.AddUser("User", targetUser, true);
logEmbed.AddUser("Moderator", context.message.author);
logEmbed.AddReason(reason);
await interaction.reply(replyText);
const publicEmbed = new PublicEmbed(context, "", `${targetUser} has been kicked`);
await targetMember.kick(`Moderator: ${context.message.author.tag}, Reason: ${reason || "*none*"}`);
await logEmbed.SendToModLogsChannel();
publicEmbed.SendToCurrentChannel();
return {
commandContext: context,
embeds: [logEmbed, publicEmbed]
};
}
}

View file

@ -1,30 +0,0 @@
import { CommandInteraction, PermissionFlagsBits, SlashCommandBuilder } from "discord.js";
import { Command } from "../type/command";
import SettingsHelper from "../helpers/SettingsHelper";
export default class Linkonly extends Command {
constructor() {
super();
this.CommandBuilder = new SlashCommandBuilder()
.setName("linkonly")
.setDescription("Set the link only channel, leave blank to disable")
.setDefaultMemberPermissions(PermissionFlagsBits.ManageMessages)
.addChannelOption(x => x
.setName("channel")
.setDescription("The channel"));
}
public override async execute(interaction: CommandInteraction) {
if (!interaction.guild) return;
const channel = interaction.options.get("channel")?.channel;
const channelid = channel?.id ?? "";
const channelName = channel?.name ?? "<NONE>";
await SettingsHelper.SetSetting("channel.linkonly", interaction.guild.id, channelid);
await interaction.reply(`Set the link only channel to \`${channelName}\``);
}
}

96
src/commands/mute.ts Normal file
View file

@ -0,0 +1,96 @@
import ErrorMessages from "../constants/ErrorMessages";
import { ICommandContext } from "../contracts/ICommandContext";
import ICommandReturnContext from "../contracts/ICommandReturnContext";
import ErrorEmbed from "../helpers/embeds/ErrorEmbed";
import LogEmbed from "../helpers/embeds/LogEmbed";
import PublicEmbed from "../helpers/embeds/PublicEmbed";
import { Command } from "../type/command";
export default class Mute extends Command {
constructor() {
super();
super._category = "Moderation";
super._roles = [
"moderator"
];
}
public override async execute(context: ICommandContext): Promise<ICommandReturnContext> {
const targetUser = context.message.mentions.users.first();
if (!targetUser) {
const embed = new ErrorEmbed(context, "User does not exist");
embed.SendToCurrentChannel();
return {
commandContext: context,
embeds: [embed]
};
}
const targetMember = context.message.guild?.member(targetUser);
if (!targetMember) {
const embed = new ErrorEmbed(context, "User is not in this server");
embed.SendToCurrentChannel();
return {
commandContext: context,
embeds: [embed]
};
}
const reasonArgs = context.args;
reasonArgs.splice(0, 1);
const reason = reasonArgs.join(" ");
if (!context.message.guild?.available) {
return {
commandContext: context,
embeds: []
};
}
if (!targetMember.manageable) {
const embed = new ErrorEmbed(context, ErrorMessages.InsufficientBotPermissions);
embed.SendToCurrentChannel();
return {
commandContext: context,
embeds: [embed]
};
}
const logEmbed = new LogEmbed(context, "Member Muted");
logEmbed.AddUser("User", targetUser, true)
logEmbed.AddUser("Moderator", context.message.author);
logEmbed.AddReason(reason);
const publicEmbed = new PublicEmbed(context, "", `${targetUser} has been muted`);
publicEmbed.AddReason(reason);
const mutedRole = context.message.guild.roles.cache.find(role => role.name == process.env.ROLES_MUTED);
if (!mutedRole) {
const embed = new ErrorEmbed(context, ErrorMessages.RoleNotFound);
embed.SendToCurrentChannel();
return {
commandContext: context,
embeds: [embed]
};
}
await targetMember.roles.add(mutedRole, `Moderator: ${context.message.author.tag}, Reason: ${reason || "*none*"}`);
await logEmbed.SendToModLogsChannel();
publicEmbed.SendToCurrentChannel();
return {
commandContext: context,
embeds: [logEmbed, publicEmbed]
};
}
}

View file

@ -1,62 +1,31 @@
import { CommandInteraction, SlashCommandBuilder } from "discord.js";
import { ICommandContext } from "../contracts/ICommandContext";
import ICommandReturnContext from "../contracts/ICommandReturnContext";
import ErrorEmbed from "../helpers/embeds/ErrorEmbed";
import PublicEmbed from "../helpers/embeds/PublicEmbed";
import { Command } from "../type/command";
import { EmbedBuilder } from "@discordjs/builders";
import EmbedColours from "../constants/EmbedColours";
export default class Poll extends Command {
constructor() {
super();
this.CommandBuilder = new SlashCommandBuilder()
.setName('poll')
.setDescription('Run a poll, automatically adding reaction emojis as options')
.addStringOption(option =>
option
.setName('title')
.setDescription('Title of the poll')
.setRequired(true))
.addStringOption(option =>
option
.setName('option1')
.setDescription('Option 1')
.setRequired(true))
.addStringOption(option =>
option
.setName('option2')
.setDescription('Option 2')
.setRequired(true))
.addStringOption(option =>
option
.setName('option3')
.setDescription('Option 3'))
.addStringOption(option =>
option
.setName('option4')
.setDescription('Option 4'))
.addStringOption(option =>
option
.setName('option5')
.setDescription('Option 5'));
super._category = "General";
}
public override async execute(interaction: CommandInteraction) {
const title = interaction.options.get('title');
const option1 = interaction.options.get('option1');
const option2 = interaction.options.get('option2');
const option3 = interaction.options.get('option3');
const option4 = interaction.options.get('option4');
const option5 = interaction.options.get('option5');
public override async execute(context: ICommandContext): Promise<ICommandReturnContext> {
const argsJoined = context.args.join(" ");
const argsSplit = argsJoined.split(";");
if (!title || !option1 || !option2) return;
if (argsSplit.length < 3 || argsSplit.length > 10) {
const errorEmbed = new ErrorEmbed(context, "Usage: <title>;<option 1>;<option 2>... (separate options with semicolons), maximum of 9 options");
errorEmbed.SendToCurrentChannel();
return {
commandContext: context,
embeds: [errorEmbed]
};
}
const description = [
option1.value as string,
option2.value as string,
option3?.value as string,
option4?.value as string,
option5?.value as string
]
.filter(x => x != null);
const title = argsSplit[0];
const arrayOfNumbers = [
':one:',
@ -64,28 +33,35 @@ export default class Poll extends Command {
':three:',
':four:',
':five:',
':six:',
':seven:',
':eight:',
':nine:'
];
const reactionEmojis = ["1⃣", "2⃣", "3⃣", "4⃣", "5⃣", "6⃣", "7⃣", "8⃣", "9⃣"];
const description = arrayOfNumbers.splice(0, argsSplit.length - 1);
description.forEach((value, index) => {
description[index] = `${reactionEmojis[index]} ${description[index]}`;
description[index] = `${value} ${argsSplit[index + 1]}`;
});
const embed = new EmbedBuilder()
.setColor(EmbedColours.Ok)
.setTitle(title.value as string)
.setDescription(description.join('\n'))
.setFooter({
text: `Poll by ${interaction.user.username}`,
iconURL: interaction.user.avatarURL()!,
});
const embed = new PublicEmbed(context, title, description.join("\n"));
const message = await interaction.reply({ embeds: [ embed ]});
const message = await context.message.channel.send(embed);
description.forEach(async (value, index) => {
await (await message.fetch()).react(reactionEmojis[index]);
await message.react(reactionEmojis[index]);
});
if (context.message.deletable) {
await context.message.delete({ reason: "Poll command" });
}
return {
commandContext: context,
embeds: [embed]
};
}
}

99
src/commands/role.ts Normal file
View file

@ -0,0 +1,99 @@
import ErrorEmbed from "../helpers/embeds/ErrorEmbed";
import PublicEmbed from "../helpers/embeds/PublicEmbed";
import { Role as DiscordRole } from "discord.js";
import { Command } from "../type/command";
import { ICommandContext } from "../contracts/ICommandContext";
import ICommandReturnContext from "../contracts/ICommandReturnContext";
export default class Role extends Command {
constructor() {
super();
super._category = "General";
}
public override async execute(context: ICommandContext) {
const roles = process.env.COMMANDS_ROLE_ROLES!.split(',');
if (context.args.length == 0) {
this.SendRolesList(context, roles);
} else {
await this.ToggleRole(context, roles);
}
}
public SendRolesList(context: ICommandContext, roles: String[]): ICommandReturnContext {
const description = `Do ${process.env.BOT_PREFIX}role <role> to get the role!\n${roles.join('\n')}`;
const embed = new PublicEmbed(context, "Roles", description);
embed.SendToCurrentChannel();
return {
commandContext: context,
embeds: [embed]
};
}
public async ToggleRole(context: ICommandContext, roles: String[]): Promise<ICommandReturnContext> {
const requestedRole = context.args[0];
if (!roles.includes(requestedRole)) {
const errorEmbed = new ErrorEmbed(context, "This role isn't marked as assignable, to see a list of assignable roles, run this command without any parameters");
errorEmbed.SendToCurrentChannel();
return {
commandContext: context,
embeds: [errorEmbed]
};
}
const assignRole = context.message.guild?.roles.cache.find(x => x.name == requestedRole);
if (!assignRole) {
const errorEmbed = new ErrorEmbed(context, "The current server doesn't have this role. Please contact the server's moderators");
errorEmbed.SendToCurrentChannel();
return {
commandContext: context,
embeds: [errorEmbed]
};
}
const role = context.message.member?.roles.cache.find(x => x.name == requestedRole)
if (!role) {
await this.AddRole(context, assignRole);
} else {
await this.RemoveRole(context, assignRole);
}
return {
commandContext: context,
embeds: []
};
}
public async AddRole(context: ICommandContext, role: DiscordRole): Promise<ICommandReturnContext> {
await context.message.member?.roles.add(role, "Toggled with role command");
const embed = new PublicEmbed(context, "", `Gave role: ${role.name}`);
embed.SendToCurrentChannel();
return {
commandContext: context,
embeds: [embed]
};
}
public async RemoveRole(context: ICommandContext, role: DiscordRole): Promise<ICommandReturnContext> {
await context.message.member?.roles.remove(role, "Toggled with role command");
const embed = new PublicEmbed(context, "", `Removed role: ${role.name}`);
embed.SendToCurrentChannel();
return {
commandContext: context,
embeds: [embed]
};
}
}

View file

@ -1,8 +1,9 @@
import { ActionRowBuilder, ButtonBuilder, ButtonStyle, CommandInteraction, EmbedBuilder, PermissionsBitField, SlashCommandBuilder, TextChannel } from "discord.js";
import { existsSync, readFileSync } from "fs";
import EmbedColours from "../constants/EmbedColours";
import { ICommandContext } from "../contracts/ICommandContext";
import ICommandReturnContext from "../contracts/ICommandReturnContext";
import ErrorEmbed from "../helpers/embeds/ErrorEmbed";
import PublicEmbed from "../helpers/embeds/PublicEmbed";
import { Command } from "../type/command";
import SettingsHelper from "../helpers/SettingsHelper";
interface IRules {
title?: string;
@ -15,109 +16,42 @@ export default class Rules extends Command {
constructor() {
super();
this.CommandBuilder = new SlashCommandBuilder()
.setName('rules')
.setDescription("Rules-related commands")
.setDefaultMemberPermissions(PermissionsBitField.Flags.Administrator)
.addSubcommand(x =>
x
.setName('embeds')
.setDescription('Send the rules embeds for this server'))
.addSubcommand(x =>
x
.setName('access')
.setDescription('Send the server verification embed button'));
super._category = "Admin";
super._roles = [
"administrator"
];
}
public override async execute(interaction: CommandInteraction) {
if (!interaction.isChatInputCommand()) return;
public override execute(context: ICommandContext): ICommandReturnContext {
if (!existsSync(`${process.cwd()}/data/rules/${context.message.guild?.id}.json`)) {
const errorEmbed = new ErrorEmbed(context, "Rules file doesn't exist");
errorEmbed.SendToCurrentChannel();
switch (interaction.options.getSubcommand()) {
case "embeds":
await this.SendEmbeds(interaction);
break;
case "access":
await this.SendAccessButton(interaction);
break;
default:
await interaction.reply("Subcommand doesn't exist.");
}
}
private async SendEmbeds(interaction: CommandInteraction) {
if (!interaction.guildId) return;
if (!existsSync(`${process.cwd()}/data/rules/${interaction.guildId}.json`)) {
await interaction.reply('Rules file doesn\'t exist.');
return;
return {
commandContext: context,
embeds: [errorEmbed]
};
}
const rulesFile = readFileSync(`${process.cwd()}/data/rules/${interaction.guildId}.json`).toString();
const rulesFile = readFileSync(`${process.cwd()}/data/rules/${context.message.guild?.id}.json`).toString();
const rules = JSON.parse(rulesFile) as IRules[];
const embeds: EmbedBuilder[] = [];
if (rules.length == 0) {
await interaction.reply({ content: "No rules have been supplied within code base for this server.", ephemeral: true });
return;
}
const embeds: PublicEmbed[] = [];
rules.forEach(rule => {
const embed = new EmbedBuilder()
.setColor(EmbedColours.Ok)
.setTitle(rule.title || "Rules")
.setDescription(rule.description ? rule.description.join("\n") : "*none*");
const embed = new PublicEmbed(context, rule.title || "", rule.description?.join("\n") || "");
if (rule.image) {
embed.setImage(rule.image);
}
if (rule.footer) {
embed.setFooter({ text: rule.footer });
}
embed.setImage(rule.image || "");
embed.setFooter(rule.footer || "");
embeds.push(embed);
});
const channel = interaction.channel as TextChannel;
embeds.forEach(x => x.SendToCurrentChannel());
if (!channel) {
await interaction.reply({ content: "Channel not found.", ephemeral: true });
return;
}
await channel.send({ embeds: embeds });
const successEmbed = new EmbedBuilder()
.setColor(EmbedColours.Ok)
.setTitle("Success")
.setDescription("The rules have sent to this channel successfully");
await interaction.reply({ embeds: [ successEmbed ], ephemeral: true });
}
private async SendAccessButton(interaction: CommandInteraction) {
if (!interaction.guildId) return;
const buttonLabel = await SettingsHelper.GetSetting("rules.access.label", interaction.guildId);
const row = new ActionRowBuilder<ButtonBuilder>()
.addComponents([
new ButtonBuilder()
.setCustomId('verify')
.setStyle(ButtonStyle.Primary)
.setLabel(buttonLabel || "Verify")
]);
const channel = interaction.channel as TextChannel;
await channel.send({
components: [ row ]
});
await interaction.reply({
content: "Success",
ephemeral: true,
});
return {
commandContext: context,
embeds: embeds
};
}
}

View file

@ -1,24 +0,0 @@
import { ActionRowBuilder, ButtonBuilder, ButtonStyle, CommandInteraction, EmbedBuilder, SlashCommandBuilder } from "discord.js";
import EmbedColours from "../constants/EmbedColours";
import { Command } from "../type/command";
export default class Say extends Command {
constructor() {
super();
this.CommandBuilder = new SlashCommandBuilder()
.setName('say')
.setDescription('Have the bot reply with your message')
.addStringOption(x =>
x
.setName("message")
.setDescription("The message to repeat")
.setRequired(true));
}
public override async execute(interaction: CommandInteraction) {
const message = interaction.options.get("message", true);
await interaction.reply(message.value as string);
}
}

View file

@ -1,31 +1,37 @@
import { CommandInteraction, PermissionsBitField, SlashCommandBuilder } from "discord.js";
import Server from "../database/entities/Server";
import { ICommandContext } from "../contracts/ICommandContext";
import ICommandReturnContext from "../contracts/ICommandReturnContext";
import Server from "../entity/Server";
import ErrorEmbed from "../helpers/embeds/ErrorEmbed";
import PublicEmbed from "../helpers/embeds/PublicEmbed";
import { Command } from "../type/command";
export default class Setup extends Command {
constructor() {
super();
this.CommandBuilder = new SlashCommandBuilder()
.setName('setup')
.setDescription('Makes the server ready to be configured')
.setDefaultMemberPermissions(PermissionsBitField.Flags.Administrator);
super._category = "Administration";
super._roles = [
"moderator"
]
}
public override async execute(interaction: CommandInteraction) {
if (!interaction.guildId) return;
const server = await Server.FetchOneById(Server, interaction.guildId);
if (server) {
await interaction.reply('This server has already been setup, please configure using the config command.');
public override async execute(context: ICommandContext) {
if (!context.message.guild) {
return;
}
const newServer = new Server(interaction.guildId);
const server = await Server.FetchOneById(Server, context.message.guild?.id);
if (server) {
const embed = new ErrorEmbed(context, "This server has already been setup, please configure using the config command");
embed.SendToCurrentChannel();
return;
}
const newServer = new Server(context.message.guild?.id);
await newServer.Save(Server, newServer);
await interaction.reply('Success, please configure using the configure command.');
const embed = new PublicEmbed(context, "Success", "Please configure using the config command");
embed.SendToCurrentChannel();
}
}

View file

@ -1,133 +0,0 @@
import { CacheType, CommandInteraction, EmbedBuilder, GuildMember, PermissionsBitField, SlashCommandBuilder, TextChannel } from "discord.js";
import { AuditType } from "../constants/AuditType";
import EmbedColours from "../constants/EmbedColours";
import Audit from "../database/entities/Audit";
import SettingsHelper from "../helpers/SettingsHelper";
import TimeLengthInput from "../helpers/TimeLengthInput";
import { Command } from "../type/command";
export default class Timeout extends Command {
constructor() {
super();
this.CommandBuilder = new SlashCommandBuilder()
.setName("timeout")
.setDescription("Timeouts a user out, sending them a DM with the reason if possible")
.setDefaultMemberPermissions(PermissionsBitField.Flags.ModerateMembers)
.addUserOption(option =>
option
.setName('target')
.setDescription('The user')
.setRequired(true))
.addStringOption(option =>
option
.setName("length")
.setDescription("How long to timeout for? (Example: 24h, 60m)")
.setRequired(true))
.addStringOption(option =>
option
.setName('reason')
.setDescription('The reason'));
}
public override async execute(interaction: CommandInteraction<CacheType>) {
if (!interaction.guild || !interaction.guildId) return;
// Interaction Inputs
const targetUser = interaction.options.get('target');
const lengthInput = interaction.options.get('length');
const reasonInput = interaction.options.get('reason');
// Validation
if (!targetUser || !targetUser.user || !targetUser.member) {
await interaction.reply('Fields are required.');
return;
}
if (!lengthInput || !lengthInput.value) {
await interaction.reply('Fields are required.');
return;
}
if (targetUser.user.bot) {
await interaction.reply('Cannot timeout bots.');
return;
}
// General Variables
const targetMember = targetUser.member as GuildMember;
const reason = reasonInput && reasonInput.value ? reasonInput.value.toString() : null;
const timeLength = new TimeLengthInput(lengthInput.value.toString());
const logEmbed = new EmbedBuilder()
.setColor(EmbedColours.Ok)
.setTitle("Member Timed Out")
.setDescription(`<@${targetUser.user.id}> \`${targetUser.user.tag}\``)
.setThumbnail(targetUser.user.avatarURL())
.addFields([
{
name: "Moderator",
value: `<@${interaction.user.id}>`,
},
{
name: "Reason",
value: reason || "*none*",
},
{
name: "Length",
value: timeLength.GetLengthShort(),
},
{
name: "Until",
value: timeLength.GetDateFromNow().toString(),
},
]);
// Bot Permissions Check
if (!targetMember.manageable) {
await interaction.reply('Insufficient bot permissions. Please contact a moderator.');
return;
}
// Execute Timeout
await targetMember.timeout(timeLength.GetMilliseconds(), reason || "");
// Log Embed To Channel
const channelName = await SettingsHelper.GetSetting('channels.logs.mod', interaction.guildId);
if (!channelName) return;
const channel = interaction.guild.channels.cache.find(x => x.name == channelName) as TextChannel;
if (channel) {
await channel.send({ embeds: [ logEmbed ]});
}
const dmEmbed = new EmbedBuilder()
.setColor(EmbedColours.Ok)
.setTitle(interaction.guild.name)
.setDescription("You have been given a warning by a moderator.")
.addFields([
{
name: "Reason",
value: reason || "*none*",
},
]);
let replyText = "Successfully warned user.";
try {
const dmChannel = await targetUser.user!.createDM();
await dmChannel.send({ embeds: [ dmEmbed ] });
} catch {
replyText += " *Note: I was unable to DM the user the reason.*";
}
// Create Audit
const audit = new Audit(targetUser.user.id, AuditType.Timeout, reason || "*none*", interaction.user.id, interaction.guildId);
await audit.Save(Audit, audit);
await interaction.reply(replyText);
}
}

96
src/commands/unmute.ts Normal file
View file

@ -0,0 +1,96 @@
import ErrorMessages from "../constants/ErrorMessages";
import { ICommandContext } from "../contracts/ICommandContext";
import ICommandReturnContext from "../contracts/ICommandReturnContext";
import ErrorEmbed from "../helpers/embeds/ErrorEmbed";
import LogEmbed from "../helpers/embeds/LogEmbed";
import PublicEmbed from "../helpers/embeds/PublicEmbed";
import { Command } from "../type/command";
export default class Unmute extends Command {
constructor() {
super();
super._category = "Moderation";
super._roles = [
"moderator"
];
}
public override async execute(context: ICommandContext): Promise<ICommandReturnContext> {
const targetUser = context.message.mentions.users.first();
if (!targetUser) {
const embed = new ErrorEmbed(context, "User does not exist");
embed.SendToCurrentChannel();
return {
commandContext: context,
embeds: [embed]
};
}
const targetMember = context.message.guild?.member(targetUser);
if (!targetMember) {
const embed = new ErrorEmbed(context, "User is not in this server");
embed.SendToCurrentChannel();
return {
commandContext: context,
embeds: [embed]
};
}
const reasonArgs = context.args;
reasonArgs.splice(0, 1);
const reason = reasonArgs.join(" ");
if (!context.message.guild?.available) {
return {
commandContext: context,
embeds: []
};
}
if (!targetMember.manageable) {
const embed = new ErrorEmbed(context, ErrorMessages.InsufficientBotPermissions);
embed.SendToCurrentChannel();
return {
commandContext: context,
embeds: [embed]
};
}
const logEmbed = new LogEmbed(context, "Member Unmuted");
logEmbed.AddUser("User", targetUser, true)
logEmbed.AddUser("Moderator", context.message.author);
logEmbed.AddReason(reason);
const publicEmbed = new PublicEmbed(context, "", `${targetUser} has been unmuted`);
publicEmbed.AddReason(reason);
const mutedRole = context.message.guild.roles.cache.find(role => role.name == process.env.ROLES_MUTED);
if (!mutedRole) {
const embed = new ErrorEmbed(context, ErrorMessages.RoleNotFound);
embed.SendToCurrentChannel();
return {
commandContext: context,
embeds: [embed]
};
}
await targetMember.roles.remove(mutedRole, `Moderator: ${context.message.author.tag}, Reason: ${reason || "*none*"}`);
await logEmbed.SendToModLogsChannel();
publicEmbed.SendToCurrentChannel();
return {
commandContext: context,
embeds: [logEmbed, publicEmbed]
};
}
}

View file

@ -1,91 +1,71 @@
import { CommandInteraction, EmbedBuilder, PermissionsBitField, SlashCommandBuilder, TextChannel } from "discord.js";
import { AuditType } from "../constants/AuditType";
import EmbedColours from "../constants/EmbedColours";
import Audit from "../database/entities/Audit";
import SettingsHelper from "../helpers/SettingsHelper";
import { ICommandContext } from "../contracts/ICommandContext";
import ICommandReturnContext from "../contracts/ICommandReturnContext";
import ErrorEmbed from "../helpers/embeds/ErrorEmbed";
import LogEmbed from "../helpers/embeds/LogEmbed";
import PublicEmbed from "../helpers/embeds/PublicEmbed";
import { Command } from "../type/command";
export default class Warn extends Command {
constructor() {
super();
this.CommandBuilder = new SlashCommandBuilder()
.setName("warn")
.setDescription("Warns a member in the server with an optional reason")
.setDefaultMemberPermissions(PermissionsBitField.Flags.ModerateMembers)
.addUserOption(option =>
option
.setName('target')
.setDescription('The user')
.setRequired(true))
.addStringOption(option =>
option
.setName('reason')
.setDescription('The reason'));
super._category = "Moderation";
super._roles = [
"moderator"
];
}
public override async execute(interaction: CommandInteraction) {
if (!interaction.guild || !interaction.guildId) return;
public override async execute(context: ICommandContext): Promise<ICommandReturnContext> {
const user = context.message.mentions.users.first();
const targetUser = interaction.options.get('target');
const reasonInput = interaction.options.get('reason');
if (!targetUser || !targetUser.user || !targetUser.member) {
await interaction.reply('Fields are required.');
return;
if (!user) {
const errorEmbed = new ErrorEmbed(context, "User does not exist");
errorEmbed.SendToCurrentChannel();
return {
commandContext: context,
embeds: [errorEmbed]
};
}
const reason = reasonInput && reasonInput.value ? reasonInput.value.toString() : "*none*";
const member = context.message.guild?.member(user);
const logEmbed = new EmbedBuilder()
.setColor(EmbedColours.Ok)
.setTitle("Member Warned")
.setDescription(`<@${targetUser.user.id}> \`${targetUser.user.tag}\``)
.setThumbnail(targetUser.user.avatarURL())
.addFields([
{
name: "Moderator",
value: `<@${interaction.user.id}>`,
},
{
name: "Reason",
value: reason,
},
]);
const channelName = await SettingsHelper.GetSetting('channels.logs.mod', interaction.guildId);
if (!channelName) return;
const channel = interaction.guild.channels.cache.find(x => x.name == channelName) as TextChannel;
if (channel) {
await channel.send({ embeds: [ logEmbed ]});
if (!member) {
const errorEmbed = new ErrorEmbed(context, "User is not in this server");
errorEmbed.SendToCurrentChannel();
return {
commandContext: context,
embeds: [errorEmbed]
};
}
const dmEmbed = new EmbedBuilder()
.setColor(EmbedColours.Ok)
.setTitle(interaction.guild.name)
.setDescription("You have been given a warning by a moderator.")
.addFields([
{
name: "Reason",
value: reason,
},
]);
const reasonArgs = context.args;
reasonArgs.splice(0, 1);
let replyText = "Successfully warned user.";
const reason = reasonArgs.join(" ");
try {
const dmChannel = await targetUser.user!.createDM();
await dmChannel.send({ embeds: [ dmEmbed ] });
} catch {
replyText += " *Note: I was unable to DM the user the reason.*";
if (!context.message.guild?.available) {
return {
commandContext: context,
embeds: []
};
}
const audit = new Audit(targetUser.user.id, AuditType.Warn, reason, interaction.user.id, interaction.guildId);
await audit.Save(Audit, audit);
const logEmbed = new LogEmbed(context, "Member Warned");
logEmbed.AddUser("User", user, true);
logEmbed.AddUser("Moderator", context.message.author);
logEmbed.AddReason(reason);
await interaction.reply(replyText);
const publicEmbed = new PublicEmbed(context, "", `${user} has been warned`);
publicEmbed.AddReason(reason);
await logEmbed.SendToModLogsChannel();
publicEmbed.SendToCurrentChannel();
return {
commandContext: context,
embeds: [logEmbed, publicEmbed]
};
}
}

View file

@ -1,8 +0,0 @@
export enum AuditType {
General,
Warn,
Mute,
Kick,
Ban,
Timeout,
}

View file

@ -0,0 +1,7 @@
export enum CommandResponse {
Ok,
Unauthorised,
ServerNotSetup,
NotInServer,
FeatureDisabled,
}

View file

@ -6,7 +6,7 @@ export default class DefaultValues {
this.SetValues();
const res = this.values.find(x => x.Key == key);
if (!res) {
return undefined;
}
@ -17,7 +17,11 @@ export default class DefaultValues {
private static SetValues() {
if (this.values.length == 0) {
// Bot
this.values.push({ Key: "bot.prefix", Value: process.env.BOT_PREFIX || "v!" })
if (this.useDevPrefix) {
this.values.push({ Key: "bot.prefix", Value: "d!" });
} else {
this.values.push({ Key: "bot.prefix", Value: "v!" });
}
// Commands
this.values.push({ Key: "commands.disabled", Value: "" });
@ -31,7 +35,6 @@ export default class DefaultValues {
// Rules (Command)
this.values.push({ Key: "rules.file", Value: "data/rules/rules" });
this.values.push({ Key: "rules.access.label", Value: "Verify" });
// Channels
this.values.push({ Key: "channels.logs.message", Value: "message-logs" });
@ -43,26 +46,6 @@ export default class DefaultValues {
this.values.push({ Key: "verification.channel", Value: "entry" });
this.values.push({ Key: "verification.role", Value: "Entry" });
this.values.push({ Key: "verification.code", Value: "" });
// Gif Only Mode
this.values.push({ Key: "channel.linkonly", Value: "" })
// Event
this.values.push({ Key: "event.message.delete.enabled", Value: "false" });
this.values.push({ Key: "event.message.delete.channel", Value: "message-logs" });
this.values.push({ Key: "event.message.update.enabled", Value: "false" });
this.values.push({ Key: "event.message.update.channel", Value: "message-logs" });
this.values.push({ Key: "event.member.add.enabled", Value: "false" });
this.values.push({ Key: "event.member.add.channel", Value: "member-logs" });
this.values.push({ Key: "event.member.remove.enabled", Value: "false" });
this.values.push({ Key: "event.member.remove.channel", Value: "member-logs" });
this.values.push({ Key: "event.member.update.enabled", Value: "false" });
this.values.push({ Key: "event.member.update.channel", Value: "member-logs" });
}
}
}

View file

@ -1,7 +0,0 @@
export default class EmbedColours {
public static readonly Ok = 0x3050ba;
public static readonly Warning = 0xffbf00;
public static readonly Danger = 0xd2042d;
public static readonly Moon = 0x50C878;
}

View file

@ -0,0 +1,27 @@
import { CommandResponse } from "./CommandResponse";
export default class ErrorMessages {
public static readonly InsufficientBotPermissions = "Unable to do this action, am I missing permissions?";
public static readonly ChannelNotFound = "Unable to find channel";
public static readonly RoleNotFound = "Unable to find role";
public static readonly UserUnauthorised = "You are not authorised to use this command";
public static readonly ServerNotSetup = "This server hasn't been setup yet, please run the setup command";
public static readonly NotInServer = "This command requires to be ran inside of a server";
public static readonly FeatureDisabled = "This feature is currently disabled by a server moderator";
public static GetErrorMessage(response: CommandResponse): string {
switch (response) {
case CommandResponse.Unauthorised:
return this.UserUnauthorised;
case CommandResponse.ServerNotSetup:
return this.ServerNotSetup;
case CommandResponse.NotInServer:
return this.NotInServer;
case CommandResponse.FeatureDisabled:
return this.FeatureDisabled;
default:
return "";
}
}
}

View file

@ -1,15 +0,0 @@
export enum EventType {
ChannelCreate,
ChannelDelete,
ChannelUpdate,
GuildBanAdd,
GuildBanRemove,
GuildCreate,
GuildMemberAdd,
GuildMemberRemove,
GuildMemberUpdate,
MessageCreate,
MessageDelete,
MessageUpdate,
Ready,
}

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