Compare commits

..

No commits in common. "develop" and "v3.1-alpha1" have entirely different histories.

159 changed files with 11485 additions and 6422 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

@ -7,22 +7,7 @@
# any secret values. # any secret values.
BOT_TOKEN= BOT_TOKEN=
BOT_VER=3.3.0 BOT_VER=3.0.4
BOT_AUTHOR=Vylpes BOT_AUTHOR=Vylpes
BOT_DATE=06 Jul 2022
BOT_OWNERID=147392775707426816 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

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: cp -r . ${{ 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,67 +0,0 @@
name: Deploy To Stage
on:
push:
branches:
- develop
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: cp -r . ${{ 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: ${{ vars.STAGE_BOT_VER }}
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,24 +0,0 @@
name: Test
on:
push:
branches:
- feature/*
- hotfix/*
- 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: [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

1
.gitignore vendored
View file

@ -106,4 +106,3 @@ dist
config.json config.json
.DS_Store .DS_Store
ormconfig.json ormconfig.json
.temp/

0
.gitlab/.gitkeep Normal file
View file

View file

View file

@ -8,14 +8,14 @@ Fixes # (issue)
Please delete options that are not relevant. Please delete options that are not relevant.
- [ ] Bug fix (non-breaking change which fixes an issue) - Bug fix (non-breaking change which fixes an issue)
- [ ] New feature (non-breaking change which adds functionality) - New feature (non-breaking change which adds functionality)
- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected) - Breaking change (fix or feature that would cause existing functionality to not work as expected)
- [ ] This change requires a documentation update - This change requires a documentation update
# How Has This Been Tested? # 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 # 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 commented my code, particularly in hard-to-understand areas
- [ ] I have made corresponding changes to the documentation - [ ] I have made corresponding changes to the documentation
- [ ] My changes generate no new warnings - [ ] 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 - [ ] 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

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) 2023 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

@ -6,7 +6,7 @@
"title": "Vylpes' Den", "title": "Vylpes' Den",
"description": [ "description": [
"Welcome to Vylpes' Den! Make sure to say hi!", "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": [ "description": [
"This server uses a bot made by me, VylBot, to help moderate the server.", "This server uses a bot made by me, VylBot, to help moderate the server.",
"For more information on it, see the GitHub repositories:", "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", "title": "Links",
"description": [ "description": [
"YouTube: https://www.youtube.com/@vylpes", "YouTube: https://www.youtube.com/channel/UCwPlzKwCmP5Q9bCX3fHk2BA",
"Patreon: https://www.patreon.com/vylpes", "Patreon: https://www.patreon.com/vylpes",
"Twitch: https://www.twitch.tv/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": [ "description": [
"*You must follow Discord's TOS, including the rule where", "you must be 13 years or older.", "Welcome to Vylpes' Den! Make sure to say hi!",
"If moderators know you're under 13, we will have to ban you!*", "Invite link: https://discord.gg/UyAhAVp"
"",
"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>."
] ]
},
{
"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!*", "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.", "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

@ -7,6 +7,7 @@ commands.disabled: Disabled commands, separated by commas (Default: "")
role.moderator: The moderator role name (Default: "Moderator") role.moderator: The moderator role name (Default: "Moderator")
role.administrator: The administrator role name (Default: "Administrator") 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") rules.file: The location of the rules file (Default: "data/rules/rules")
@ -25,11 +26,11 @@ event.message.delete.channel: Sets the channel the bot will log message delete e
event.message.update.enabled: Enables/Disables the message delete log event (Default: "false") 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.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.enabled: Enables/Disables the message delete log event (Default: "false")
event.member.add.channel: Sets the channel the bot will log member join events to (Default: "member-logs") event.member.add.channel: Sets the channel the bot will log message delete events to (Default: "member-logs")
event.member.remove.enabled: Enables/Disables the member leave log event (Default: "false") event.member.remove.enabled: Enables/Disables the message delete log event (Default: "false")
event.member.remove.channel: Sets the channel the bot will log member leave events to (Default: "member-logs") event.member.remove.channel: Sets the channel the bot will log message delete events to (Default: "member-logs")
event.member.update.enabled: Enables/Disables the member update log event (Default: "false") event.member.update.enabled: Enables/Disables the message delete log event (Default: "false")
event.member.update.channel: Sets the channel the bot will log member update events to (Default: "member-logs") event.member.update.channel: Sets the channel the bot will log message delete events to (Default: "member-logs")

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,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,17 +1,24 @@
version: "3.9" version: "3.9"
services: services:
# discord:
# build: .
database: database:
image: mysql/mysql-server image: mysql/mysql-server
command: --default-authentication-plugin=mysql_native_password command: --default-authentication-plugin=mysql_native_password
restart: always restart: always
environment: environment:
- MYSQL_DATABASE=$DB_NAME - MYSQL_DATABASE=vylbot
- MYSQL_USER=$DB_AUTH_USER - MYSQL_USER=dev
- MYSQL_PASSWORD=$DB_AUTH_PASS - MYSQL_PASSWORD=dev
- MYSQL_ROOT_PASSWORD=$DB_AUTH_PASS - MYSQL_ROOT_PASSWORD=root
- MYSQL_ROOT_HOST=$DB_ROOT_HOST
ports: ports:
- "$DB_PORT:3306" - 3306:3306
volumes:
- $DB_DATA_LOCATION:/var/lib/mysql 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": false,
"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,51 +1,40 @@
{ {
"name": "vylbot-app", "name": "vylbot-app",
"version": "3.2.3", "version": "3.0.4",
"description": "A discord bot made for Vylpes' Den", "description": "A discord bot made for Vylpes' Den",
"main": "./dist/vylbot", "main": "./dist/vylbot",
"typings": "./dist", "typings": "./dist",
"scripts": { "scripts": {
"clean": "rm -rf node_modules/ dist/",
"build": "tsc", "build": "tsc",
"start": "node ./dist/vylbot", "start": "node ./dist/vylbot",
"test": "jest . --passWithNoTests", "test": "jest",
"db:up": "typeorm migration:run -d dist/database/dataSources/appDataSource.js", "db:up": "typeorm migration:run",
"db:down": "typeorm migration:revert -d dist/database/dataSources/appDataSource.js", "db:down": "typeorm migration:revert"
"db:create": "typeorm migration:create ./src/database/migrations",
"release": "np --no-publish"
}, },
"repository": { "repository": {
"type": "git", "type": "git",
"url": "https://github.com/Vylpes/vylbot-app" "url": "git+https://github.com/Vylpes/vylbot-app.git"
}, },
"author": "Vylpes", "author": "Vylpes",
"license": "MIT", "license": "MIT",
"bugs": { "bugs": "https://github.com/Vylpes/vylbot-app/issues",
"url": "https://github.com/Vylpes/vylbot-app/issues",
"email": "helpdesk@vylpes.com"
},
"homepage": "https://github.com/Vylpes/vylbot-app", "homepage": "https://github.com/Vylpes/vylbot-app",
"funding": "https://ko-fi.com/vylpes",
"dependencies": { "dependencies": {
"@discordjs/rest": "^2.0.0", "@types/jest": "^27.0.3",
"@types/uuid": "^10.0.0", "@types/uuid": "^8.3.4",
"discord.js": "^14.3.0", "discord.js": "^13.6.0",
"dotenv": "^16.0.0", "dotenv": "^10.0.0",
"emoji-regex": "^10.0.0", "emoji-regex": "^9.2.0",
"minimatch": "10.0.1", "jest": "^27.4.5",
"jest-mock-extended": "^2.0.4",
"mysql": "^2.18.1", "mysql": "^2.18.1",
"random-bunny": "^2.1.6", "random-bunny": "^2.0.0",
"typeorm": "^0.3.20" "ts-jest": "^27.1.2",
}, "typeorm": "^0.2.44",
"resolutions": { "uuid": "^8.3.2"
}, },
"devDependencies": { "devDependencies": {
"@types/jest": "^29.5.12", "@types/node": "^16.11.10",
"@types/node": "^22.0.0", "typescript": "^4.5.2"
"jest": "^29.7.0",
"jest-mock-extended": "^3.0.7",
"np": "^10.0.0",
"ts-jest": "^29.2.4",
"typescript": "^5.0.0"
} }
} }

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,53 +0,0 @@
import {ActionRowBuilder, ButtonBuilder, ButtonInteraction, ButtonStyle, EmbedBuilder} from "discord.js";
import Moon from "../../database/entities/304276391837302787/Moon";
import EmbedColours from "../../constants/EmbedColours";
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);
const pageLength = 10;
const moons = await Moon.FetchPaginatedMoonsByUserId(userId, pageLength, pageNumber);
if (!moons || moons[0].length == 0) {
await interaction.reply(`${member?.user.username ?? "This user"} does not have any moons or page is invalid.`);
return;
}
const totalPages = Math.ceil(moons[1] / pageLength);
const description = moons[0].flatMap(x => `**${x.MoonNumber} -** ${x.Description.slice(0, 15)}`);
const embed = new EmbedBuilder()
.setTitle(`${member?.user.username}'s Moons`)
.setColor(EmbedColours.Ok)
.setDescription(description.join("\n"))
.setFooter({ text: `Page ${page + 1} of ${totalPages} · ${moons[1]} 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(pageNumber + 1 == totalPages));
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,22 +1,18 @@
import { Client, Partials } from "discord.js"; import { Client } from "discord.js";
import * as dotenv from "dotenv"; import * as dotenv from "dotenv";
import { createConnection } from "typeorm"; import { createConnection } from "typeorm";
import { EventType } from "../constants/EventType"; import DefaultValues from "../constants/DefaultValues";
import ICommandItem from "../contracts/ICommandItem"; import ICommandItem from "../contracts/ICommandItem";
import IEventItem from "../contracts/IEventItem"; import IEventItem from "../contracts/IEventItem";
import { Command } from "../type/command"; import { Command } from "../type/command";
import { Event } from "../type/event";
import { Events } from "./events"; import { Events } from "./events";
import { Util } from "./util"; 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";
export class CoreClient extends Client { export class CoreClient extends Client {
private static _commandItems: ICommandItem[]; private static _commandItems: ICommandItem[];
private static _eventItems: IEventItem[]; private static _eventItems: IEventItem[];
private static _buttonEvents: ButtonEventItem[];
private _events: Events; private _events: Events;
private _util: Util; private _util: Util;
@ -29,17 +25,14 @@ export class CoreClient extends Client {
return this._eventItems; return this._eventItems;
} }
public static get buttonEvents(): ButtonEventItem[] { constructor(intents: number[], devmode: boolean = false) {
return this._buttonEvents; super({ intents: intents });
}
constructor(intents: number[], partials: Partials[]) {
super({ intents: intents, partials: partials });
dotenv.config(); dotenv.config();
DefaultValues.useDevPrefix = devmode;
CoreClient._commandItems = []; CoreClient._commandItems = [];
CoreClient._eventItems = []; CoreClient._eventItems = [];
CoreClient._buttonEvents = [];
this._events = new Events(); this._events = new Events();
this._util = new Util(); this._util = new Util();
@ -51,21 +44,19 @@ export class CoreClient extends Client {
return; return;
} }
await AppDataSource.initialize() await createConnection().catch(e => {
.then(() => console.log("Data Source Initialized")) console.error(e);
.catch((err) => console.error("Error Initialising Data Source", err)); return;
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);
}); });
super.on("messageCreate", (message) => {
this._events.onMessageCreate(message, CoreClient._commandItems)
});
super.on("ready", this._events.onReady);
super.login(process.env.BOT_TOKEN);
this._util.loadEvents(this, CoreClient._eventItems); this._util.loadEvents(this, CoreClient._eventItems);
this._util.loadSlashCommands(this);
} }
public static RegisterCommand(name: string, command: Command, serverId?: string) { public static RegisterCommand(name: string, command: Command, serverId?: string) {
@ -78,21 +69,11 @@ export class CoreClient extends Client {
CoreClient._commandItems.push(item); CoreClient._commandItems.push(item);
} }
public static RegisterEvent(eventType: EventType, func: Function) { public static RegisterEvent(event: Event) {
const item: IEventItem = { const item: IEventItem = {
EventType: eventType, Event: event,
ExecutionFunction: func,
}; };
CoreClient._eventItems.push(item); CoreClient._eventItems.push(item);
} }
public static RegisterButtonEvent(buttonId: string, event: ButtonEvent) {
const item: ButtonEventItem = {
ButtonId: buttonId,
Event: event,
};
CoreClient._buttonEvents.push(item);
}
} }

View file

@ -1,17 +1,32 @@
import { Interaction } from "discord.js"; import { Message } from "discord.js";
import ChatInputCommand from "./interactionCreate/chatInputCommand"; import ICommandItem from "../contracts/ICommandItem";
import Button from "./interactionCreate/button"; import SettingsHelper from "../helpers/SettingsHelper";
import { Util } from "./util";
export class Events { export class Events {
public async onInteractionCreate(interaction: Interaction) { private _util: Util;
if (!interaction.guildId) return;
if (interaction.isChatInputCommand()) { constructor() {
ChatInputCommand.onChatInput(interaction); this._util = new Util();
} }
if (interaction.isButton()) { // Emit when a message is sent
Button.onButtonClicked(interaction); // Used to check for commands
public async onMessageCreate(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,101 @@
import { Client, REST, Routes, SlashCommandBuilder } from "discord.js"; // Required Components
import { EventType } from "../constants/EventType"; import { Client, Message } from "discord.js";
import { ICommandContext } from "../contracts/ICommandContext";
import ICommandItem from "../contracts/ICommandItem";
import IEventItem from "../contracts/IEventItem"; 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 { export class Util {
public loadSlashCommands(client: Client) { public async loadCommand(name: string, args: string[], message: Message, commands: ICommandItem[]) {
const registeredCommands = CoreClient.commandItems; if (!message.member) return;
if (!message.guild) return;
const globalCommands = registeredCommands.filter(x => !x.ServerId); const disabledCommandsString = await SettingsHelper.GetSetting("commands.disabled", message.guild?.id);
const guildCommands = registeredCommands.filter(x => x.ServerId); const disabledCommands = disabledCommandsString?.split(",");
const globalCommandData: SlashCommandBuilder[] = globalCommands if (disabledCommands?.find(x => x == name)) {
.filter(x => x.Command.CommandBuilder) message.reply(process.env.COMMANDS_DISABLED_MESSAGE || "This command is disabled.");
.flatMap(x => x.Command.CommandBuilder); return;
}
const guildIds: string[] = []; const item = commands.find(x => x.Name == name && !x.ServerId);
const itemForServer = commands.find(x => x.Name == name && x.ServerId == message.guild?.id);
for (let command of guildCommands) { let itemToUse: ICommandItem;
if (!guildIds.find(x => x == command.ServerId)) {
guildIds.push(command.ServerId!); if (!itemForServer) {
if (!item) {
message.reply('Command not found');
return;
}
itemToUse = item;
} else {
itemToUse = itemForServer;
}
const requiredRoles = itemToUse.Command.Roles;
if (message.author.id != process.env.BOT_OWNERID && message.author.id != message.guild.ownerId) {
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 rest = new REST({ version: '10' }).setToken(process.env.BOT_TOKEN!); const context: ICommandContext = {
name: name,
args: args,
message: message
};
rest.put( const precheckResponse = itemToUse.Command.precheck(context);
Routes.applicationCommands(process.env.BOT_CLIENTID!), const precheckAsyncResponse = await itemToUse.Command.precheckAsync(context);
{
body: globalCommandData if (precheckResponse != CommandResponse.Ok) {
message.reply(ErrorMessages.GetErrorMessage(precheckResponse));
return;
} }
);
for (let guild of guildIds) { if (precheckAsyncResponse != CommandResponse.Ok) {
const guildCommandData = guildCommands.filter(x => x.ServerId == guild) message.reply(ErrorMessages.GetErrorMessage(precheckAsyncResponse));
.filter(x => x.Command.CommandBuilder) return;
.flatMap(x => x.Command.CommandBuilder);
if (!client.guilds.cache.has(guild)) continue;
rest.put(
Routes.applicationGuildCommands(process.env.BOT_CLIENTID!, guild),
{
body: guildCommandData
}
)
} }
itemToUse.Command.execute(context);
} }
// Load the events // Load the events
loadEvents(client: Client, events: IEventItem[]) { loadEvents(client: Client, events: IEventItem[]) {
events.forEach((e) => { events.forEach((e) => {
switch(e.EventType) { client.on('channelCreate', e.Event.channelCreate);
case EventType.ChannelCreate: client.on('channelDelete', e.Event.channelDelete);
client.on('channelCreate', (channel) => e.ExecutionFunction(channel)); client.on('channelUpdate', e.Event.channelUpdate);
break; client.on('guildBanAdd', e.Event.guildBanAdd);
case EventType.ChannelDelete: client.on('guildBanRemove', e.Event.guildBanRemove);
client.on('channelDelete', (channel) => e.ExecutionFunction(channel)); client.on('guildCreate', e.Event.guildCreate);
break; client.on('guildMemberAdd', e.Event.guildMemberAdd);
case EventType.ChannelUpdate: client.on('guildMemberRemove', e.Event.guildMemberRemove);
client.on('channelUpdate', (channel) => e.ExecutionFunction(channel)); client.on('guildMemberUpdate', e.Event.guildMemberUpdate);
break; client.on('messageCreate', e.Event.messageCreate);
case EventType.GuildBanAdd: client.on('messageDelete', e.Event.messageDelete);
client.on('guildBanAdd', (ban) => e.ExecutionFunction(ban)); client.on('messageUpdate', e.Event.messageUpdate);
break; client.on('ready', e.Event.ready);
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.');
}
}); });
} }
} }

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,26 +0,0 @@
import {CommandInteraction, EmbedBuilder} from "discord.js";
import Moon from "../../../database/entities/304276391837302787/Moon";
import EmbedColours from "../../../constants/EmbedColours";
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;
}
const moonCount = await Moon.FetchMoonCountByUserId(interaction.user.id);
const moon = new Moon(moonCount + 1, 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,45 +0,0 @@
import {ActionRowBuilder, ButtonBuilder, ButtonStyle, CommandInteraction, EmbedBuilder} from "discord.js";
import Moon from "../../../database/entities/304276391837302787/Moon";
import EmbedColours from "../../../constants/EmbedColours";
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);
if (!moons || moons[0].length == 0) {
await interaction.reply(`${user.username} does not have any moons or page is invalid.`);
return;
}
const totalPages = Math.ceil(moons[1] / pageLength);
const description = moons[0].flatMap(x => `**${x.MoonNumber} -** ${x.Description.slice(0, 15)}`);
const embed = new EmbedBuilder()
.setTitle(`${user.username}'s Moons`)
.setColor(EmbedColours.Ok)
.setDescription(description.join("\n"))
.setFooter({ text: `Page ${page + 1} of ${totalPages} · ${moons[1]} 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(page + 1 == totalPages));
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 { ICommandContext } from "../../contracts/ICommandContext";
import EmbedColours from "../../constants/EmbedColours"; import PublicEmbed from "../../helpers/embeds/PublicEmbed";
import SettingsHelper from "../../helpers/SettingsHelper"; import SettingsHelper from "../../helpers/SettingsHelper";
import { Command } from "../../type/command"; import { Command } from "../../type/command";
@ -7,25 +7,19 @@ export default class Entry extends Command {
constructor() { constructor() {
super(); super();
this.CommandBuilder = new SlashCommandBuilder() super.Category = "Moderation";
.setName('entry') super.Roles = [
.setDescription('Sends the entry embed') "moderator"
.setDefaultMemberPermissions(PermissionsBitField.Flags.ModerateMembers); ];
} }
public override async execute(interaction: CommandInteraction) { public override async execute(context: ICommandContext) {
if (!interaction.guildId) return; if (!context.message.guild) return;
if (!interaction.channel) 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() 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.`);
.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 channel = interaction.channel as TextChannel; await embedInfo.SendToCurrentChannel();
await channel.send({ embeds: [ embed ]});
} }
} }

View file

@ -0,0 +1,159 @@
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");
await errorEmbed.SendToCurrentChannel();
return;
}
switch (context.args[1]) {
case "add":
await this.AddLobbyConfig(context);
break;
case "remove":
await this.RemoveLobbyConfig(context);
break;
case "help":
default:
await this.SendConfigHelp(context);
}
}
private async SendConfigHelp(context: ICommandContext) {
const helpText = readFileSync(`${process.cwd()}/data/usage/lobby.txt`).toString();
const embed = new PublicEmbed(context, "Configure Lobby Command", helpText);
await embed.SendToCurrentChannel();
}
private async AddLobbyConfig(context: ICommandContext) {
const channel = context.message.guild!.channels.cache.find(x => x.id == context.args[2]);
const role = context.message.guild!.roles.cache.find(x => x.id == context.args[3]);
const cooldown = Number(context.args[4]) || 30;
const gameName = context.args.splice(5).join(" ");
if (!channel) {
const errorEmbed = new ErrorEmbed(context, "The channel id you provided is invalid or channel does not exist.");
errorEmbed.SendToCurrentChannel();
return;
}
if (!role) {
const errorEmbed = new ErrorEmbed(context, "The role id you provided is invalid or role does not exist.");
errorEmbed.SendToCurrentChannel();
return;
}
const lobby = await eLobby.FetchOneByChannelId(channel.id);
if (lobby) {
const errorEmbed = new ErrorEmbed(context, "This channel has already been setup.");
errorEmbed.SendToCurrentChannel();
return;
}
const entity = new eLobby(channel.id, role.id, cooldown, gameName);
await entity.Save(eLobby, entity);
const embed = new PublicEmbed(context, "", `Added \`${channel.name}\` as a new lobby channel with a cooldown of \`${cooldown} minutes\` and will ping \`${role.name}\` on use`);
await embed.SendToCurrentChannel();
}
private async RemoveLobbyConfig(context: ICommandContext) {
const entity = await eLobby.FetchOneByChannelId(context.args[2]);
if (!entity) {
const errorEmbed = new ErrorEmbed(context, "The channel id you provided has not been setup as a lobby, unable to remove.");
await errorEmbed.SendToCurrentChannel();
return;
}
await BaseEntity.Remove<eLobby>(eLobby, entity);
const embed = new PublicEmbed(context, "", `Removed <#${context.args[2]}> from the list of lobby channels`);
await 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,19 @@
import { ActionRowBuilder, ButtonBuilder, ButtonStyle, CommandInteraction, EmbedBuilder, SlashCommandBuilder } from "discord.js"; import { ICommandContext } from "../contracts/ICommandContext";
import EmbedColours from "../constants/EmbedColours"; import PublicEmbed from "../helpers/embeds/PublicEmbed";
import { Command } from "../type/command"; import { Command } from "../type/command";
export default class About extends Command { export default class About extends Command {
constructor() { constructor() {
super(); super();
super.Category = "General";
this.CommandBuilder = new SlashCommandBuilder()
.setName('about')
.setDescription('About VylBot');
} }
public override async execute(interaction: CommandInteraction) { public override async execute(context: ICommandContext) {
const fundingLink = process.env.ABOUT_FUNDING; const embed = new PublicEmbed(context, "About", "")
const repoLink = process.env.ABOUT_REPO; .addField("Version", process.env.BOT_VER!)
.addField("Author", process.env.BOT_AUTHOR!)
.addField("Date", process.env.BOT_DATE!);
const embed = new EmbedBuilder() await embed.SendToCurrentChannel();
.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 ] : [] });
} }
} }

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

View file

@ -1,47 +1,29 @@
import ErrorEmbed from "../helpers/embeds/ErrorEmbed";
import PublicEmbed from "../helpers/embeds/PublicEmbed";
import { Command } from "../type/command"; import { Command } from "../type/command";
import { ICommandContext } from "../contracts/ICommandContext";
import randomBunny from "random-bunny"; import randomBunny from "random-bunny";
import { CommandInteraction, EmbedBuilder, SlashCommandBuilder } from "discord.js";
import EmbedColours from "../constants/EmbedColours";
export default class Bunny extends Command { export default class Bunny extends Command {
constructor() { constructor() {
super(); super();
this.CommandBuilder = new SlashCommandBuilder() super.Category = "Fun";
.setName("bunny")
.setDescription("Get a random picture of a rabbit.");
} }
public override async execute(interaction: CommandInteraction) { public override async execute(context: ICommandContext) {
if (!interaction.isChatInputCommand()) return; const result = await randomBunny('rabbits', 'hot');
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) { if (result.IsSuccess) {
const embed = new EmbedBuilder() const embed = new PublicEmbed(context, result.Result!.Title, "")
.setColor(EmbedColours.Ok)
.setTitle(result.Result!.Title)
.setDescription(result.Result!.Permalink)
.setImage(result.Result!.Url) .setImage(result.Result!.Url)
.setURL(`https://reddit.com${result.Result!.Permalink}`) .setURL(`https://reddit.com${result.Result!.Permalink}`)
.setFooter({ text: `r/${selectedSubreddit} · ${result.Result!.Ups} upvotes`}); .setFooter({ text: `r/Rabbits · ${result.Result!.Ups} upvotes` });
await interaction.editReply({ embeds: [ embed ]}); await embed.SendToCurrentChannel();
} else { } else {
await interaction.editReply("There was an error running this command."); const errorEmbed = new ErrorEmbed(context, "There was an error using this command.");
await errorEmbed.SendToCurrentChannel();
} }
} }
} }

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 { Command } from "../type/command";
import { ICommandContext } from "../contracts/ICommandContext";
import ICommandReturnContext from "../contracts/ICommandReturnContext";
export default class Clear extends Command { export default class Clear extends Command {
constructor() { constructor() {
super(); super();
this.CommandBuilder = new SlashCommandBuilder() super.Category = "Moderation";
.setName("clear") super.Roles = [
.setDescription("Clears the channel of messages") "moderator"
.setDefaultMemberPermissions(PermissionsBitField.Flags.ManageMessages) ];
.addNumberOption(option =>
option
.setName('count')
.setDescription('The amount to delete')
.setRequired(true)
.setMinValue(1)
.setMaxValue(100));
} }
public override async execute(interaction: CommandInteraction) { public override async execute(context: ICommandContext): Promise<ICommandReturnContext> {
if (!interaction.isChatInputCommand()) return; if (context.args.length == 0) {
if (!interaction.channel) return; const errorEmbed = new ErrorEmbed(context, "Please specify an amount between 1 and 100");
await 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) { if (!totalToClear || totalToClear <= 0 || totalToClear > 100) {
await interaction.reply('Please specify an amount between 1 and 100.'); const errorEmbed = new ErrorEmbed(context, "Please specify an amount between 1 and 100");
return; await errorEmbed.SendToCurrentChannel();
return {
commandContext: context,
embeds: [errorEmbed]
};
} }
const channel = interaction.channel as TextChannel; await (context.message.channel as TextChannel).bulkDelete(totalToClear);
if (!channel.manageable) { const embed = new PublicEmbed(context, "", `${totalToClear} message(s) were removed`);
await interaction.reply('Insufficient permissions. Please contact a moderator.'); await embed.SendToCurrentChannel();
return;
}
await channel.bulkDelete(totalToClear); return {
commandContext: context,
await interaction.reply(`${totalToClear} message(s) were removed.`); 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 SettingsHelper from "../helpers/SettingsHelper";
import StringTools from "../helpers/StringTools"; import StringTools from "../helpers/StringTools";
import { Command } from "../type/command"; import { Command } from "../type/command";
@ -7,60 +10,85 @@ export default class Code extends Command {
constructor() { constructor() {
super(); super();
this.CommandBuilder = new SlashCommandBuilder() super.Category = "Moderation";
.setName('code') super.Roles = [
.setDescription('Manage the verification code of the server') "moderator"
.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'));
} }
public override async execute(interaction: CommandInteraction) { public override async precheckAsync(context: ICommandContext): Promise<CommandResponse> {
if (!interaction.isChatInputCommand()) return; 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": case "randomise":
await this.Randomise(interaction); await this.Randomise(context);
break; break;
case "embed": case "embed":
await this.SendEmbed(interaction); await this.SendEmbed(context);
break; break;
default:
await this.SendUsage(context);
} }
} }
private async Randomise(interaction: CommandInteraction) { private async SendUsage(context: ICommandContext) {
if (!interaction.guildId) return; 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 randomCode = StringTools.RandomString(5); const embed = new PublicEmbed(context, "", description);
await embed.SendToCurrentChannel();
await SettingsHelper.SetSetting("verification.code", interaction.guildId, randomCode);
await interaction.reply(`Entry code has been set to \`${randomCode}\``);
} }
private async SendEmbed(interaction: CommandInteraction) { private async Randomise(context: ICommandContext) {
if (!interaction.guildId) return; if (!context.message.guild) {
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.");
return; return;
} }
const embed = new EmbedBuilder() const randomCode = StringTools.RandomString(5);
.setTitle("Entry Code")
.setDescription(code);
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}\``);
await 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!);
await embed.SendToCurrentChannel();
} }
} }

View file

@ -1,192 +1,126 @@
import { CommandInteraction, EmbedBuilder, PermissionsBitField, SlashCommandBuilder } from "discord.js";
import { readFileSync } from "fs"; import { readFileSync } from "fs";
import { CommandResponse } from "../constants/CommandResponse";
import DefaultValues from "../constants/DefaultValues"; import DefaultValues from "../constants/DefaultValues";
import EmbedColours from "../constants/EmbedColours"; import { ICommandContext } from "../contracts/ICommandContext";
import Server from "../database/entities/Server"; import Server from "../entity/Server";
import Setting from "../database/entities/Setting"; import Setting from "../entity/Setting";
import ErrorEmbed from "../helpers/embeds/ErrorEmbed";
import PublicEmbed from "../helpers/embeds/PublicEmbed";
import { Command } from "../type/command"; import { Command } from "../type/command";
export default class Config extends Command { export default class Config extends Command {
constructor() { constructor() {
super(); super();
super.Category = "Administration";
this.CommandBuilder = new SlashCommandBuilder() super.Roles = [
.setName('config') "administrator"
.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'))
} }
public override async execute(interaction: CommandInteraction) { public override async precheckAsync(context: ICommandContext): Promise<CommandResponse> {
if (!interaction.isChatInputCommand()) return; if (!context.message.guild) {
if (!interaction.guildId) return; return CommandResponse.ServerNotSetup;
}
const server = await Server.FetchOneById<Server>(Server, interaction.guildId, [ const server = await Server.FetchOneById<Server>(Server, context.message.guild?.id, [
"Settings", "Settings",
]); ]);
if (!server) { 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; return;
} }
switch (interaction.options.getSubcommand()) { const server = await Server.FetchOneById<Server>(Server, context.message.guild?.id, [
case 'list': "Settings",
await this.SendHelpText(interaction); ]);
break;
if (!server) {
return;
}
const key = context.args[0];
const action = context.args[1];
const value = context.args.splice(2).join(" ");
if (!key) {
this.SendHelpText(context);
} else if (!action) {
this.GetValue(context, server, key);
} else {
switch(action) {
case 'reset': case 'reset':
await this.ResetValue(interaction); this.ResetValue(context, server, key);
break;
case 'get':
await this.GetValue(interaction);
break; break;
case 'set': case 'set':
await this.SetValue(interaction); if (!value) {
const errorEmbed = new ErrorEmbed(context, "Value is required when setting");
errorEmbed.SendToCurrentChannel();
return;
}
this.SetValue(context, server, key, value);
break; break;
default: default:
await interaction.reply('Subcommand not found.'); const errorEmbed = new ErrorEmbed(context, "Action must be either set or reset");
errorEmbed.SendToCurrentChannel();
return;
}
} }
} }
private async SendHelpText(interaction: CommandInteraction) { private async SendHelpText(context: ICommandContext) {
const description = readFileSync(`${process.cwd()}/data/usage/config.txt`).toString(); const description = readFileSync(`${process.cwd()}/data/usage/config.txt`).toString();
const embed = new EmbedBuilder() const embed = new PublicEmbed(context, "Config", description);
.setColor(EmbedColours.Ok)
.setTitle("Config")
.setDescription(description);
await interaction.reply({ embeds: [ embed ]}); await embed.SendToCurrentChannel();
} }
private async GetValue(interaction: CommandInteraction) { private async GetValue(context: ICommandContext, server: Server, key: string) {
if (!interaction.guildId) return; const setting = server.Settings.filter(x => x.Key == key)[0];
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, [
"Settings",
]);
if (!server) {
await interaction.reply('Server not found.');
return;
}
const setting = server.Settings.filter(x => x.Key == key.value)[0];
if (setting) { if (setting) {
await interaction.reply(`\`${key.value}\`: \`${setting.Value}\``); const embed = new PublicEmbed(context, "", `${key}: ${setting.Value}`);
await embed.SendToCurrentChannel();
} else { } else {
var defaultValue = DefaultValues.GetValue(key.value.toString()); const embed = new PublicEmbed(context, "", `${key}: ${DefaultValues.GetValue(key)} <DEFAULT>`);
await embed.SendToCurrentChannel();
if (defaultValue) {
await interaction.reply(`\`${key.value}\`: \`${defaultValue}\` <DEFAULT>`);
} else {
await interaction.reply(`\`${key.value}\`: <NONE>`);
}
} }
} }
private async ResetValue(interaction: CommandInteraction) { private async ResetValue(context: ICommandContext, server: Server, key: string) {
if (!interaction.guildId) return; const setting = server.Settings.filter(x => x.Key == key)[0];
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, [
"Settings",
]);
if (!server) {
await interaction.reply('Server not found.');
return;
}
const setting = server.Settings.filter(x => x.Key == key.value)[0];
if (!setting) { if (!setting) {
await interaction.reply('Setting not found.'); const embed = new PublicEmbed(context, "", "Setting has been reset");
await embed.SendToCurrentChannel();
return; return;
} }
await Setting.Remove(Setting, setting); 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");
await embed.SendToCurrentChannel();
} }
private async SetValue(interaction: CommandInteraction) { private async SetValue(context: ICommandContext, server: Server, key: string, value: string) {
if (!interaction.guildId) return; const setting = server.Settings.filter(x => x.Key == key)[0];
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];
if (setting) { if (setting) {
setting.UpdateBasicDetails(key.value.toString(), value.value.toString()); setting.UpdateBasicDetails(key, value);
await setting.Save(Setting, setting); await setting.Save(Setting, setting);
} else { } else {
const newSetting = new Setting(key.value.toString(), value.value.toString()); const newSetting = new Setting(key, value);
await newSetting.Save(Setting, newSetting); await newSetting.Save(Setting, newSetting);
@ -195,6 +129,7 @@ export default class Config extends Command {
await server.Save(Server, server); await server.Save(Server, server);
} }
await interaction.reply('Setting has been set.'); const embed = new PublicEmbed(context, "", "Setting has been set");
await embed.SendToCurrentChannel();
} }
} }

View file

@ -1,4 +1,5 @@
import { CommandInteraction, PermissionsBitField, SlashCommandBuilder } from "discord.js"; import { ICommandContext } from "../contracts/ICommandContext";
import PublicEmbed from "../helpers/embeds/PublicEmbed";
import SettingsHelper from "../helpers/SettingsHelper"; import SettingsHelper from "../helpers/SettingsHelper";
import { Command } from "../type/command"; import { Command } from "../type/command";
@ -6,86 +7,87 @@ export default class Disable extends Command {
constructor() { constructor() {
super(); super();
this.CommandBuilder = new SlashCommandBuilder() super.Category = "Moderation";
.setName('disable') super.Roles = [
.setDescription('Disables a command') "moderator"
.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)));
} }
public override async execute(interaction: CommandInteraction) { public override async execute(context: ICommandContext) {
if (!interaction.isChatInputCommand()) return; const action = context.args[0];
switch (interaction.options.getSubcommand()) { switch (action) {
case "add": case "add":
await this.Add(interaction); await this.Add(context);
break; break;
case "remove": case "remove":
await this.Remove(interaction); await this.Remove(context);
break; break;
default: default:
await interaction.reply('Subcommand not found.'); await this.SendUsage(context);
} }
} }
private async Add(interaction: CommandInteraction) { private async SendUsage(context: ICommandContext) {
if (!interaction.guildId) return; 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 commandName = interaction.options.get('name'); const embed = new PublicEmbed(context, "", description);
await embed.SendToCurrentChannel();
}
if (!commandName || !commandName.value) { private async Add(context: ICommandContext) {
await interaction.reply('Fields are required.'); if (!context.message.guild) {
return; return;
} }
const disabledCommandsString = await SettingsHelper.GetSetting("commands.disabled", interaction.guildId); const commandName = context.args[1];
const disabledCommands = disabledCommandsString != "" ? disabledCommandsString?.split(",") : [];
disabledCommands?.push(commandName.value.toString()); if (!commandName) {
this.SendUsage(context);
await SettingsHelper.SetSetting("commands.disabled", interaction.guildId, disabledCommands!.join(","));
await interaction.reply(`Disabled command ${commandName.value}`);
}
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.');
return; return;
} }
const disabledCommandsString = await SettingsHelper.GetSetting("commands.disabled", interaction.guildId); const disabledCommandsString = await SettingsHelper.GetSetting("commands.disabled", context.message.guild.id);
const disabledCommands = disabledCommandsString != "" ? disabledCommandsString?.split(",") : []; 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}`);
await 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) { if (disabledCommandsInstance! > -1) {
disabledCommands?.splice(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}`);
await embed.SendToCurrentChannel();
} }
} }

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

@ -0,0 +1,68 @@
import { existsSync, readdirSync } from "fs";
import { CoreClient } from "../client/client";
import { ICommandContext } from "../contracts/ICommandContext";
import ErrorEmbed from "../helpers/embeds/ErrorEmbed";
import PublicEmbed from "../helpers/embeds/PublicEmbed";
import StringTools from "../helpers/StringTools";
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 async execute(context: ICommandContext) {
if (context.args.length == 0) {
await this.SendAll(context);
} else {
await this.SendSingle(context);
}
}
public async SendAll(context: ICommandContext) {
const allCommands = CoreClient.commandItems
.filter(x => !x.ServerId || x.ServerId == context.message.guild?.id);
const cateogries = [...new Set(allCommands.map(x => x.Command.Category))];
const embed = new PublicEmbed(context, "Commands", "");
cateogries.forEach(category => {
let filtered = allCommands.filter(x => x.Command.Category == category);
embed.addField(StringTools.Capitalise(category || "Uncategorised"), StringTools.CapitaliseArray(filtered.flatMap(x => x.Name)).join(", "));
});
await embed.SendToCurrentChannel();
}
public async SendSingle(context: ICommandContext) {
const command = CoreClient.commandItems.find(x => x.Name == context.args[0] && !x.ServerId);
const exclusiveCommand = CoreClient.commandItems.find(x => x.Name == context.args[0] && x.ServerId == context.message.guild?.id);
if (exclusiveCommand) {
const embed = new PublicEmbed(context, StringTools.Capitalise(exclusiveCommand.Name), "");
embed.addField("Category", StringTools.Capitalise(exclusiveCommand.Command.Category || "Uncategorised"));
embed.addField("Required Roles", StringTools.Capitalise(exclusiveCommand.Command.Roles.join(", ")) || "Everyone");
await embed.SendToCurrentChannel();
} else if (command) {
const embed = new PublicEmbed(context, StringTools.Capitalise(command.Name), "");
embed.addField("Category", StringTools.Capitalise(command.Command.Category || "Uncategorised"));
embed.addField("Required Roles", StringTools.Capitalise(command.Command.Roles.join(", ")) || "Everyone");
await embed.SendToCurrentChannel();
} else {
const errorEmbed = new ErrorEmbed(context, "Command does not exist");
await errorEmbed.SendToCurrentChannel();
}
}
}

View file

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

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 { 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 { export default class Kick extends Command {
constructor() { constructor() {
super(); super();
this.CommandBuilder = new SlashCommandBuilder() super.Category = "Moderation";
.setName("kick") super.Roles = [
.setDescription("Kick a member from the server with an optional reason") "moderator"
.setDefaultMemberPermissions(PermissionsBitField.Flags.KickMembers) ];
.addUserOption(option =>
option
.setName('target')
.setDescription('The user')
.setRequired(true))
.addStringOption(option =>
option
.setName('reason')
.setDescription('The reason'));
} }
public override async execute(interaction: CommandInteraction) { public override async execute(context: ICommandContext): Promise<ICommandReturnContext> {
if (!interaction.isChatInputCommand()) return; const targetUser = context.message.mentions.users.first();
if (!interaction.guildId) return;
if (!interaction.guild) return;
const targetUser = interaction.options.get('target'); if (!targetUser) {
const reasonInput = interaction.options.get('reason'); const embed = new ErrorEmbed(context, "User does not exist");
await embed.SendToCurrentChannel();
if (!targetUser || !targetUser.user || !targetUser.member) { return {
await interaction.reply("User not found."); commandContext: context,
return; embeds: [embed]
};
} }
const member = targetUser.member as GuildMember; const targetMember = context.message.guild?.members.cache.find(x => x.user.id == targetUser.id);
const reason = reasonInput && reasonInput.value ? reasonInput.value.toString() : "*none*";
const logEmbed = new EmbedBuilder() if (!targetMember) {
.setColor(EmbedColours.Ok) const embed = new ErrorEmbed(context, "User is not in this server");
.setTitle("Member Kicked") await embed.SendToCurrentChannel();
.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) { return {
await interaction.reply('Insufficient permissions. Please contact a moderator.'); commandContext: context,
return; embeds: [embed]
};
} }
await member.kick(); const reasonArgs = context.args;
reasonArgs.splice(0, 1)
const channelName = await SettingsHelper.GetSetting('channels.logs.mod', interaction.guildId); const reason = reasonArgs.join(" ");
if (!channelName) return; if (!context.message.guild?.available) {
return {
const channel = interaction.guild.channels.cache.find(x => x.name == channelName) as TextChannel; commandContext: context,
embeds: []
if (channel) { };
await channel.send({ embeds: [ logEmbed ]});
} }
const dmEmbed = new EmbedBuilder() if (!targetMember.kickable) {
.setColor(EmbedColours.Ok) const embed = new ErrorEmbed(context, ErrorMessages.InsufficientBotPermissions);
.setTitle(interaction.guild.name) await embed.SendToCurrentChannel();
.setDescription("You have been kicked from the server.")
.addFields([
{
name: "Reason",
value: reason,
},
]);
let replyText = "Successfully kicked user."; return {
commandContext: context,
try { embeds: [embed]
const dmChannel = await targetUser.user!.createDM(); };
await dmChannel.send({ embeds: [ dmEmbed ] });
} catch {
replyText += " *Note: I was unable to DM the user the reason.*";
} }
const audit = new Audit(targetUser.user.id, AuditType.Kick, reason, interaction.user.id, interaction.guildId); const logEmbed = new LogEmbed(context, "Member Kicked");
await audit.Save(Audit, audit); 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();
await 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");
await embed.SendToCurrentChannel();
return {
commandContext: context,
embeds: [embed]
};
}
const targetMember = context.message.guild?.members.cache.find(x => x.user.id == targetUser.id);
if (!targetMember) {
const embed = new ErrorEmbed(context, "User is not in this server");
await 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);
await 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);
await embed.SendToCurrentChannel();
return {
commandContext: context,
embeds: [embed]
};
}
await targetMember.roles.add(mutedRole, `Moderator: ${context.message.author.tag}, Reason: ${reason || "*none*"}`);
await logEmbed.SendToModLogsChannel();
await 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 { Command } from "../type/command";
import { EmbedBuilder } from "@discordjs/builders";
import EmbedColours from "../constants/EmbedColours";
export default class Poll extends Command { export default class Poll extends Command {
constructor() { constructor() {
super(); super();
this.CommandBuilder = new SlashCommandBuilder() super.Category = "General";
.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'));
} }
public override async execute(interaction: CommandInteraction) { public override async execute(context: ICommandContext): Promise<ICommandReturnContext> {
const title = interaction.options.get('title'); const argsJoined = context.args.join(" ");
const option1 = interaction.options.get('option1'); const argsSplit = argsJoined.split(";");
const option2 = interaction.options.get('option2');
const option3 = interaction.options.get('option3');
const option4 = interaction.options.get('option4');
const option5 = interaction.options.get('option5');
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");
await errorEmbed.SendToCurrentChannel();
const description = [ return {
option1.value as string, commandContext: context,
option2.value as string, embeds: [errorEmbed]
option3?.value as string, };
option4?.value as string, }
option5?.value as string
] const title = argsSplit[0];
.filter(x => x != null);
const arrayOfNumbers = [ const arrayOfNumbers = [
':one:', ':one:',
@ -64,28 +33,35 @@ export default class Poll extends Command {
':three:', ':three:',
':four:', ':four:',
':five:', ':five:',
':six:',
':seven:',
':eight:',
':nine:'
]; ];
const reactionEmojis = ["1⃣", "2⃣", "3⃣", "4⃣", "5⃣", "6⃣", "7⃣", "8⃣", "9⃣"]; const reactionEmojis = ["1⃣", "2⃣", "3⃣", "4⃣", "5⃣", "6⃣", "7⃣", "8⃣", "9⃣"];
const description = arrayOfNumbers.splice(0, argsSplit.length - 1);
description.forEach((value, index) => { description.forEach((value, index) => {
description[index] = `${reactionEmojis[index]} ${description[index]}`; description[index] = `${value} ${argsSplit[index + 1]}`;
}); });
const embed = new EmbedBuilder() const embed = new PublicEmbed(context, title, description.join("\n"));
.setColor(EmbedColours.Ok)
.setTitle(title.value as string)
.setDescription(description.join('\n'))
.setFooter({
text: `Poll by ${interaction.user.username}`,
iconURL: interaction.user.avatarURL()!,
});
const message = await context.message.channel.send({ embeds: [ embed ]});
const message = await interaction.reply({ embeds: [ embed ]});
description.forEach(async (value, index) => { 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();
}
return {
commandContext: context,
embeds: [embed]
};
} }
} }

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

@ -0,0 +1,232 @@
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";
import SettingsHelper from "../helpers/SettingsHelper";
import { readFileSync } from "fs";
import { default as eRole } from "../entity/Role";
import Server from "../entity/Server";
export default class Role 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) {
if (context.args.length == 0) {
await this.SendRolesList(context, context.message.guild!.id);
} else {
await this.ToggleRole(context);
}
}
public async GetRolesList(context: ICommandContext): Promise<string[]> {
const rolesArray = await eRole.FetchAllByServerId(context.message.guild!.id);
const stringArray: string[] = [];
for (let i = 0; i < rolesArray.length; i++) {
const serverRole = context.message.guild!.roles.cache.find(x => x.id == rolesArray[i].RoleId);
if (serverRole) {
stringArray.push(serverRole.name);
}
}
return stringArray;
}
public async SendRolesList(context: ICommandContext, serverId: string): Promise<ICommandReturnContext> {
const roles = await this.GetRolesList(context);
const botPrefix = await SettingsHelper.GetServerPrefix(serverId);
const description = roles.length == 0 ? "*no roles*" : `Do ${botPrefix}role <role> to get the role!\n\n${roles.join('\n')}`;
const embed = new PublicEmbed(context, "Roles", description);
await embed.SendToCurrentChannel();
return {
commandContext: context,
embeds: [embed]
};
}
public async ToggleRole(context: ICommandContext): Promise<ICommandReturnContext> {
const roles = await this.GetRolesList(context);
const requestedRole = context.args.join(" ");
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");
await 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");
await 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}\``);
await 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}\``);
await embed.SendToCurrentChannel();
return {
commandContext: context,
embeds: [embed]
};
}
// ======
// 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");
await errorEmbed.SendToCurrentChannel();
return;
}
switch (context.args[1]) {
case "add":
await this.AddRoleConfig(context);
break;
case "remove":
await this.RemoveRoleConfig(context);
break;
default:
await this.SendConfigHelp(context);
}
}
private async SendConfigHelp(context: ICommandContext) {
const helpText = readFileSync(`${process.cwd()}/data/usage/role.txt`).toString();
const embed = new PublicEmbed(context, "Configure Role Command", helpText);
await embed.SendToCurrentChannel();
}
private async AddRoleConfig(context: ICommandContext) {
const role = context.message.guild!.roles.cache.find(x => x.id == context.args[2]);
if (!role) {
this.SendConfigHelp(context);
return;
}
const existingRole = await eRole.FetchOneByRoleId(role.id);
if (existingRole) {
const errorEmbed = new ErrorEmbed(context, "This role has already been setup");
await errorEmbed.SendToCurrentChannel();
return;
}
const server = await Server.FetchOneById(Server, context.message.guild!.id, [
"Roles",
]);
if (!server) {
const errorEmbed = new ErrorEmbed(context, "Server not setup, please request the server owner runs the setup command.");
await errorEmbed.SendToCurrentChannel();
return;
}
const roleSetting = new eRole(role.id);
await roleSetting.Save(eRole, roleSetting);
server.AddRoleToServer(roleSetting);
await server.Save(Server, server);
const embed = new PublicEmbed(context, "", `Added \`${role.name}\` as a new assignable role`);
await embed.SendToCurrentChannel();
}
private async RemoveRoleConfig(context: ICommandContext) {
const role = context.message.guild!.roles.cache.find(x => x.id == context.args[2]);
if (!role) {
this.SendConfigHelp(context);
return;
}
const existingRole = await eRole.FetchOneByRoleId(role.id);
if (!existingRole) {
const errorEmbed = new ErrorEmbed(context, "Unable to find this role");
errorEmbed.SendToCurrentChannel();
return;
}
await eRole.Remove(eRole, existingRole);
const embed = new PublicEmbed(context, "", `Removed \`${role.name}\` from the list of assignable roles`);
await embed.SendToCurrentChannel();
}
}

View file

@ -1,8 +1,8 @@
import { ActionRowBuilder, ButtonBuilder, ButtonStyle, CommandInteraction, EmbedBuilder, PermissionsBitField, SlashCommandBuilder, TextChannel } from "discord.js";
import { existsSync, readFileSync } from "fs"; import { existsSync, readFileSync } from "fs";
import EmbedColours from "../constants/EmbedColours"; import { ICommandContext } from "../contracts/ICommandContext";
import ErrorEmbed from "../helpers/embeds/ErrorEmbed";
import PublicEmbed from "../helpers/embeds/PublicEmbed";
import { Command } from "../type/command"; import { Command } from "../type/command";
import SettingsHelper from "../helpers/SettingsHelper";
interface IRules { interface IRules {
title?: string; title?: string;
@ -15,109 +15,38 @@ export default class Rules extends Command {
constructor() { constructor() {
super(); super();
this.CommandBuilder = new SlashCommandBuilder() super.Category = "Admin";
.setName('rules') super.Roles = [
.setDescription("Rules-related commands") "administrator"
.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'));
} }
public override async execute(interaction: CommandInteraction) { public override async execute(context: ICommandContext) {
if (!interaction.isChatInputCommand()) return; if (!existsSync(`${process.cwd()}/data/rules/${context.message.guild?.id}.json`)) {
const errorEmbed = new ErrorEmbed(context, "Rules file doesn't exist");
await 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;
} }
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 rules = JSON.parse(rulesFile) as IRules[];
const embeds: EmbedBuilder[] = []; const embeds: PublicEmbed[] = [];
if (rules.length == 0) {
await interaction.reply({ content: "No rules have been supplied within code base for this server.", ephemeral: true });
return;
}
rules.forEach(rule => { rules.forEach(rule => {
const embed = new EmbedBuilder() const embed = new PublicEmbed(context, rule.title || "", rule.description?.join("\n") || "");
.setColor(EmbedColours.Ok)
.setTitle(rule.title || "Rules")
.setDescription(rule.description ? rule.description.join("\n") : "*none*");
if (rule.image) { embed.setImage(rule.image || "");
embed.setImage(rule.image); embed.setFooter({ text: rule.footer || "" });
}
if (rule.footer) {
embed.setFooter({ text: rule.footer });
}
embeds.push(embed); embeds.push(embed);
}); });
const channel = interaction.channel as TextChannel; for (let i = 0; i < embeds.length; i++) {
const embed = embeds[i];
if (!channel) { await embed.SendToCurrentChannel();
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,
});
} }
} }

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 { ICommandContext } from "../contracts/ICommandContext";
import Server from "../database/entities/Server"; import Server from "../entity/Server";
import ErrorEmbed from "../helpers/embeds/ErrorEmbed";
import PublicEmbed from "../helpers/embeds/PublicEmbed";
import { Command } from "../type/command"; import { Command } from "../type/command";
export default class Setup extends Command { export default class Setup extends Command {
constructor() { constructor() {
super(); super();
super.Category = "Administration";
this.CommandBuilder = new SlashCommandBuilder() super.Roles = [
.setName('setup') "moderator"
.setDescription('Makes the server ready to be configured') ]
.setDefaultMemberPermissions(PermissionsBitField.Flags.Administrator);
} }
public override async execute(interaction: CommandInteraction) { public override async execute(context: ICommandContext) {
if (!interaction.guildId) return; if (!context.message.guild) {
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.');
return; 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");
await embed.SendToCurrentChannel();
return;
}
const newServer = new Server(context.message.guild?.id);
await newServer.Save(Server, newServer); 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");
await 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");
await embed.SendToCurrentChannel();
return {
commandContext: context,
embeds: [embed]
};
}
const targetMember = context.message.guild?.members.cache.find(x => x.user.id == targetUser.id);
if (!targetMember) {
const embed = new ErrorEmbed(context, "User is not in this server");
await 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);
await 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);
await embed.SendToCurrentChannel();
return {
commandContext: context,
embeds: [embed]
};
}
await targetMember.roles.remove(mutedRole, `Moderator: ${context.message.author.tag}, Reason: ${reason || "*none*"}`);
await logEmbed.SendToModLogsChannel();
await publicEmbed.SendToCurrentChannel();
return {
commandContext: context,
embeds: [logEmbed, publicEmbed]
};
}
}

View file

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

@ -17,7 +17,11 @@ export default class DefaultValues {
private static SetValues() { private static SetValues() {
if (this.values.length == 0) { if (this.values.length == 0) {
// Bot // 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 // Commands
this.values.push({ Key: "commands.disabled", Value: "" }); this.values.push({ Key: "commands.disabled", Value: "" });
@ -31,7 +35,6 @@ export default class DefaultValues {
// Rules (Command) // Rules (Command)
this.values.push({ Key: "rules.file", Value: "data/rules/rules" }); this.values.push({ Key: "rules.file", Value: "data/rules/rules" });
this.values.push({ Key: "rules.access.label", Value: "Verify" });
// Channels // Channels
this.values.push({ Key: "channels.logs.message", Value: "message-logs" }); this.values.push({ Key: "channels.logs.message", Value: "message-logs" });
@ -44,9 +47,6 @@ export default class DefaultValues {
this.values.push({ Key: "verification.role", Value: "Entry" }); this.values.push({ Key: "verification.role", Value: "Entry" });
this.values.push({ Key: "verification.code", Value: "" }); this.values.push({ Key: "verification.code", Value: "" });
// Gif Only Mode
this.values.push({ Key: "channel.linkonly", Value: "" })
// Event // Event
this.values.push({ Key: "event.message.delete.enabled", Value: "false" }); 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.delete.channel", Value: "message-logs" });
@ -61,7 +61,7 @@ export default class DefaultValues {
this.values.push({ Key: "event.member.remove.channel", Value: "member-logs" }); 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.enabled", Value: "false" });
this.values.push({ Key: "event.member.update.channel", Value: "member-logs" }); this.values.push({ Key: "event.member.remove.channel", Value: "member-logs" });
} }
} }

View file

@ -1,4 +0,0 @@
export default class EmbedColours {
public static readonly Ok = 0x3050ba;
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,
}

View file

@ -1,6 +1,5 @@
import { Column, DeepPartial, EntityTarget, PrimaryColumn, ObjectLiteral, FindOptionsWhere } from "typeorm"; import { Column, DeepPartial, EntityTarget, getConnection, PrimaryColumn } from "typeorm";
import { v4 } from "uuid"; import { v4 } from "uuid";
import AppDataSource from "../database/dataSources/appDataSource";
export default class BaseEntity { export default class BaseEntity {
constructor() { constructor() {
@ -19,38 +18,48 @@ export default class BaseEntity {
@Column() @Column()
WhenUpdated: Date; WhenUpdated: Date;
public async Save<T extends BaseEntity>(target: EntityTarget<T>, entity: DeepPartial<T>): Promise<void> { public async Save<T>(target: EntityTarget<T>, entity: DeepPartial<T>): Promise<void> {
this.WhenUpdated = new Date(); this.WhenUpdated = new Date();
const repository = AppDataSource.getRepository<T>(target); const connection = getConnection();
const repository = connection.getRepository<T>(target);
await repository.save(entity); await repository.save(entity);
} }
public static async Remove<T extends BaseEntity>(target: EntityTarget<T>, entity: T): Promise<void> { public static async Remove<T>(target: EntityTarget<T>, entity: T): Promise<void> {
const repository = AppDataSource.getRepository<T>(target); const connection = getConnection();
const repository = connection.getRepository<T>(target);
await repository.remove(entity); await repository.remove(entity);
} }
public static async FetchAll<T extends BaseEntity>(target: EntityTarget<T>, relations?: string[]): Promise<T[]> { public static async FetchAll<T>(target: EntityTarget<T>, relations?: string[]): Promise<T[]> {
const repository = AppDataSource.getRepository<T>(target); const connection = getConnection();
const repository = connection.getRepository<T>(target);
const all = await repository.find({ relations: relations || [] }); const all = await repository.find({ relations: relations || [] });
return all; return all;
} }
public static async FetchOneById<T extends BaseEntity>(target: EntityTarget<T>, id: string, relations?: string[]): Promise<T | null> { public static async FetchOneById<T>(target: EntityTarget<T>, id: string, relations?: string[]): Promise<T | undefined> {
const repository = AppDataSource.getRepository<T>(target); const connection = getConnection();
const single = await repository.findOne({ where: ({ Id: id } as FindOptionsWhere<T>), relations: relations || {} }); const repository = connection.getRepository<T>(target);
const single = await repository.findOne(id, { relations: relations || [] });
return single; return single;
} }
public static async Any<T extends ObjectLiteral>(target: EntityTarget<T>): Promise<boolean> { public static async Any<T>(target: EntityTarget<T>): Promise<boolean> {
const repository = AppDataSource.getRepository<T>(target); const connection = getConnection();
const repository = connection.getRepository<T>(target);
const any = await repository.find(); const any = await repository.find();

View file

@ -1,8 +0,0 @@
import { ButtonEvent } from "../type/buttonEvent";
interface ButtonEventItem {
ButtonId: string,
Event: ButtonEvent,
}
export default ButtonEventItem;

View file

@ -0,0 +1,7 @@
import { Message } from "discord.js";
export interface ICommandContext {
name: string;
args: string[];
message: Message;
}

View file

@ -0,0 +1,7 @@
import { MessageEmbed } from "discord.js";
import { ICommandContext } from "./ICommandContext";
export default interface ICommandReturnContext {
commandContext: ICommandContext,
embeds: MessageEmbed[]
}

View file

@ -1,7 +1,6 @@
import { EventType } from "../constants/EventType"; import { Event } from "../type/event";
export default interface IEventItem { export default interface IEventItem {
EventType: EventType, Event: Event,
ExecutionFunction: Function,
} }

View file

@ -0,0 +1,6 @@
import { MessageEmbed } from "discord.js";
import { ICommandContext } from "./ICommandContext";
export default interface ICommandReturnContext {
embeds: MessageEmbed[]
}

View file

@ -1,26 +0,0 @@
import { DataSource } from "typeorm";
import * as dotenv from "dotenv";
dotenv.config();
const AppDataSource = new DataSource({
type: "mysql",
host: process.env.DB_HOST,
port: Number(process.env.DB_PORT),
username: process.env.DB_AUTH_USER,
password: process.env.DB_AUTH_PASS,
database: process.env.DB_NAME,
synchronize: process.env.DB_SYNC == "true",
logging: process.env.DB_LOGGING == "true",
entities: [
"dist/database/entities/**/*.js",
],
migrations: [
"dist/database/migrations/**/*.js",
],
subscribers: [
"dist/database/subscribers/**/*.js",
],
});
export default AppDataSource;

View file

@ -1,57 +0,0 @@
import { Column, Entity, IsNull } from "typeorm";
import BaseEntity from "../../../contracts/BaseEntity";
import AppDataSource from "../../dataSources/appDataSource";
@Entity()
export default class Moon extends BaseEntity {
constructor(moonNumber: number, description: string, userId: string) {
super();
this.MoonNumber = moonNumber;
this.Description = description;
this.UserId = userId;
}
@Column()
MoonNumber: number;
@Column()
Description: string;
@Column({ nullable: true })
WhenArchived?: Date;
@Column()
UserId: string;
public static async FetchMoonsByUserId(userId: string): Promise<Moon[] | null> {
const repository = AppDataSource.getRepository(Moon);
const all = await repository.find({ where: { UserId: userId } });
return all;
}
public static async FetchPaginatedMoonsByUserId(userId: string, pageLength: number, page: number): Promise<[ Moon[], number ]> {
const rangeStart = page * pageLength;
const repository = AppDataSource.getRepository(Moon);
const moons = await repository.findAndCount({
where: { UserId: userId, WhenArchived: IsNull() },
order: { MoonNumber: "ASC" },
skip: rangeStart,
take: pageLength,
});
return moons;
}
public static async FetchMoonCountByUserId(userId: string): Promise<number> {
const repository = AppDataSource.getRepository(Moon);
const count = await repository.count({ where: { UserId: userId } });
return count;
}
}

View file

@ -1,53 +0,0 @@
import { Column, Entity } from "typeorm";
import { AuditType } from "../../constants/AuditType";
import BaseEntity from "../../contracts/BaseEntity";
import StringTools from "../../helpers/StringTools";
import AppDataSource from "../dataSources/appDataSource";
@Entity()
export default class Audit extends BaseEntity {
constructor(userId: string, auditType: AuditType, reason: string, moderatorId: string, serverId: string) {
super();
this.AuditId = StringTools.RandomString(5).toUpperCase();
this.UserId = userId;
this.AuditType = auditType;
this.Reason = reason;
this.ModeratorId = moderatorId;
this.ServerId = serverId;
}
@Column()
AuditId: string;
@Column()
UserId: string;
@Column()
AuditType: AuditType;
@Column()
Reason: string;
@Column()
ModeratorId: string;
@Column()
ServerId: string;
public static async FetchAuditsByUserId(userId: string, serverId: string): Promise<Audit[] | null> {
const repository = AppDataSource.getRepository(Audit);
const all = await repository.find({ where: { UserId: userId, ServerId: serverId } });
return all;
}
public static async FetchAuditByAuditId(auditId: string, serverId: string): Promise<Audit | null> {
const repository = AppDataSource.getRepository(Audit);
const single = await repository.findOne({ where: { AuditId: auditId, ServerId: serverId } });
return single;
}
}

View file

@ -1,20 +0,0 @@
import { Entity } from "typeorm";
import BaseEntity from "../../contracts/BaseEntity";
import AppDataSource from "../dataSources/appDataSource";
@Entity()
export default class IgnoredChannel extends BaseEntity {
constructor(channelId: string) {
super();
this.Id = channelId;
}
public static async IsChannelIgnored(channelId: string): Promise<boolean> {
const repository = AppDataSource.getRepository(IgnoredChannel);
const single = await repository.findOne({ where: { Id: channelId } });
return single != undefined;
}
}

View file

@ -1,45 +0,0 @@
import { Column, Entity, ManyToOne } from "typeorm";
import BaseEntity from "../../contracts/BaseEntity"
import Server from "./Server";
import AppDataSource from "../dataSources/appDataSource";
@Entity()
export default class Role extends BaseEntity {
constructor(roleId: string) {
super();
this.RoleId = roleId;
}
@Column()
RoleId: string;
@ManyToOne(() => Server, x => x.Roles)
Server: Server;
public SetServer(server: Server) {
this.Server = server;
}
public static async FetchOneByRoleId(roleId: string, relations?: string[]): Promise<Role | null> {
const repository = AppDataSource.getRepository(Role);
const single = await repository.findOne({ where: { RoleId: roleId }, relations: relations || []});
return single;
}
public static async FetchAllByServerId(serverId: string): Promise<Role[]> {
const repository = AppDataSource.getRepository(Server);
const all = await repository.findOne({ where: { Id: serverId }, relations: [
"Roles",
] });
if (!all) {
return [];
}
return all.Roles;
}
}

View file

@ -1,30 +0,0 @@
import { MigrationInterface, QueryRunner } from "typeorm"
import MigrationHelper from "../../../helpers/MigrationHelper"
export class vylbot1662399171315 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
MigrationHelper.Up('1662399171315-CreateBase', '3.1', [
"01-table/Audit",
"01-table/IgnoredChannel",
"01-table/Lobby",
"01-table/Role",
"01-table/Server",
"01-table/Setting",
"02-key/Audit",
"02-key/IgnoredChannel",
"02-key/Lobby",
"02-key/Role",
"02-key/Server",
"02-key/Setting",
"03-constraint/Role",
"03-constraint/Setting",
], queryRunner);
}
public async down(queryRunner: QueryRunner): Promise<void> {
}
}

View file

@ -1,15 +0,0 @@
import { MigrationInterface, QueryRunner } from "typeorm"
import MigrationHelper from "../../../helpers/MigrationHelper"
export class AddServerCacheDate1709316734401 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
MigrationHelper.Up('1709316734401-AddServerCacheDate', '3.2.1', [
"01-Server",
], queryRunner);
}
public async down(queryRunner: QueryRunner): Promise<void> {
}
}

View file

@ -1,15 +0,0 @@
import { MigrationInterface, QueryRunner } from "typeorm"
import MigrationHelper from "../../../helpers/MigrationHelper"
export class CreateMoon1719856023429 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
MigrationHelper.Up('1719856023429-CreateMoon', '3.3.0', [
"01-Moon",
], queryRunner);
}
public async down(queryRunner: QueryRunner): Promise<void> {
}
}

View file

@ -1,6 +1,5 @@
import { Column, Entity } from "typeorm"; import { Column, Entity, getConnection } from "typeorm";
import BaseEntity from "../../../contracts/BaseEntity"; import BaseEntity from "../../contracts/BaseEntity";
import AppDataSource from "../../dataSources/appDataSource";
@Entity() @Entity()
export default class Lobby extends BaseEntity { export default class Lobby extends BaseEntity {
@ -34,10 +33,12 @@ export default class Lobby extends BaseEntity {
this.LastUsed = new Date(); this.LastUsed = new Date();
} }
public static async FetchOneByChannelId(channelId: string, relations?: string[]): Promise<Lobby | null> { public static async FetchOneByChannelId(channelId: string, relations?: string[]): Promise<Lobby | undefined> {
const repository = AppDataSource.getRepository(Lobby); const connection = getConnection();
const single = await repository.findOne({ where: { ChannelId: channelId }, relations: relations || [] }); const repository = connection.getRepository(Lobby);
const single = await repository.findOne({ ChannelId: channelId }, { relations: relations || [] });
return single; return single;
} }

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