parent
998ff503e3
commit
b69510ffab
63 changed files with 7360 additions and 2 deletions
2
.eslintignore
Normal file
2
.eslintignore
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
/node_modules/
|
||||||
|
/dist/
|
17
.eslintrc
Normal file
17
.eslintrc
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
{
|
||||||
|
"root": true,
|
||||||
|
"parser": "@typescript-eslint/parser",
|
||||||
|
"extends": ["eslint:recommended", "plugin:@typescript-eslint/recommended"],
|
||||||
|
"env": {
|
||||||
|
"node": true,
|
||||||
|
"es6": true
|
||||||
|
},
|
||||||
|
"parserOptions": {
|
||||||
|
"ecmaVersion": 6
|
||||||
|
},
|
||||||
|
"rules": {
|
||||||
|
"semi": ["error", "never"],
|
||||||
|
"quotes": ["error", "double"],
|
||||||
|
"@typescript-eslint/ban-ts-comment": "off"
|
||||||
|
}
|
||||||
|
}
|
BIN
.github/img/example.png
vendored
Normal file
BIN
.github/img/example.png
vendored
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.7 MiB |
BIN
.github/img/logo-white.png
vendored
Normal file
BIN
.github/img/logo-white.png
vendored
Normal file
Binary file not shown.
After Width: | Height: | Size: 20 KiB |
BIN
.github/img/logo.png
vendored
Normal file
BIN
.github/img/logo.png
vendored
Normal file
Binary file not shown.
After Width: | Height: | Size: 9.3 KiB |
146
.gitignore
vendored
Normal file
146
.gitignore
vendored
Normal file
|
@ -0,0 +1,146 @@
|
||||||
|
_/
|
||||||
|
.env
|
||||||
|
/build/
|
||||||
|
/src/secret/
|
||||||
|
|
||||||
|
|
||||||
|
# Created by https://www.toptal.com/developers/gitignore/api/node,visualstudiocode
|
||||||
|
# Edit at https://www.toptal.com/developers/gitignore?templates=node,visualstudiocode
|
||||||
|
|
||||||
|
### Node ###
|
||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
lerna-debug.log*
|
||||||
|
.pnpm-debug.log*
|
||||||
|
|
||||||
|
# Diagnostic reports (https://nodejs.org/api/report.html)
|
||||||
|
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
|
||||||
|
|
||||||
|
# Runtime data
|
||||||
|
pids
|
||||||
|
*.pid
|
||||||
|
*.seed
|
||||||
|
*.pid.lock
|
||||||
|
|
||||||
|
# Directory for instrumented libs generated by jscoverage/JSCover
|
||||||
|
lib-cov
|
||||||
|
|
||||||
|
# Coverage directory used by tools like istanbul
|
||||||
|
coverage
|
||||||
|
*.lcov
|
||||||
|
|
||||||
|
# nyc test coverage
|
||||||
|
.nyc_output
|
||||||
|
|
||||||
|
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
|
||||||
|
.grunt
|
||||||
|
|
||||||
|
# Bower dependency directory (https://bower.io/)
|
||||||
|
bower_components
|
||||||
|
|
||||||
|
# node-waf configuration
|
||||||
|
.lock-wscript
|
||||||
|
|
||||||
|
# Compiled binary addons (https://nodejs.org/api/addons.html)
|
||||||
|
build/Release
|
||||||
|
|
||||||
|
# Dependency directories
|
||||||
|
node_modules/
|
||||||
|
jspm_packages/
|
||||||
|
|
||||||
|
# Snowpack dependency directory (https://snowpack.dev/)
|
||||||
|
web_modules/
|
||||||
|
|
||||||
|
# TypeScript cache
|
||||||
|
*.tsbuildinfo
|
||||||
|
|
||||||
|
# Optional npm cache directory
|
||||||
|
.npm
|
||||||
|
|
||||||
|
# Optional eslint cache
|
||||||
|
.eslintcache
|
||||||
|
|
||||||
|
# Microbundle cache
|
||||||
|
.rpt2_cache/
|
||||||
|
.rts2_cache_cjs/
|
||||||
|
.rts2_cache_es/
|
||||||
|
.rts2_cache_umd/
|
||||||
|
|
||||||
|
# Optional REPL history
|
||||||
|
.node_repl_history
|
||||||
|
|
||||||
|
# Output of 'npm pack'
|
||||||
|
*.tgz
|
||||||
|
|
||||||
|
# Yarn Integrity file
|
||||||
|
.yarn-integrity
|
||||||
|
|
||||||
|
# dotenv environment variables file
|
||||||
|
.env
|
||||||
|
.env.test
|
||||||
|
.env.production
|
||||||
|
|
||||||
|
# parcel-bundler cache (https://parceljs.org/)
|
||||||
|
.cache
|
||||||
|
.parcel-cache
|
||||||
|
|
||||||
|
# Next.js build output
|
||||||
|
.next
|
||||||
|
out
|
||||||
|
|
||||||
|
# Nuxt.js build / generate output
|
||||||
|
.nuxt
|
||||||
|
dist
|
||||||
|
|
||||||
|
# Gatsby files
|
||||||
|
.cache/
|
||||||
|
# Comment in the public line in if your project uses Gatsby and not Next.js
|
||||||
|
# https://nextjs.org/blog/next-9-1#public-directory-support
|
||||||
|
# public
|
||||||
|
|
||||||
|
# vuepress build output
|
||||||
|
.vuepress/dist
|
||||||
|
|
||||||
|
# Serverless directories
|
||||||
|
.serverless/
|
||||||
|
|
||||||
|
# FuseBox cache
|
||||||
|
.fusebox/
|
||||||
|
|
||||||
|
# DynamoDB Local files
|
||||||
|
.dynamodb/
|
||||||
|
|
||||||
|
# TernJS port file
|
||||||
|
.tern-port
|
||||||
|
|
||||||
|
# Stores VSCode versions used for testing VSCode extensions
|
||||||
|
.vscode-test
|
||||||
|
|
||||||
|
# yarn v2
|
||||||
|
.yarn/cache
|
||||||
|
.yarn/unplugged
|
||||||
|
.yarn/build-state.yml
|
||||||
|
.yarn/install-state.gz
|
||||||
|
.pnp.*
|
||||||
|
|
||||||
|
### VisualStudioCode ###
|
||||||
|
.vscode/*
|
||||||
|
!.vscode/settings.json
|
||||||
|
!.vscode/tasks.json
|
||||||
|
!.vscode/launch.json
|
||||||
|
!.vscode/extensions.json
|
||||||
|
*.code-workspace
|
||||||
|
|
||||||
|
# Local History for Visual Studio Code
|
||||||
|
.history/
|
||||||
|
|
||||||
|
### VisualStudioCode Patch ###
|
||||||
|
# Ignore all local history of files
|
||||||
|
.history
|
||||||
|
.ionide
|
||||||
|
|
||||||
|
# End of https://www.toptal.com/developers/gitignore/api/node,visualstudiocode
|
4
.prettierrc
Normal file
4
.prettierrc
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
{
|
||||||
|
"semi": false,
|
||||||
|
"useTabs": true
|
||||||
|
}
|
3
.vscode/extensions.json
vendored
Normal file
3
.vscode/extensions.json
vendored
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
{
|
||||||
|
"recommendations": ["esbenp.prettier-vscode", "dbaeumer.vscode-eslint"]
|
||||||
|
}
|
18
.vscode/settings.json
vendored
Normal file
18
.vscode/settings.json
vendored
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
{
|
||||||
|
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
||||||
|
"editor.formatOnSave": true,
|
||||||
|
"editor.formatOnPaste": true,
|
||||||
|
"cSpell.words": [
|
||||||
|
"adminsdk",
|
||||||
|
"clapify",
|
||||||
|
"colorette",
|
||||||
|
"cooldown",
|
||||||
|
"firestore",
|
||||||
|
"Neko",
|
||||||
|
"nekos",
|
||||||
|
"OwOify"
|
||||||
|
],
|
||||||
|
"[ignore]": {
|
||||||
|
"editor.defaultFormatter": "foxundermoon.shell-format"
|
||||||
|
}
|
||||||
|
}
|
130
CONTRIBUTING.md
Normal file
130
CONTRIBUTING.md
Normal file
|
@ -0,0 +1,130 @@
|
||||||
|
# Contribution guide
|
||||||
|
|
||||||
|
## Table of contents
|
||||||
|
|
||||||
|
- [Contribution guide](#contribution-guide)
|
||||||
|
- [Table of contents](#table-of-contents)
|
||||||
|
- [Purpose](#purpose)
|
||||||
|
- [Assumptions](#assumptions)
|
||||||
|
- [Getting started](#getting-started)
|
||||||
|
- [Development environment](#development-environment)
|
||||||
|
- [Required knowledge](#required-knowledge)
|
||||||
|
- [Bot](#bot)
|
||||||
|
- [Web Interface](#web-interface)
|
||||||
|
- [Documentation](#documentation)
|
||||||
|
- [Commit message](#commit-message)
|
||||||
|
- [Creating / updating a command](#creating--updating-a-command)
|
||||||
|
- [Help message](#help-message)
|
||||||
|
- [Structure](#structure)
|
||||||
|
|
||||||
|
## Purpose
|
||||||
|
|
||||||
|
There are several goals this guide aims to achieve:
|
||||||
|
|
||||||
|
- Help new contributors get started
|
||||||
|
- Prevent information fragmentation as people work on different parts of the software
|
||||||
|
- Streamline the development workflow
|
||||||
|
- Make things work in the project owner's absence
|
||||||
|
- Minimize intervention and back-and-forth communication (e.g. The contributor didn't format their code properly)
|
||||||
|
|
||||||
|
## Assumptions
|
||||||
|
|
||||||
|
All contributors are assumed to be familiar with the following:
|
||||||
|
|
||||||
|
- git (and by extension github and developer collaboration)
|
||||||
|
- node.js and its ecosystem (npm packages, yarn, etc.)
|
||||||
|
- javascript and typescript
|
||||||
|
- code formatting
|
||||||
|
|
||||||
|
## Getting started
|
||||||
|
|
||||||
|
### Development environment
|
||||||
|
|
||||||
|
Contributors are free to use whatever IDE they want but the usage of [vscode](https://code.visualstudio.com) is highly recommended.
|
||||||
|
|
||||||
|
Format markdown file(s) with [prettier](https://prettier.io) formatter
|
||||||
|
|
||||||
|
### Required knowledge
|
||||||
|
|
||||||
|
### Bot
|
||||||
|
|
||||||
|
- [discord.js](https://discord.js.org)
|
||||||
|
- [sapphire framework](https://www.sapphirejs.dev)
|
||||||
|
|
||||||
|
### Web Interface
|
||||||
|
|
||||||
|
- [express.js](https://expressjs.com)
|
||||||
|
- [passport.js](https://www.passportjs.org)
|
||||||
|
- [firebase functions](https://firebase.google.com/docs/functions)
|
||||||
|
|
||||||
|
### Documentation
|
||||||
|
|
||||||
|
- [Docusaurus](https://docusaurus.io)
|
||||||
|
- [Markdown](https://www.markdownguide.org/basic-syntax)
|
||||||
|
|
||||||
|
## Commit message
|
||||||
|
|
||||||
|
The commit message should be a clear and concise description of what the commit does.
|
||||||
|
The first line should be no more than 50 characters and the rest no more than 72.
|
||||||
|
|
||||||
|
## Creating / updating a command
|
||||||
|
|
||||||
|
Use the `CustomCommand` defined in [src/custom/CustomCommand.ts](./src/custom/CustomCommand.ts) instead of importing from `@sapphire/framework`.
|
||||||
|
|
||||||
|
### Help message
|
||||||
|
|
||||||
|
- consistent usage information
|
||||||
|
|
||||||
|
### Structure
|
||||||
|
|
||||||
|
- one export per command
|
||||||
|
- functions should be `static` whenever possible
|
||||||
|
- Keep the `messageRun` function clean (ideally less than 50 lines).
|
||||||
|
- Separate regular imports from type imports.
|
||||||
|
- Separate the embedded message building process to a separate function.
|
||||||
|
|
||||||
|
This also makes testing easier
|
||||||
|
|
||||||
|
- jsdoc comments should...
|
||||||
|
|
||||||
|
- not include argument and return types.
|
||||||
|
|
||||||
|
They should be included in the function instead.
|
||||||
|
|
||||||
|
- not include a dash (`-`) in the `@returns` tag.
|
||||||
|
|
||||||
|
Do this:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
/**
|
||||||
|
* This function does something.
|
||||||
|
*
|
||||||
|
* - Here's some extra information
|
||||||
|
*
|
||||||
|
* @param argument - This is an argument.
|
||||||
|
* @returns some string.
|
||||||
|
*/
|
||||||
|
function someFunction(argument: string): string {
|
||||||
|
const someString = "some string"
|
||||||
|
|
||||||
|
return someString
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Not this:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
/**
|
||||||
|
* This function does something.
|
||||||
|
*
|
||||||
|
* - Here's some extra information
|
||||||
|
*
|
||||||
|
* @param {string} argument - This is an argument.
|
||||||
|
* @returns {string} - some string.
|
||||||
|
*/
|
||||||
|
function someFunction(argument) {
|
||||||
|
const someString = "some string"
|
||||||
|
|
||||||
|
return someString
|
||||||
|
}
|
||||||
|
```
|
19
LICENSE
Normal file
19
LICENSE
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
Copyright (c) 2019 developomp
|
||||||
|
|
||||||
|
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.
|
106
README.md
106
README.md
|
@ -1,2 +1,104 @@
|
||||||
**ATTENTION!**<br />
|
# Llama bot
|
||||||
The llama bot has found a new home: https://github.com/llama-bot/llama-bot
|
|
||||||
|

|
||||||
|
|
||||||
|
The Llama bot is a [discord](https://discord.com) bot made for the
|
||||||
|
[LP community discord server](https://discord.gg/2fsar34APa).<br />
|
||||||
|
|
||||||
|
## Setting up
|
||||||
|
|
||||||
|
### Pre-requirements
|
||||||
|
|
||||||
|
- Node.js 16.6.0+
|
||||||
|
- [yarn](https://yarnpkg.com)
|
||||||
|
- A Discord account
|
||||||
|
- A Google Firebase account
|
||||||
|
- ~~A sacrifice to be given to the llama gods~~ (no longer needed)
|
||||||
|
|
||||||
|
### Discord
|
||||||
|
|
||||||
|
1. Create a new application from the [Discord Developer Portal](https://discord.com/developers/applications). Select one if you already have it.
|
||||||
|
2. Go to the `Bot` tab and convert your application to a discord bot. Be cautious since this operation is **NOT REVERSIBLE**.
|
||||||
|
3. Copy the bot token. This will be used during the [Server](#server) setup.
|
||||||
|
|
||||||
|
### Firebase
|
||||||
|
|
||||||
|
1. Head over to https://console.firebase.google.com and create a firebase project.
|
||||||
|
2. Enable firestore database.
|
||||||
|
3. [Generate and download](https://console.firebase.google.com/project/_/settings/serviceaccounts/adminsdk) the service account key.
|
||||||
|
This will be used during the [Server](#server) setup.
|
||||||
|
|
||||||
|
### Server
|
||||||
|
|
||||||
|
- Assumes UNIX-like environment (Linux, BSD, Mac, etc.)
|
||||||
|
|
||||||
|
1. Clone this repository and open it.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone https://github.com/llama-bot/llama-bot.git
|
||||||
|
```
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd llama-bot
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Install dependencies.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
yarn install
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Create `.env` file in the project root and put the discord bot token generated during the [Discord](#discord) setup.
|
||||||
|
|
||||||
|
```dosini
|
||||||
|
TOKEN=PUT_YOUR_DISCORD_BOT_TOKEN_HERE
|
||||||
|
TESTING=true # set it to false on production
|
||||||
|
PREFIX_PROD=PUT_PRODUCTION_DEFAULT_PREFIX_HERE
|
||||||
|
PREFIX_DEV=PUT_DEVELOPMENT_DEFAULT_PREFIX_HERE
|
||||||
|
OWNER_IDS=ID1,ID2,ID3,...
|
||||||
|
```
|
||||||
|
|
||||||
|
4. Create `secret` directory in the `src` directory,
|
||||||
|
rename the firebase admin key generated during the [Firebase](#firebase) setup to `firebase-adminsdk.json`,
|
||||||
|
and put it in the `secret` directory.
|
||||||
|
|
||||||
|
5. Build the bot.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
yarn build
|
||||||
|
```
|
||||||
|
|
||||||
|
6. Install pm2 globally.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
yarn global add pm2
|
||||||
|
```
|
||||||
|
|
||||||
|
7. Start the bot.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pm2 start build/index.js --watch --name "Llama Bot"
|
||||||
|
```
|
||||||
|
|
||||||
|
| Option | Explanation |
|
||||||
|
| -------------------- | ---------------------------------------------------------- |
|
||||||
|
| `--watch` | Auto restart bot if bot files have been changed |
|
||||||
|
| `--name "Llama Bot"` | Set the name of the process so it can be easily recognized |
|
||||||
|
|
||||||
|
8. Make the process automatically start on boot.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pm2 startup
|
||||||
|
```
|
||||||
|
|
||||||
|
## More info
|
||||||
|
|
||||||
|
- [discord developers documentation](https://discord.com/developers/docs)
|
||||||
|
- discord API's javascript implementation [documentation](https://discord.js.org/#/docs), [guide](https://discordjs.guide), and bot [framework documentation](https://sapphiredev.github.io/framework)
|
||||||
|
- [firebase admin sdk documentation](https://firebase.google.com/docs)
|
||||||
|
- [pm2 documentation](https://pm2.keymetrics.io/docs/usage/quick-start)
|
||||||
|
|
||||||
|
## Special thanks
|
||||||
|
|
||||||
|
- `Dabidoo#9888 (265697563280146433)` for making the [colored logo](./.github/img/logo.png)
|
||||||
|
- `Sɪʟᴋ Sᴘɪᴅᴇʀ#8364 (419184817368858644)` for making the [white logo](./.github/img/logo-white.png)
|
||||||
|
|
5
jest.config.js
Normal file
5
jest.config.js
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
/** @type {import('ts-jest/dist/types').InitialOptionsTsJest} */
|
||||||
|
module.exports = {
|
||||||
|
preset: "ts-jest",
|
||||||
|
testEnvironment: "node",
|
||||||
|
}
|
36
package.json
Normal file
36
package.json
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
{
|
||||||
|
"main": "dist/index.js",
|
||||||
|
"private": true,
|
||||||
|
"scripts": {
|
||||||
|
"start": "yarn build && node dist/index.js",
|
||||||
|
"test": "jest",
|
||||||
|
"lint": "eslint .",
|
||||||
|
"clean": "del dist",
|
||||||
|
"build": "yarn clean && tsc"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@sapphire/decorators": "^4.2.6",
|
||||||
|
"@sapphire/framework": "^2.4.1",
|
||||||
|
"discord.js": "^13.6.0",
|
||||||
|
"firebase-admin": "^10.0.2",
|
||||||
|
"nekos.life": "^2.0.9"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/jest": "^27.4.1",
|
||||||
|
"@types/node": "^17.0.21",
|
||||||
|
"@types/string-similarity": "^4.0.0",
|
||||||
|
"@typescript-eslint/eslint-plugin": "^5.13.0",
|
||||||
|
"@typescript-eslint/parser": "^5.13.0",
|
||||||
|
"colorette": "^2.0.16",
|
||||||
|
"del-cli": "^4.0.1",
|
||||||
|
"dotenv": "^16.0.0",
|
||||||
|
"eslint": "^8.10.0",
|
||||||
|
"jest": "^27.5.1",
|
||||||
|
"pretty-error": "^4.0.0",
|
||||||
|
"string-similarity": "^4.0.4",
|
||||||
|
"ts-jest": "^27.1.3",
|
||||||
|
"tslint-config-prettier": "^1.18.0",
|
||||||
|
"typescript": "^4.6.2",
|
||||||
|
"utility-types": "^3.10.0"
|
||||||
|
}
|
||||||
|
}
|
29
src/DB/defaults.ts
Normal file
29
src/DB/defaults.ts
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
import type { Settings } from "../types/bot"
|
||||||
|
|
||||||
|
export const defaultSettings: Settings = {
|
||||||
|
clear_emojis: ["🧹"],
|
||||||
|
quotes: [
|
||||||
|
"Paul: Caaaaarrrrrlllll!!!!",
|
||||||
|
"Carl: And I... I... stabbed him 37 times in the chest!",
|
||||||
|
"Carl: Well, I kill people and I eat hands! That's—that's two things!",
|
||||||
|
"Carl: That is what forgiveness sounds like, screaming and then silence.",
|
||||||
|
"Carl: Probably because I'm a dangerous sociopath with a long history of violence.",
|
||||||
|
"Carl: I may have created a crack in space time... through which to collect millions of baby hands.",
|
||||||
|
"Carl: Whities gotta pay... and the payment is baby hands.",
|
||||||
|
"Paul: CAAAAAAAAARL! WHAT DID YOU DO?",
|
||||||
|
"Carl: My stomach was making the rumblies - that only hands could satisfy.",
|
||||||
|
"Paul: What is wrong with you, Carl?",
|
||||||
|
"Paul: And then you started making out with the ice sculptures.",
|
||||||
|
"Carl: I will not apologize for art.",
|
||||||
|
"Carl: Well, I'm building a meat dragon and not just any meat will do.",
|
||||||
|
"Carl: What's that? It's hard to hear you over the sound of melting city!",
|
||||||
|
"Carl: Who's laughing? Clearly not all the people who've just exploded.",
|
||||||
|
"Paul: All you do is kill people, Carl!\n" +
|
||||||
|
"Carl:That's like saying all Mozart did was write songs.",
|
||||||
|
"Carl: It's not a meat grinder, it's an orphan stomper.",
|
||||||
|
"Carl: Let me explain: Efficiency, industry, never before has this many dead bodies been so manageable.",
|
||||||
|
"Carl: I'm the Henry Ford of human meat!",
|
||||||
|
`Paul: It's horrifying, Carl.
|
||||||
|
Carl: Thank you.`,
|
||||||
|
],
|
||||||
|
}
|
89
src/DB/index.ts
Normal file
89
src/DB/index.ts
Normal file
|
@ -0,0 +1,89 @@
|
||||||
|
/**
|
||||||
|
* @file Firestore Database interface for the bot.
|
||||||
|
*
|
||||||
|
* More information about firestore can be found here: https://firebase.google.com/docs/firestore
|
||||||
|
* Though there are no plans to move away from firebase,
|
||||||
|
* all firebase interactions are contained in this file so it's easier to move to other platform.
|
||||||
|
*
|
||||||
|
* Database structure:
|
||||||
|
* bot/ // Discord Bot related data
|
||||||
|
* settings/ // Discord bot settings
|
||||||
|
* servers/ // Server-specific data
|
||||||
|
* SERVER_ID/ // discord server ID (snowflake)
|
||||||
|
* settings/ // server-specific settings
|
||||||
|
* name: string // server name
|
||||||
|
* vars/ // server-specific variables
|
||||||
|
* users/ // user data
|
||||||
|
* USER_ID/ // discord user ID (snowflake)
|
||||||
|
* avatar: string // discord avatar ID
|
||||||
|
* discriminator: string // a 4 digit numerical discord user discriminator
|
||||||
|
* id: string // discord user ID (snowflake)
|
||||||
|
* username: string // discord user name
|
||||||
|
*/
|
||||||
|
|
||||||
|
// todo: create and populate if it doesn't exist already
|
||||||
|
|
||||||
|
import admin from "firebase-admin"
|
||||||
|
import serviceAccountKey from "../secret/firebase-adminsdk.json"
|
||||||
|
|
||||||
|
import type { Snowflake } from "discord-api-types"
|
||||||
|
import type { Settings, Servers, ServerData } from "../types/bot"
|
||||||
|
|
||||||
|
admin.initializeApp({
|
||||||
|
credential: admin.credential.cert(serviceAccountKey as admin.ServiceAccount),
|
||||||
|
})
|
||||||
|
|
||||||
|
const firestoreDB = admin.firestore()
|
||||||
|
|
||||||
|
const botCollection = firestoreDB.collection("bot")
|
||||||
|
const serversCollection = firestoreDB.collection("servers")
|
||||||
|
|
||||||
|
const settingsDoc = botCollection.doc("settings")
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Global bot settings. Fetched by calling the {@link fetchSettings} function.
|
||||||
|
*/
|
||||||
|
export let settings: Settings = {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Server-specific data.
|
||||||
|
* Fetched by calling the {@link fetchServerData} function.
|
||||||
|
*/
|
||||||
|
export const servers: Servers = {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch settings from the database.
|
||||||
|
*/
|
||||||
|
export async function fetchSettings(): Promise<void> {
|
||||||
|
await settingsDoc.get().then((doc) => {
|
||||||
|
settings = doc.data() as Settings
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch server-specific data from the database.
|
||||||
|
*/
|
||||||
|
export async function fetchServerData(
|
||||||
|
serverSnowflake: Snowflake
|
||||||
|
): Promise<void> {
|
||||||
|
await serversCollection
|
||||||
|
.doc(serverSnowflake)
|
||||||
|
.get()
|
||||||
|
.then((doc) => {
|
||||||
|
servers[serverSnowflake] = doc.data() as unknown as ServerData
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// initialize
|
||||||
|
//
|
||||||
|
|
||||||
|
// todo: make sure settings is fetched before anyone runs a command
|
||||||
|
fetchSettings()
|
||||||
|
|
||||||
|
export default {
|
||||||
|
settings,
|
||||||
|
servers,
|
||||||
|
fetchSettings,
|
||||||
|
fetchServerData,
|
||||||
|
}
|
240
src/commands/core/help.ts
Normal file
240
src/commands/core/help.ts
Normal file
|
@ -0,0 +1,240 @@
|
||||||
|
// todo: also prevent command from having same name as command category
|
||||||
|
|
||||||
|
import { MessageEmbed } from "discord.js"
|
||||||
|
import { ApplyOptions } from "@sapphire/decorators"
|
||||||
|
import stringSimilarity from "string-similarity"
|
||||||
|
|
||||||
|
import type { Message } from "discord.js"
|
||||||
|
import type {
|
||||||
|
Args,
|
||||||
|
Command,
|
||||||
|
CommandOptions,
|
||||||
|
CommandStore,
|
||||||
|
} from "@sapphire/framework"
|
||||||
|
|
||||||
|
import CustomCommand from "../../custom/CustomCommand"
|
||||||
|
|
||||||
|
enum QueryType {
|
||||||
|
empty = "empty",
|
||||||
|
command = "command",
|
||||||
|
category = "category",
|
||||||
|
unknown = "unknown",
|
||||||
|
}
|
||||||
|
|
||||||
|
type CategorizeQueryReturn =
|
||||||
|
| { queryType: QueryType.empty }
|
||||||
|
| { queryType: QueryType.command; command: Command }
|
||||||
|
| { queryType: QueryType.category }
|
||||||
|
| { queryType: QueryType.unknown }
|
||||||
|
|
||||||
|
@ApplyOptions<CommandOptions>({
|
||||||
|
aliases: ["h"],
|
||||||
|
description:
|
||||||
|
"Shows list of helpful information about a command or a command category.",
|
||||||
|
})
|
||||||
|
export default class HelpCommand extends CustomCommand {
|
||||||
|
usage = `> {$} ["command"|"category"]
|
||||||
|
|
||||||
|
ex:
|
||||||
|
List categories:
|
||||||
|
> {$}
|
||||||
|
|
||||||
|
List commands in the \`util\` category:
|
||||||
|
> {$} util
|
||||||
|
|
||||||
|
Shows information about the \`ping\` command:
|
||||||
|
> {$} ping`
|
||||||
|
|
||||||
|
//
|
||||||
|
commands: CommandStore = this.container.client.stores.get("commands")
|
||||||
|
// lower case names of categories
|
||||||
|
lowerCaseCategoryNames: string[] = []
|
||||||
|
// all command names and aliases
|
||||||
|
allCommands: string[] = []
|
||||||
|
|
||||||
|
async messageRun(message: Message, args: Args): Promise<void> {
|
||||||
|
if (this.lowerCaseCategoryNames.length <= 0) {
|
||||||
|
// can not put this in class constructor because then `this.commands.categories` will be equal to `[]`
|
||||||
|
// can not put this in the "ready" listener either because of race conditions
|
||||||
|
this.lowerCaseCategoryNames = this.commands.categories.map((elem) =>
|
||||||
|
elem.toLowerCase()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const query: string = await args.pick("string").catch(() => "")
|
||||||
|
const queryCategory = this.categorizeQuery(query)
|
||||||
|
|
||||||
|
switch (queryCategory.queryType) {
|
||||||
|
case QueryType.empty:
|
||||||
|
this.sendDefaultHelpMessage(message)
|
||||||
|
break
|
||||||
|
|
||||||
|
case QueryType.command:
|
||||||
|
this.sendCommandHelpMessage(message, queryCategory.command)
|
||||||
|
break
|
||||||
|
|
||||||
|
case QueryType.category:
|
||||||
|
this.sendCategoryHelpMessage(message, query)
|
||||||
|
break
|
||||||
|
|
||||||
|
default:
|
||||||
|
this.sendCommandNotFoundMessage(message, query)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
categorizeQuery(input: string): CategorizeQueryReturn {
|
||||||
|
const query = input.toLowerCase()
|
||||||
|
|
||||||
|
if (!query) return { queryType: QueryType.empty }
|
||||||
|
|
||||||
|
if (this.lowerCaseCategoryNames.includes(query))
|
||||||
|
return { queryType: QueryType.category }
|
||||||
|
|
||||||
|
const command = this.commands.find(
|
||||||
|
(command: Command, key: string) =>
|
||||||
|
key.toLowerCase() === query ||
|
||||||
|
command.aliases.some((elem) => elem.toLowerCase() === query)
|
||||||
|
)
|
||||||
|
if (command) return { queryType: QueryType.command, command }
|
||||||
|
|
||||||
|
return { queryType: QueryType.unknown }
|
||||||
|
}
|
||||||
|
|
||||||
|
sendDefaultHelpMessage(message: Message): void {
|
||||||
|
const helpEmbed = new MessageEmbed({
|
||||||
|
title: "Help",
|
||||||
|
description: `Use the \`${process.env.PREFIX}help <category>\` command to get more information about a category.
|
||||||
|
This command is not case sensitive.
|
||||||
|
|
||||||
|
You can read more about the bot in the [documentation](https://docs.llama.developomp.com/docs/usage/overview).
|
||||||
|
|
||||||
|
**Categories:**`,
|
||||||
|
})
|
||||||
|
|
||||||
|
//
|
||||||
|
// add categories
|
||||||
|
//
|
||||||
|
|
||||||
|
this.commands.categories.map((categoryName) => {
|
||||||
|
const commandsInCategory = this.commands.filter((command) =>
|
||||||
|
command.fullCategory.includes(categoryName)
|
||||||
|
)
|
||||||
|
|
||||||
|
helpEmbed.addField(
|
||||||
|
categoryName,
|
||||||
|
`${commandsInCategory.size} commands`,
|
||||||
|
true
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
//
|
||||||
|
// reply
|
||||||
|
//
|
||||||
|
|
||||||
|
message.channel.send({
|
||||||
|
embeds: [helpEmbed],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
sendCommandHelpMessage(message: Message, command: Command): void {
|
||||||
|
const aliases = command.aliases
|
||||||
|
? `\`${command.aliases.join("`, `")}\``
|
||||||
|
: "None"
|
||||||
|
|
||||||
|
message.channel.send({
|
||||||
|
embeds: [
|
||||||
|
new MessageEmbed({
|
||||||
|
title: command.name,
|
||||||
|
description: `Aliases: ${aliases}`,
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: "Description",
|
||||||
|
value: command.description || "WIP",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Usage",
|
||||||
|
value:
|
||||||
|
// replace `{$}` with <prefix><command>
|
||||||
|
command.usage?.replace(
|
||||||
|
/{\$}/g,
|
||||||
|
`${process.env.PREFIX}${command.name}`
|
||||||
|
) || "WIP",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
sendCategoryHelpMessage(message: Message, query: string): void {
|
||||||
|
//
|
||||||
|
// find category
|
||||||
|
//
|
||||||
|
|
||||||
|
let selectedCategoryName = ""
|
||||||
|
|
||||||
|
const lowerCaseCategoryName = query.toLowerCase()
|
||||||
|
this.commands.categories.some((categoryName) => {
|
||||||
|
if (categoryName.toLowerCase() === lowerCaseCategoryName) {
|
||||||
|
selectedCategoryName = categoryName
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
//
|
||||||
|
// Find commands in category
|
||||||
|
//
|
||||||
|
|
||||||
|
const commandsInCategory = this.commands.filter((command) =>
|
||||||
|
command.fullCategory.includes(selectedCategoryName)
|
||||||
|
)
|
||||||
|
|
||||||
|
//
|
||||||
|
// Reply
|
||||||
|
//
|
||||||
|
|
||||||
|
message.channel.send({
|
||||||
|
embeds: [
|
||||||
|
new MessageEmbed({
|
||||||
|
title: `${selectedCategoryName} category`,
|
||||||
|
description: `Use the \`${process.env.PREFIX}help <command>\` command to get more information about a command.
|
||||||
|
This command is not case sensitive.`,
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: "commands",
|
||||||
|
value: commandsInCategory
|
||||||
|
.map((command) => `- \`${command.name}\`\n`)
|
||||||
|
.join(""),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
sendCommandNotFoundMessage(message: Message, query: string): void {
|
||||||
|
if (this.allCommands.length <= 0) {
|
||||||
|
this.allCommands = [...this.commands.keys()].concat(
|
||||||
|
this.commands.aliases
|
||||||
|
.map((elem) => elem.aliases)
|
||||||
|
.reduce((prev, curr) => prev.concat(curr)) as string[]
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const mostLikelyGuess =
|
||||||
|
this.allCommands[
|
||||||
|
stringSimilarity.findBestMatch(query, this.allCommands).bestMatchIndex
|
||||||
|
]
|
||||||
|
|
||||||
|
message.channel.send({
|
||||||
|
embeds: [
|
||||||
|
new MessageEmbed({
|
||||||
|
title: "Command not found",
|
||||||
|
description: `Command \`${query}\` was not found. Did you mean \`${mostLikelyGuess}\`?
|
||||||
|
You can also use the \`${process.env.PREFIX}help\` command to list all available commands.`,
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
29
src/commands/core/testing.ts
Normal file
29
src/commands/core/testing.ts
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
/**
|
||||||
|
* Cannot name file to `test.ts` because then it'll treat it like a testing file.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { ApplyOptions } from "@sapphire/decorators"
|
||||||
|
|
||||||
|
import type { Message } from "discord.js"
|
||||||
|
import type { Args, CommandOptions } from "@sapphire/framework"
|
||||||
|
|
||||||
|
import CustomCommand from "../../custom/CustomCommand"
|
||||||
|
|
||||||
|
@ApplyOptions<CommandOptions>({
|
||||||
|
aliases: ["t"],
|
||||||
|
description: "Tests bot features. Only available to bot owners.",
|
||||||
|
preconditions: ["OwnersOnly"],
|
||||||
|
})
|
||||||
|
export default class TestCommand extends CustomCommand {
|
||||||
|
usage = `> {$} [feature]
|
||||||
|
|
||||||
|
If feature is not passed, then the command will result in a error.
|
||||||
|
This is an intended behavior to make testing easier.
|
||||||
|
`
|
||||||
|
|
||||||
|
async messageRun(message: Message, args: Args) {
|
||||||
|
await args.pick("string").catch(() => {
|
||||||
|
throw new Error("") // the intended behavior
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
64
src/commands/fun/8ball.ts
Normal file
64
src/commands/fun/8ball.ts
Normal file
|
@ -0,0 +1,64 @@
|
||||||
|
import { MessageEmbed } from "discord.js"
|
||||||
|
import { ApplyOptions } from "@sapphire/decorators"
|
||||||
|
|
||||||
|
import type { Message } from "discord.js"
|
||||||
|
import type { Args, CommandOptions } from "@sapphire/framework"
|
||||||
|
|
||||||
|
import { globalObject } from "../.."
|
||||||
|
import CustomCommand from "../../custom/CustomCommand"
|
||||||
|
|
||||||
|
@ApplyOptions<CommandOptions>({
|
||||||
|
aliases: ["8", "ball"],
|
||||||
|
description:
|
||||||
|
"Gives you the best advice you can get. We are not responsible for your action though.",
|
||||||
|
})
|
||||||
|
export default class EightBallCommand extends CustomCommand {
|
||||||
|
usage = `> {$} [your question]
|
||||||
|
|
||||||
|
e.g.
|
||||||
|
> {$}
|
||||||
|
|
||||||
|
> {$} Should I buy more doge coin?
|
||||||
|
`
|
||||||
|
|
||||||
|
async messageRun(message: Message, args: Args) {
|
||||||
|
message.channel.sendTyping()
|
||||||
|
|
||||||
|
//
|
||||||
|
// Parse user input
|
||||||
|
//
|
||||||
|
|
||||||
|
let text = ""
|
||||||
|
|
||||||
|
const allStr = await args.repeat("string").catch(() => "")
|
||||||
|
|
||||||
|
if (typeof allStr === "string") {
|
||||||
|
text = allStr
|
||||||
|
}
|
||||||
|
if (Array.isArray(allStr)) {
|
||||||
|
text = allStr.join(" ")
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Get response from nekos.life
|
||||||
|
//
|
||||||
|
|
||||||
|
const { response, url } = await globalObject.nekosClient.sfw["eightBall"]({
|
||||||
|
text,
|
||||||
|
})
|
||||||
|
|
||||||
|
//
|
||||||
|
// Reply
|
||||||
|
//
|
||||||
|
|
||||||
|
message.channel.send({
|
||||||
|
embeds: [
|
||||||
|
new MessageEmbed({
|
||||||
|
title: response,
|
||||||
|
image: { url },
|
||||||
|
footer: { text: "powered by nekos.life" },
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
59
src/commands/fun/clapify.ts
Normal file
59
src/commands/fun/clapify.ts
Normal file
|
@ -0,0 +1,59 @@
|
||||||
|
import { Formatters, MessageEmbed } from "discord.js"
|
||||||
|
import { ApplyOptions } from "@sapphire/decorators"
|
||||||
|
|
||||||
|
import type { Message } from "discord.js"
|
||||||
|
import type { Args, CommandOptions } from "@sapphire/framework"
|
||||||
|
|
||||||
|
import CustomCommand from "../../custom/CustomCommand"
|
||||||
|
|
||||||
|
@ApplyOptions<CommandOptions>({
|
||||||
|
aliases: ["c", "clap"],
|
||||||
|
description:
|
||||||
|
"Does the annoying Karen clap.👏Does👏not👏work👏with👏external👏emojis.",
|
||||||
|
})
|
||||||
|
export default class ClapifyCommand extends CustomCommand {
|
||||||
|
usage = `> {$} [message to clapify]
|
||||||
|
|
||||||
|
e.g.
|
||||||
|
> {$} I said bring me the manager.
|
||||||
|
`
|
||||||
|
|
||||||
|
async messageRun(message: Message, args: Args): Promise<void> {
|
||||||
|
const inputs = await args.repeat("string").catch(() => [])
|
||||||
|
|
||||||
|
//
|
||||||
|
// Handle empty argument
|
||||||
|
//
|
||||||
|
|
||||||
|
if (!inputs) {
|
||||||
|
message.channel.send({
|
||||||
|
embeds: [
|
||||||
|
new MessageEmbed({
|
||||||
|
title: "Error!",
|
||||||
|
description: "What should I clapify?",
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
})
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Reply
|
||||||
|
//
|
||||||
|
|
||||||
|
message.channel.send({
|
||||||
|
embeds: [
|
||||||
|
new MessageEmbed({
|
||||||
|
description: `**${Formatters.userMention(message.author.id)} says:**
|
||||||
|
|
||||||
|
${ClapifyCommand.clapify(inputs)}`,
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
static clapify(inputs: string[]): string {
|
||||||
|
return inputs.join("👏")
|
||||||
|
}
|
||||||
|
}
|
30
src/commands/fun/fact.ts
Normal file
30
src/commands/fun/fact.ts
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
import { MessageEmbed } from "discord.js"
|
||||||
|
import { ApplyOptions } from "@sapphire/decorators"
|
||||||
|
|
||||||
|
import type { Message } from "discord.js"
|
||||||
|
import type { CommandOptions } from "@sapphire/framework"
|
||||||
|
|
||||||
|
import { globalObject } from "../.."
|
||||||
|
import CustomCommand from "../../custom/CustomCommand"
|
||||||
|
|
||||||
|
@ApplyOptions<CommandOptions>({
|
||||||
|
aliases: ["f", "facts"],
|
||||||
|
description: "Shows useless facts.",
|
||||||
|
})
|
||||||
|
export default class FactCommand extends CustomCommand {
|
||||||
|
usage = "> {$}"
|
||||||
|
|
||||||
|
async messageRun(message: Message) {
|
||||||
|
message.channel.sendTyping()
|
||||||
|
|
||||||
|
message.channel.send({
|
||||||
|
embeds: [
|
||||||
|
new MessageEmbed({
|
||||||
|
title: "Here's a fact for you",
|
||||||
|
description: (await globalObject.nekosClient.sfw.fact()).fact,
|
||||||
|
footer: { text: "powered by nekos.life" },
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
205
src/commands/fun/image.ts
Normal file
205
src/commands/fun/image.ts
Normal file
|
@ -0,0 +1,205 @@
|
||||||
|
import { CommandOptions } from "@sapphire/framework"
|
||||||
|
import { ApplyOptions } from "@sapphire/decorators"
|
||||||
|
import { Formatters, MessageEmbed } from "discord.js"
|
||||||
|
|
||||||
|
import type NekoClient from "nekos.life"
|
||||||
|
import type { FunctionKeys, $PropertyType } from "utility-types"
|
||||||
|
import type { Message } from "discord.js"
|
||||||
|
import type { Args } from "@sapphire/framework"
|
||||||
|
|
||||||
|
import { isChannelInMessageNSFW, caseInsensitiveIndexOf } from "../../util"
|
||||||
|
import { globalObject } from "../.."
|
||||||
|
import CustomCommand from "../../custom/CustomCommand"
|
||||||
|
|
||||||
|
type nsfwOptionsType = FunctionKeys<$PropertyType<NekoClient, "nsfw">>
|
||||||
|
type sfwOptionsType = Exclude<
|
||||||
|
FunctionKeys<$PropertyType<NekoClient, "sfw">>,
|
||||||
|
"why" | "catText" | "OwOify" | "eightBall" | "fact" | "spoiler"
|
||||||
|
>
|
||||||
|
|
||||||
|
const nsfwOptions: nsfwOptionsType[] = Object.getOwnPropertyNames(
|
||||||
|
globalObject.nekosClient.nsfw
|
||||||
|
) as nsfwOptionsType[]
|
||||||
|
|
||||||
|
const sfwOptions: sfwOptionsType[] = Object.getOwnPropertyNames(
|
||||||
|
globalObject.nekosClient.sfw
|
||||||
|
).filter(
|
||||||
|
(elem) =>
|
||||||
|
// the return values for these options do not have the url attribute
|
||||||
|
!["why", "catText", "OwOify", "eightBall", "fact", "spoiler"].includes(elem)
|
||||||
|
) as sfwOptionsType[]
|
||||||
|
|
||||||
|
@ApplyOptions<CommandOptions>({
|
||||||
|
aliases: ["i", "img", "images"],
|
||||||
|
description: "Shows some good images",
|
||||||
|
})
|
||||||
|
export default class ImageCommand extends CustomCommand {
|
||||||
|
usage = `> {$} [<"nsfw"|"sfw"|"list"|"help"> <image type>]
|
||||||
|
|
||||||
|
e.g.
|
||||||
|
List all available image types:
|
||||||
|
> {$} help
|
||||||
|
|
||||||
|
Show an image of a puppy:
|
||||||
|
> {$} woof
|
||||||
|
`
|
||||||
|
|
||||||
|
async messageRun(message: Message, args: Args): Promise<void> {
|
||||||
|
const option1 = (await args.pick("string").catch(() => "")).toLowerCase()
|
||||||
|
const option2 = await args.pick("string").catch(() => "")
|
||||||
|
|
||||||
|
//
|
||||||
|
// Show help message
|
||||||
|
//
|
||||||
|
|
||||||
|
if (!option1 || !option2 || option1 === "list" || option1 === "help") {
|
||||||
|
this.list(message)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Handle invalid input
|
||||||
|
//
|
||||||
|
|
||||||
|
if (option1 != "sfw" && option1 != "nsfw") {
|
||||||
|
message.channel.send({
|
||||||
|
embeds: [
|
||||||
|
new MessageEmbed({
|
||||||
|
title: "Error!",
|
||||||
|
description:
|
||||||
|
"Option should be either `list`, `help`, `nsfw` or `sfw`",
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Logic starts here
|
||||||
|
//
|
||||||
|
|
||||||
|
message.channel.sendTyping()
|
||||||
|
|
||||||
|
//
|
||||||
|
// Get option index
|
||||||
|
//
|
||||||
|
|
||||||
|
let index = -1
|
||||||
|
|
||||||
|
if (option1 === "sfw") {
|
||||||
|
index = caseInsensitiveIndexOf(nsfwOptions, option2)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (option1 === "nsfw") {
|
||||||
|
index = caseInsensitiveIndexOf(nsfwOptions, option2)
|
||||||
|
}
|
||||||
|
|
||||||
|
// check if option is valid
|
||||||
|
if (index < 0) {
|
||||||
|
this.option2NotFound(message, option2)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// check if the channel allows NSFW content
|
||||||
|
//
|
||||||
|
|
||||||
|
// todo: handle server-wide NSFW settings
|
||||||
|
if (option1 === "nsfw" && !isChannelInMessageNSFW(message)) {
|
||||||
|
message.channel.send({
|
||||||
|
embeds: [
|
||||||
|
new MessageEmbed({
|
||||||
|
title: "Error!",
|
||||||
|
description: "You cannot run this command outside NSFW channels.",
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
})
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Get image url
|
||||||
|
//
|
||||||
|
|
||||||
|
let url = ""
|
||||||
|
if (option1 === "sfw") {
|
||||||
|
url = (await globalObject.nekosClient.sfw[sfwOptions[index]]()).url
|
||||||
|
}
|
||||||
|
if (option1 === "nsfw") {
|
||||||
|
url = (await globalObject.nekosClient.nsfw[nsfwOptions[index]]()).url
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Send image
|
||||||
|
//
|
||||||
|
|
||||||
|
this.sendImage(message, url)
|
||||||
|
}
|
||||||
|
|
||||||
|
sendImage(message: Message, url: string): void {
|
||||||
|
if (!message.member) {
|
||||||
|
message.channel.send({
|
||||||
|
embeds: [
|
||||||
|
new MessageEmbed({
|
||||||
|
title: "Error",
|
||||||
|
description: "Failed to identify command caller",
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
})
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
message.channel.send({
|
||||||
|
embeds: [
|
||||||
|
new MessageEmbed({
|
||||||
|
title: "Image",
|
||||||
|
description: `requested by: ${Formatters.userMention(
|
||||||
|
message.member.id
|
||||||
|
)}\n**[Click if you don't see the image](${url})**`,
|
||||||
|
image: { url },
|
||||||
|
footer: { text: "powered by nekos.life" },
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
option2NotFound(message: Message, option: string): void {
|
||||||
|
message.channel.send({
|
||||||
|
embeds: [
|
||||||
|
new MessageEmbed({
|
||||||
|
title: "Error!",
|
||||||
|
description: `Option \`${option}\` is not a valid option.`,
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
})
|
||||||
|
|
||||||
|
this.list(message)
|
||||||
|
}
|
||||||
|
|
||||||
|
list(message: Message): void {
|
||||||
|
message.channel.send({
|
||||||
|
embeds: [
|
||||||
|
new MessageEmbed({
|
||||||
|
title: "Image Options",
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: "Usage",
|
||||||
|
value: this.usage,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "NSFW",
|
||||||
|
value: `\`${nsfwOptions.join("`, `")}\``,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "SFW",
|
||||||
|
value: `\`${sfwOptions.join("`, `")}\``,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
34
src/commands/fun/llama.ts
Normal file
34
src/commands/fun/llama.ts
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
import { CommandOptions } from "@sapphire/framework"
|
||||||
|
import { ApplyOptions } from "@sapphire/decorators"
|
||||||
|
import { MessageEmbed } from "discord.js"
|
||||||
|
|
||||||
|
import type { Message } from "discord.js"
|
||||||
|
|
||||||
|
import { settings, fetchSettings } from "../../DB"
|
||||||
|
import CustomCommand from "../../custom/CustomCommand"
|
||||||
|
|
||||||
|
@ApplyOptions<CommandOptions>({
|
||||||
|
aliases: ["l", "llamaQuote", "llamaQuotes", "lq"],
|
||||||
|
description: "Shows a random llama quote.",
|
||||||
|
})
|
||||||
|
export default class LlamaCommand extends CustomCommand {
|
||||||
|
usage = "> {$}"
|
||||||
|
|
||||||
|
async messageRun(message: Message) {
|
||||||
|
if (!settings.quotes) {
|
||||||
|
await fetchSettings()
|
||||||
|
}
|
||||||
|
|
||||||
|
message.channel.send({
|
||||||
|
embeds: [
|
||||||
|
new MessageEmbed({
|
||||||
|
title: "Llama quote that'll make your day",
|
||||||
|
description:
|
||||||
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
|
// @ts-ignore
|
||||||
|
settings.quotes[Number(message.id) % settings.quotes.length],
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
42
src/commands/fun/owo.ts
Normal file
42
src/commands/fun/owo.ts
Normal file
|
@ -0,0 +1,42 @@
|
||||||
|
import { Formatters, MessageEmbed } from "discord.js"
|
||||||
|
import { CommandOptions } from "@sapphire/framework"
|
||||||
|
import { ApplyOptions } from "@sapphire/decorators"
|
||||||
|
|
||||||
|
import type { Message } from "discord.js"
|
||||||
|
import type { Args } from "@sapphire/framework"
|
||||||
|
|
||||||
|
import { globalObject } from "../.."
|
||||||
|
import CustomCommand from "../../custom/CustomCommand"
|
||||||
|
|
||||||
|
@ApplyOptions<CommandOptions>({
|
||||||
|
aliases: ["owoify"],
|
||||||
|
description: "OwOifies youw message OwO",
|
||||||
|
})
|
||||||
|
export default class CatCommand extends CustomCommand {
|
||||||
|
usage = "> {$} [message to owoify]"
|
||||||
|
|
||||||
|
async messageRun(message: Message, args: Args) {
|
||||||
|
message.channel.sendTyping()
|
||||||
|
|
||||||
|
// combine all arguments to a single string
|
||||||
|
const input = [...(await args.repeat("string").catch(() => ""))].join(" ")
|
||||||
|
|
||||||
|
message.channel.send({
|
||||||
|
embeds: [
|
||||||
|
new MessageEmbed({
|
||||||
|
title: "OwO",
|
||||||
|
description: `**${Formatters.userMention(message.author.id)} says:**
|
||||||
|
|
||||||
|
${
|
||||||
|
(
|
||||||
|
await globalObject.nekosClient.sfw.OwOify({
|
||||||
|
text: input,
|
||||||
|
})
|
||||||
|
).owo
|
||||||
|
}`,
|
||||||
|
footer: { text: "powered by nekos.life" },
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
114
src/commands/fun/pp.ts
Normal file
114
src/commands/fun/pp.ts
Normal file
|
@ -0,0 +1,114 @@
|
||||||
|
import { Formatters } from "discord.js"
|
||||||
|
import { ApplyOptions } from "@sapphire/decorators"
|
||||||
|
|
||||||
|
import type { Message } from "discord.js"
|
||||||
|
import type { Args, CommandOptions } from "@sapphire/framework"
|
||||||
|
|
||||||
|
import { sendEmbeddedMessage } from "../../util"
|
||||||
|
import CustomCommand from "../../custom/CustomCommand"
|
||||||
|
|
||||||
|
interface PPUser {
|
||||||
|
id: string
|
||||||
|
length: number
|
||||||
|
}
|
||||||
|
|
||||||
|
@ApplyOptions<CommandOptions>({
|
||||||
|
aliases: ["penis"],
|
||||||
|
description: `Measure user's pp length and arrange them in descending order.
|
||||||
|
|
||||||
|
Shortest length (0):
|
||||||
|
\`8D\`
|
||||||
|
Longest length (30):
|
||||||
|
\`8==============================D\`
|
||||||
|
|
||||||
|
This is 101% accurate.`,
|
||||||
|
})
|
||||||
|
export default class PPCommand extends CustomCommand {
|
||||||
|
usage = `> {$} [user]*
|
||||||
|
|
||||||
|
e.g.
|
||||||
|
Measure yourself:
|
||||||
|
> {$}
|
||||||
|
|
||||||
|
Measure someone else's pp:
|
||||||
|
> {$} @someone
|
||||||
|
|
||||||
|
Measure multiple people's pp:
|
||||||
|
> {$} @someone @sometwo ...
|
||||||
|
`
|
||||||
|
|
||||||
|
async messageRun(message: Message, args: Args) {
|
||||||
|
let membersRaw: string[] = await args.repeat("string").catch(() => [])
|
||||||
|
|
||||||
|
// handle 0 argument case
|
||||||
|
if (membersRaw.length <= 0) {
|
||||||
|
if (!message.member) {
|
||||||
|
sendEmbeddedMessage(message.channel, {
|
||||||
|
title: "Error",
|
||||||
|
description: "Failed to get user",
|
||||||
|
})
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
membersRaw = [message.author.id]
|
||||||
|
}
|
||||||
|
|
||||||
|
const users = PPCommand.calculatePPLengths(membersRaw)
|
||||||
|
sendEmbeddedMessage(message.channel, {
|
||||||
|
title: "pp",
|
||||||
|
description: PPCommand.buildPPList(users),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculates pp lengths for a list of people.
|
||||||
|
*
|
||||||
|
* @param membersRaw - A list of discord snowflakes to convert to pp length.
|
||||||
|
* @returns A sorted array of users from lowest to highest.
|
||||||
|
*/
|
||||||
|
static calculatePPLengths(membersRaw: string[]): PPUser[] {
|
||||||
|
const users: PPUser[] = []
|
||||||
|
|
||||||
|
for (const memberRaw of membersRaw) {
|
||||||
|
const numbersInString = memberRaw.match(/\d+/)
|
||||||
|
if (!numbersInString) continue
|
||||||
|
const memberIDStr = numbersInString[0]
|
||||||
|
if (!memberIDStr) continue
|
||||||
|
const memberID = parseInt(memberIDStr)
|
||||||
|
if (!memberID) continue
|
||||||
|
|
||||||
|
try {
|
||||||
|
users.push({
|
||||||
|
id: memberIDStr,
|
||||||
|
length: memberID % 31 /* Calculation happens here */,
|
||||||
|
})
|
||||||
|
} catch (e) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// sort users ascending by pp length
|
||||||
|
users.sort((prev, curr) => curr.length - prev.length)
|
||||||
|
|
||||||
|
return users
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Builds the final text that will be shown to the user.
|
||||||
|
*
|
||||||
|
* @param users - A list of user and their pp size.
|
||||||
|
*/
|
||||||
|
static buildPPList(users: PPUser[]): string {
|
||||||
|
let ppList = ""
|
||||||
|
|
||||||
|
for (const user of users) {
|
||||||
|
const userMention = Formatters.userMention(user.id)
|
||||||
|
|
||||||
|
ppList += `${userMention}:\n`
|
||||||
|
ppList += `8${"=".repeat(user.length)}D **(${user.length})**\n`
|
||||||
|
}
|
||||||
|
|
||||||
|
return ppList
|
||||||
|
}
|
||||||
|
}
|
15
src/commands/fun/tests/ClapifyCommand.test.ts
Normal file
15
src/commands/fun/tests/ClapifyCommand.test.ts
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
import ClapifyCommand from "../clapify"
|
||||||
|
|
||||||
|
test("Correctly adds clap between the words", () => {
|
||||||
|
expect(
|
||||||
|
ClapifyCommand.clapify([
|
||||||
|
"<@123456789012345678>",
|
||||||
|
"<#123456789012345678>",
|
||||||
|
"<@&123456789012345678>",
|
||||||
|
"word",
|
||||||
|
"1234",
|
||||||
|
])
|
||||||
|
).toStrictEqual(
|
||||||
|
"<@123456789012345678>👏<#123456789012345678>👏<@&123456789012345678>👏word👏1234"
|
||||||
|
)
|
||||||
|
})
|
27
src/commands/fun/tests/PPCommand.test.ts
Normal file
27
src/commands/fun/tests/PPCommand.test.ts
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
import PPCommand from "../pp"
|
||||||
|
|
||||||
|
test("Correctly builds pp list", () => {
|
||||||
|
const ppList = PPCommand.calculatePPLengths([
|
||||||
|
"<@123456789012345678>",
|
||||||
|
"<@111111111111111111>",
|
||||||
|
"<@333333333333333333>",
|
||||||
|
"<@444444444444444444>",
|
||||||
|
])
|
||||||
|
|
||||||
|
expect(ppList).toStrictEqual([
|
||||||
|
{ id: "123456789012345678", length: 24 },
|
||||||
|
{ id: "444444444444444444", length: 13 },
|
||||||
|
{ id: "111111111111111111", length: 11 },
|
||||||
|
{ id: "333333333333333333", length: 2 },
|
||||||
|
])
|
||||||
|
|
||||||
|
expect(PPCommand.buildPPList(ppList)).toStrictEqual(`<@123456789012345678>:
|
||||||
|
8========================D **(24)**
|
||||||
|
<@444444444444444444>:
|
||||||
|
8=============D **(13)**
|
||||||
|
<@111111111111111111>:
|
||||||
|
8===========D **(11)**
|
||||||
|
<@333333333333333333>:
|
||||||
|
8==D **(2)**
|
||||||
|
`)
|
||||||
|
})
|
76
src/commands/util/about.ts
Normal file
76
src/commands/util/about.ts
Normal file
|
@ -0,0 +1,76 @@
|
||||||
|
import { SnowflakeUtil, MessageEmbed } from "discord.js"
|
||||||
|
import { ApplyOptions } from "@sapphire/decorators"
|
||||||
|
|
||||||
|
import type { Message } from "discord.js"
|
||||||
|
import type { CommandOptions } from "@sapphire/framework"
|
||||||
|
|
||||||
|
import { countDays, formatDate, formatTimeDiff } from "../../util"
|
||||||
|
import { globalObject } from "../.."
|
||||||
|
import CustomCommand from "../../custom/CustomCommand"
|
||||||
|
|
||||||
|
@ApplyOptions<CommandOptions>({
|
||||||
|
aliases: ["a", "u", "uptime"],
|
||||||
|
description: "Shows basic information about the bot.",
|
||||||
|
})
|
||||||
|
export default class AboutCommand extends CustomCommand {
|
||||||
|
usage = "> {$}"
|
||||||
|
|
||||||
|
async messageRun(message: Message) {
|
||||||
|
const now = message.editedTimestamp || message.createdTimestamp
|
||||||
|
const startTime = globalObject.startTime
|
||||||
|
|
||||||
|
if (!startTime || !this.container.client.id)
|
||||||
|
return this.failedToGetStartTime(message)
|
||||||
|
|
||||||
|
const formattedUptime = formatTimeDiff(startTime, now)
|
||||||
|
const botCreatedTime = SnowflakeUtil.deconstruct(this.container.client.id)
|
||||||
|
const formattedBotCreationDate = formatDate(botCreatedTime.date)
|
||||||
|
const botAgeInDays = countDays(botCreatedTime.date.getTime(), now)
|
||||||
|
|
||||||
|
const serversCount = this.container.client.guilds.cache.size || 0
|
||||||
|
|
||||||
|
message.channel.send({
|
||||||
|
embeds: [
|
||||||
|
new MessageEmbed({
|
||||||
|
title: "About",
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: "Creation date in UTC",
|
||||||
|
value: `${formattedBotCreationDate} (${botAgeInDays} days ago)`,
|
||||||
|
inline: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Uptime",
|
||||||
|
value: formattedUptime,
|
||||||
|
inline: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Servers",
|
||||||
|
value: `The bot is in ${serversCount} server${
|
||||||
|
serversCount > 1 ? "s" : ""
|
||||||
|
}.`,
|
||||||
|
inline: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Source Code",
|
||||||
|
value: "https://github.com/llama-bot",
|
||||||
|
inline: false,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
failedToGetStartTime(message: Message): void {
|
||||||
|
message.channel.send({
|
||||||
|
embeds: [
|
||||||
|
new MessageEmbed({
|
||||||
|
title: "Failed to run command",
|
||||||
|
description:
|
||||||
|
"The bot failed to get one or more information about the bot.",
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
26
src/commands/util/invite.ts
Normal file
26
src/commands/util/invite.ts
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
import { CommandOptions } from "@sapphire/framework"
|
||||||
|
import { ApplyOptions } from "@sapphire/decorators"
|
||||||
|
import { MessageEmbed } from "discord.js"
|
||||||
|
|
||||||
|
import type { Message } from "discord.js"
|
||||||
|
|
||||||
|
import CustomCommand from "../../custom/CustomCommand"
|
||||||
|
|
||||||
|
@ApplyOptions<CommandOptions>({
|
||||||
|
description: "Shows information about inviting the bot.",
|
||||||
|
})
|
||||||
|
export default class InviteCommand extends CustomCommand {
|
||||||
|
usage = "> {$}"
|
||||||
|
|
||||||
|
async messageRun(message: Message) {
|
||||||
|
message.channel.send({
|
||||||
|
embeds: [
|
||||||
|
new MessageEmbed({
|
||||||
|
title: "Sorry",
|
||||||
|
description: `Sorry, but only the owner can invite the bot.
|
||||||
|
Check the [documentation](https://docs.llama.developomp.com/docs/overview#can-i-use-this-bot-in-my-discord-server) for more information.`,
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
61
src/commands/util/ping.ts
Normal file
61
src/commands/util/ping.ts
Normal file
|
@ -0,0 +1,61 @@
|
||||||
|
import { MessageEmbed } from "discord.js"
|
||||||
|
import { ApplyOptions } from "@sapphire/decorators"
|
||||||
|
|
||||||
|
import type { Message } from "discord.js"
|
||||||
|
import type { CommandOptions } from "@sapphire/framework"
|
||||||
|
|
||||||
|
import CustomCommand from "../../custom/CustomCommand"
|
||||||
|
|
||||||
|
@ApplyOptions<CommandOptions>({
|
||||||
|
aliases: ["p", "pong", "latency"],
|
||||||
|
description:
|
||||||
|
"Measures communication delay (latency) in 1/1000 of a second, also known as millisecond (ms).",
|
||||||
|
})
|
||||||
|
export default class PingCommand extends CustomCommand {
|
||||||
|
usage = "> {$}"
|
||||||
|
|
||||||
|
async messageRun(message: Message) {
|
||||||
|
const response = await message.channel.send({
|
||||||
|
embeds: [
|
||||||
|
new MessageEmbed({
|
||||||
|
description: `Called by ${message.author}`,
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: "API Latency",
|
||||||
|
value: "...",
|
||||||
|
inline: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Bot latency",
|
||||||
|
value: "...",
|
||||||
|
inline: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
})
|
||||||
|
|
||||||
|
response.edit({
|
||||||
|
embeds: [
|
||||||
|
new MessageEmbed({
|
||||||
|
description: `Called by ${message.author}`,
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: "API latency",
|
||||||
|
value: `${
|
||||||
|
(response.editedTimestamp || response.createdTimestamp) -
|
||||||
|
(message.editedTimestamp || message.createdTimestamp)
|
||||||
|
}ms`,
|
||||||
|
inline: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Bot latency",
|
||||||
|
value: `${Math.round(this.container.client.ws.ping)}ms`,
|
||||||
|
inline: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
230
src/commands/util/serverInfo.ts
Normal file
230
src/commands/util/serverInfo.ts
Normal file
|
@ -0,0 +1,230 @@
|
||||||
|
import { Formatters, MessageEmbed, SnowflakeUtil } from "discord.js"
|
||||||
|
import { ApplyOptions } from "@sapphire/decorators"
|
||||||
|
|
||||||
|
import type { Guild, Message } from "discord.js"
|
||||||
|
import type { CommandOptions } from "@sapphire/framework"
|
||||||
|
|
||||||
|
import {
|
||||||
|
formatDate,
|
||||||
|
formatTimeDiff,
|
||||||
|
highlightIndex,
|
||||||
|
formatNumber,
|
||||||
|
} from "../../util"
|
||||||
|
import CustomCommand from "../../custom/CustomCommand"
|
||||||
|
|
||||||
|
interface Data {
|
||||||
|
// general info
|
||||||
|
guildName: string
|
||||||
|
guildIconUrl: string
|
||||||
|
guildOwner: string
|
||||||
|
membersCount: number
|
||||||
|
maxMembers: number | null
|
||||||
|
communityStatus: string
|
||||||
|
partneredStatus: string
|
||||||
|
verifiedStatus: string
|
||||||
|
ScanningStatus: string
|
||||||
|
verificationStatus: string
|
||||||
|
nitroBoosts: number
|
||||||
|
boostLevel: number
|
||||||
|
|
||||||
|
// creation date
|
||||||
|
guildCreationDate: string
|
||||||
|
guildAge: string
|
||||||
|
|
||||||
|
// channels
|
||||||
|
totalChannelCount: number
|
||||||
|
textChannelCount: number
|
||||||
|
voiceChannelCount: number
|
||||||
|
announcementChannelCount: number
|
||||||
|
stageChannelCount: number
|
||||||
|
storeChannelCount: number
|
||||||
|
}
|
||||||
|
|
||||||
|
@ApplyOptions<CommandOptions>({
|
||||||
|
aliases: ["si"],
|
||||||
|
description: "Show information about the server",
|
||||||
|
})
|
||||||
|
export default class ServerInfoCommand extends CustomCommand {
|
||||||
|
usage = "> {$}"
|
||||||
|
|
||||||
|
// value of VerificationLevels from "discord.js/typings/enums"
|
||||||
|
// DO NOT CHANGE!!
|
||||||
|
verificationLevels = ["NONE", "LOW", "MEDIUM", "HIGH", "VERY_HIGH"]
|
||||||
|
|
||||||
|
// value of ExplicitContentFilterLevels from "discord.js/typings/enums"
|
||||||
|
// DO NOT CHANGE!!
|
||||||
|
explicitContentFilterLevels = [
|
||||||
|
"DISABLED",
|
||||||
|
"MEMBERS_WITHOUT_ROLES",
|
||||||
|
"ALL_MEMBERS",
|
||||||
|
]
|
||||||
|
|
||||||
|
// value of PremiumTiers from "discord.js/typings/enums"
|
||||||
|
// DO NOT CHANGE!!
|
||||||
|
premiumTiers = ["NONE", "TIER_1", "TIER_2", "TIER_3"]
|
||||||
|
|
||||||
|
async messageRun(message: Message) {
|
||||||
|
message.channel.sendTyping()
|
||||||
|
|
||||||
|
if (!message.guild) {
|
||||||
|
message.channel.send({
|
||||||
|
embeds: [
|
||||||
|
new MessageEmbed({
|
||||||
|
title: "Error!",
|
||||||
|
description: "This command only works in servers",
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
})
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
this.reply(message, message.guild)
|
||||||
|
}
|
||||||
|
|
||||||
|
async getData(message: Message, guild: Guild): Promise<Data> {
|
||||||
|
//
|
||||||
|
// general info
|
||||||
|
//
|
||||||
|
|
||||||
|
const guildName = guild.name
|
||||||
|
const guildIconUrl = guild.iconURL() || ""
|
||||||
|
const guildOwner = Formatters.userMention(guild.ownerId)
|
||||||
|
|
||||||
|
const membersCount = guild.memberCount
|
||||||
|
const maxMembers = guild.maximumMembers
|
||||||
|
|
||||||
|
const communityStatus = highlightIndex(
|
||||||
|
["Yes", "No"],
|
||||||
|
guild.features.includes("COMMUNITY") ? 0 : 1
|
||||||
|
)
|
||||||
|
const partneredStatus = highlightIndex(
|
||||||
|
["Yes", "No"],
|
||||||
|
guild.features.includes("PARTNERED") ? 0 : 1
|
||||||
|
)
|
||||||
|
const verifiedStatus = highlightIndex(
|
||||||
|
["Yes", "No"],
|
||||||
|
guild.features.includes("VERIFIED") ? 0 : 1
|
||||||
|
)
|
||||||
|
|
||||||
|
const ScanningStatus = highlightIndex(
|
||||||
|
this.explicitContentFilterLevels,
|
||||||
|
this.explicitContentFilterLevels.indexOf(guild.explicitContentFilter)
|
||||||
|
)
|
||||||
|
const verificationStatus = highlightIndex(
|
||||||
|
this.verificationLevels,
|
||||||
|
this.verificationLevels.indexOf(guild.verificationLevel)
|
||||||
|
)
|
||||||
|
|
||||||
|
const nitroBoosts = guild.premiumSubscriptionCount || 0
|
||||||
|
const boostLevel = this.premiumTiers.indexOf(guild.premiumTier)
|
||||||
|
|
||||||
|
//
|
||||||
|
// creation date
|
||||||
|
//
|
||||||
|
|
||||||
|
const serverCreatedTime = SnowflakeUtil.deconstruct(guild.id)
|
||||||
|
const guildCreationDate = formatDate(serverCreatedTime.date)
|
||||||
|
const guildAge = formatTimeDiff(
|
||||||
|
serverCreatedTime.date.getTime(),
|
||||||
|
message.editedTimestamp || message.createdTimestamp
|
||||||
|
)
|
||||||
|
|
||||||
|
//
|
||||||
|
// channels
|
||||||
|
//
|
||||||
|
|
||||||
|
const channels = await guild.channels.fetch()
|
||||||
|
|
||||||
|
const totalChannelCount = channels.size
|
||||||
|
let textChannelCount = 0
|
||||||
|
let voiceChannelCount = 0
|
||||||
|
let announcementChannelCount = 0
|
||||||
|
let stageChannelCount = 0
|
||||||
|
let storeChannelCount = 0
|
||||||
|
|
||||||
|
for (const [, channel] of channels) {
|
||||||
|
if (channel.type === "GUILD_TEXT") textChannelCount += 1
|
||||||
|
if (channel.type === "GUILD_VOICE") voiceChannelCount += 1
|
||||||
|
if (channel.type === "GUILD_NEWS") announcementChannelCount += 1
|
||||||
|
if (channel.type === "GUILD_STAGE_VOICE") stageChannelCount += 1
|
||||||
|
if (channel.type === "GUILD_STORE") storeChannelCount += 1
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
// general info
|
||||||
|
guildName,
|
||||||
|
guildIconUrl,
|
||||||
|
guildOwner,
|
||||||
|
membersCount,
|
||||||
|
maxMembers,
|
||||||
|
communityStatus,
|
||||||
|
partneredStatus,
|
||||||
|
verifiedStatus,
|
||||||
|
ScanningStatus,
|
||||||
|
verificationStatus,
|
||||||
|
nitroBoosts,
|
||||||
|
boostLevel,
|
||||||
|
|
||||||
|
// creation date
|
||||||
|
guildCreationDate,
|
||||||
|
guildAge,
|
||||||
|
|
||||||
|
// channels
|
||||||
|
totalChannelCount,
|
||||||
|
textChannelCount,
|
||||||
|
voiceChannelCount,
|
||||||
|
announcementChannelCount,
|
||||||
|
stageChannelCount,
|
||||||
|
storeChannelCount,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async reply(message: Message, guild: Guild): Promise<void> {
|
||||||
|
const data = await this.getData(message, guild)
|
||||||
|
|
||||||
|
message.channel.send({
|
||||||
|
embeds: [
|
||||||
|
new MessageEmbed({
|
||||||
|
title: data.guildName,
|
||||||
|
thumbnail: { url: data.guildIconUrl },
|
||||||
|
|
||||||
|
description: `Owner: ${data.guildOwner}
|
||||||
|
|
||||||
|
Members: Capacity: ${formatNumber(data.membersCount)} / ${formatNumber(
|
||||||
|
data.maxMembers
|
||||||
|
)}
|
||||||
|
|
||||||
|
Community: ${data.communityStatus}
|
||||||
|
Partnered: ${data.partneredStatus}
|
||||||
|
Verified: ${data.verifiedStatus}
|
||||||
|
|
||||||
|
Scanning: ${data.ScanningStatus}
|
||||||
|
Verification: ${data.verificationStatus}
|
||||||
|
|
||||||
|
Nitro boosts: ${data.nitroBoosts} (Lv ${data.boostLevel})`,
|
||||||
|
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: "Creation date",
|
||||||
|
value: `Creation date in UTC (24h time): ${data.guildCreationDate}
|
||||||
|
|
||||||
|
${data.guildAge} ago`,
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
name: "Channels",
|
||||||
|
value: `Channels: ${data.totalChannelCount}
|
||||||
|
Text: ${data.textChannelCount}
|
||||||
|
Voice: ${data.voiceChannelCount}
|
||||||
|
Announcement: ${data.announcementChannelCount}
|
||||||
|
Stage: ${data.stageChannelCount}
|
||||||
|
Store: ${data.storeChannelCount}`,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
footer: { text: `Server ID: ${guild.id}` },
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
68
src/commands/util/snowflake.ts
Normal file
68
src/commands/util/snowflake.ts
Normal file
|
@ -0,0 +1,68 @@
|
||||||
|
import { SnowflakeUtil, MessageEmbed } from "discord.js"
|
||||||
|
import { ApplyOptions } from "@sapphire/decorators"
|
||||||
|
|
||||||
|
import type { Message } from "discord.js"
|
||||||
|
import type { Args, CommandOptions } from "@sapphire/framework"
|
||||||
|
|
||||||
|
import { countDays, formatDate, formatTimeDiff } from "../../util"
|
||||||
|
import CustomCommand from "../../custom/CustomCommand"
|
||||||
|
|
||||||
|
@ApplyOptions<CommandOptions>({
|
||||||
|
aliases: ["s"],
|
||||||
|
description: "Calculates when a discord ID (snowflake) was created.",
|
||||||
|
})
|
||||||
|
export default class SnowflakeCommand extends CustomCommand {
|
||||||
|
usage = "> {$} <discord snowflake>"
|
||||||
|
|
||||||
|
async messageRun(message: Message, args: Args) {
|
||||||
|
let input: string
|
||||||
|
|
||||||
|
try {
|
||||||
|
input = await args.pick("string")
|
||||||
|
} catch {
|
||||||
|
message.channel.send({
|
||||||
|
embeds: [
|
||||||
|
new MessageEmbed({
|
||||||
|
title: "Error",
|
||||||
|
description: "You did not pass any snowflake :(",
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
})
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const now = message.editedTimestamp || message.createdTimestamp
|
||||||
|
const creationDate = SnowflakeUtil.deconstruct(input).date
|
||||||
|
const dateDelta = countDays(creationDate.getTime(), now)
|
||||||
|
|
||||||
|
message.channel.send({
|
||||||
|
embeds: [
|
||||||
|
new MessageEmbed({
|
||||||
|
title: input,
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: "Creation Date (UTC)",
|
||||||
|
value: `${formatDate(creationDate)} (${dateDelta} days ago)`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Age",
|
||||||
|
value: formatTimeDiff(creationDate.getTime(), now),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
})
|
||||||
|
} catch {
|
||||||
|
message.channel.send({
|
||||||
|
embeds: [
|
||||||
|
new MessageEmbed({
|
||||||
|
title: "Error",
|
||||||
|
description: `Failed to parse snowflake \`${input}\``,
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
99
src/commands/util/userInfo.ts
Normal file
99
src/commands/util/userInfo.ts
Normal file
|
@ -0,0 +1,99 @@
|
||||||
|
// todo: nitro boost
|
||||||
|
|
||||||
|
import { Formatters, MessageEmbed } from "discord.js"
|
||||||
|
import { CommandOptions } from "@sapphire/framework"
|
||||||
|
import { ApplyOptions } from "@sapphire/decorators"
|
||||||
|
|
||||||
|
import type { GuildMember, Message } from "discord.js"
|
||||||
|
import type { Args } from "@sapphire/framework"
|
||||||
|
|
||||||
|
import { formatDate, formatTimeDiff } from "../../util"
|
||||||
|
import CustomCommand from "../../custom/CustomCommand"
|
||||||
|
|
||||||
|
@ApplyOptions<CommandOptions>({
|
||||||
|
aliases: ["ui"],
|
||||||
|
description: "Shows information about a user.",
|
||||||
|
})
|
||||||
|
export default class UserInfoCommand extends CustomCommand {
|
||||||
|
usage = `> {$} [user]
|
||||||
|
|
||||||
|
e.g.
|
||||||
|
Get your user info
|
||||||
|
> {$}
|
||||||
|
|
||||||
|
Get someone else's user info by mentioning
|
||||||
|
> {$} @someone
|
||||||
|
|
||||||
|
Get someone else's user info with user id
|
||||||
|
> {$} 123456789012345678
|
||||||
|
`
|
||||||
|
|
||||||
|
async messageRun(message: Message, args: Args) {
|
||||||
|
let user = await args.pick("user").catch(() => undefined)
|
||||||
|
|
||||||
|
if (!user) user = message.author
|
||||||
|
|
||||||
|
const avatarURL = user.avatarURL() || undefined
|
||||||
|
|
||||||
|
const resultEmbed = new MessageEmbed({
|
||||||
|
author: { name: user.tag, icon_url: avatarURL },
|
||||||
|
thumbnail: { url: avatarURL },
|
||||||
|
description: Formatters.userMention(user.id),
|
||||||
|
footer: { text: `USER ID: ${user.id}` },
|
||||||
|
})
|
||||||
|
|
||||||
|
resultEmbed.addField(
|
||||||
|
"Discord join date",
|
||||||
|
this.formattedJoinDate(user.createdAt)
|
||||||
|
)
|
||||||
|
|
||||||
|
if (message.guild) {
|
||||||
|
const member = await message.guild.members.fetch(user.id)
|
||||||
|
|
||||||
|
resultEmbed.addField(
|
||||||
|
"Server join date",
|
||||||
|
this.formattedJoinDate(member.joinedAt)
|
||||||
|
)
|
||||||
|
|
||||||
|
const roleMentions = this.getMemberRoles(member, message.guild.id)
|
||||||
|
resultEmbed.addField(
|
||||||
|
`Roles (${roleMentions.length})`,
|
||||||
|
roleMentions.join(" ")
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
message.channel.send({
|
||||||
|
embeds: [resultEmbed],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
formattedJoinDate(joinedAt: Date | null): string {
|
||||||
|
let result = "Unknown"
|
||||||
|
|
||||||
|
if (joinedAt) {
|
||||||
|
const xAgo = formatTimeDiff(joinedAt.getTime(), Date.now())
|
||||||
|
|
||||||
|
result = `${formatDate(joinedAt)}\n${xAgo} ago`
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
getMemberRoles(member: GuildMember, guildID: string): string[] {
|
||||||
|
return [...member.roles.cache.entries()] // returns a list if [role_id, Role]
|
||||||
|
.map((elem) => elem[0]) // only get role IDs
|
||||||
|
.filter((value) => value != guildID) // remove @everyone role
|
||||||
|
.map((id) => Formatters.roleMention(id)) // converts role ID to mention string
|
||||||
|
}
|
||||||
|
|
||||||
|
// converts "SOMETHING_LIKE_THIS" to "Something Like This"
|
||||||
|
// from https://stackoverflow.com/a/32589289/12979111
|
||||||
|
convertCase(input: string): string {
|
||||||
|
return input
|
||||||
|
.replace(/_/g, " ")
|
||||||
|
.toLowerCase()
|
||||||
|
.split(" ")
|
||||||
|
.map((word) => word.charAt(0).toUpperCase() + word.substring(1))
|
||||||
|
.join(" ")
|
||||||
|
}
|
||||||
|
}
|
14
src/custom/CustomCommand.ts
Normal file
14
src/custom/CustomCommand.ts
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
import { Command } from "@sapphire/framework"
|
||||||
|
|
||||||
|
import type { PieceContext } from "@sapphire/framework"
|
||||||
|
|
||||||
|
export default abstract class CustomCommand extends Command {
|
||||||
|
public constructor(context: PieceContext, options?: Command.Options) {
|
||||||
|
super(context, {
|
||||||
|
...options,
|
||||||
|
|
||||||
|
// default preconditions
|
||||||
|
preconditions: ["NoDM", ...(options?.preconditions || [])],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
78
src/index.ts
Normal file
78
src/index.ts
Normal file
|
@ -0,0 +1,78 @@
|
||||||
|
import { SapphireClient } from "@sapphire/framework"
|
||||||
|
import { start as startPrettyError } from "pretty-error"
|
||||||
|
import nekosClient from "nekos.life"
|
||||||
|
import dotenv from "dotenv"
|
||||||
|
import "./DB"
|
||||||
|
|
||||||
|
function initializeEnv() {
|
||||||
|
dotenv.config()
|
||||||
|
// do not start the bot if token is not found
|
||||||
|
if (!process.env.TOKEN) throw Error("Token not found!")
|
||||||
|
|
||||||
|
// set to default values if not defined already
|
||||||
|
process.env.TESTING ??= "false"
|
||||||
|
process.env.PREFIX_PROD ??= "-"
|
||||||
|
process.env.PREFIX_DEV ??= "b-"
|
||||||
|
|
||||||
|
process.env.PREFIX =
|
||||||
|
process.env.TESTING == "true"
|
||||||
|
? process.env.PREFIX_DEV
|
||||||
|
: process.env.PREFIX_PROD
|
||||||
|
}
|
||||||
|
|
||||||
|
function initialize() {
|
||||||
|
startPrettyError()
|
||||||
|
initializeEnv()
|
||||||
|
}
|
||||||
|
|
||||||
|
initialize()
|
||||||
|
|
||||||
|
export const globalObject = {
|
||||||
|
startTime: 0,
|
||||||
|
nekosClient: new nekosClient(),
|
||||||
|
}
|
||||||
|
|
||||||
|
const client = new SapphireClient({
|
||||||
|
caseInsensitiveCommands: true,
|
||||||
|
caseInsensitivePrefixes: true,
|
||||||
|
defaultPrefix: process.env.PREFIX,
|
||||||
|
partials: [
|
||||||
|
// necessary for DM events to work
|
||||||
|
// https://discordjs.guide/popular-topics/partials.html#enabling-partials
|
||||||
|
|
||||||
|
"CHANNEL",
|
||||||
|
|
||||||
|
// necessary for reaction detection to work
|
||||||
|
|
||||||
|
"MESSAGE",
|
||||||
|
"REACTION",
|
||||||
|
"USER",
|
||||||
|
],
|
||||||
|
intents: [
|
||||||
|
"DIRECT_MESSAGE_REACTIONS",
|
||||||
|
"DIRECT_MESSAGE_TYPING",
|
||||||
|
"DIRECT_MESSAGES",
|
||||||
|
|
||||||
|
"GUILD_MESSAGE_REACTIONS",
|
||||||
|
"GUILD_MESSAGE_TYPING",
|
||||||
|
"GUILD_MESSAGES",
|
||||||
|
],
|
||||||
|
defaultCooldown: {
|
||||||
|
delay: 1_000,
|
||||||
|
filteredUsers: process.env.OWNER_IDS.split(","),
|
||||||
|
limit: 5,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
//
|
||||||
|
// start the bot
|
||||||
|
//
|
||||||
|
|
||||||
|
try {
|
||||||
|
client.login(process.env.TOKEN)
|
||||||
|
} catch (err) {
|
||||||
|
console.log("The bot crashed :(")
|
||||||
|
console.error(err)
|
||||||
|
|
||||||
|
client.destroy()
|
||||||
|
}
|
37
src/listeners/commands/commandError.ts
Normal file
37
src/listeners/commands/commandError.ts
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
import { Listener } from "@sapphire/framework"
|
||||||
|
|
||||||
|
import type { CommandErrorPayload } from "@sapphire/framework"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets executed when the bot encounters an error.
|
||||||
|
*/
|
||||||
|
export class CommandError extends Listener {
|
||||||
|
run(_error: unknown, payload: CommandErrorPayload) {
|
||||||
|
this.logToConsole(payload)
|
||||||
|
this.logToChannels(payload)
|
||||||
|
this.logToFirebase(payload)
|
||||||
|
}
|
||||||
|
|
||||||
|
async logToConsole(payload: CommandErrorPayload): Promise<void> {
|
||||||
|
const message = payload.message
|
||||||
|
const author = message.author
|
||||||
|
|
||||||
|
console.error(`
|
||||||
|
===============[ ERROR ]===============
|
||||||
|
Author: ${author.id} (${author.tag})
|
||||||
|
URL: ${message.url}
|
||||||
|
Content: ${message.content}
|
||||||
|
=======================================
|
||||||
|
`)
|
||||||
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
async logToChannels(payload: CommandErrorPayload): Promise<void> {
|
||||||
|
//
|
||||||
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
async logToFirebase(payload: CommandErrorPayload): Promise<void> {
|
||||||
|
//
|
||||||
|
}
|
||||||
|
}
|
33
src/listeners/messages/messageCreate.ts
Normal file
33
src/listeners/messages/messageCreate.ts
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
import { Listener } from "@sapphire/framework"
|
||||||
|
|
||||||
|
import type { Message } from "discord.js"
|
||||||
|
|
||||||
|
import { sendEmbeddedMessage } from "../../util"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets executed when a message is sent
|
||||||
|
*/
|
||||||
|
export class MessageCreate extends Listener {
|
||||||
|
autoDeleteSeconds = 10
|
||||||
|
|
||||||
|
run(message: Message) {
|
||||||
|
if (message.channel.type === "DM") this.handleDMs(message)
|
||||||
|
}
|
||||||
|
|
||||||
|
async handleDMs(message: Message): Promise<void> {
|
||||||
|
// ignore messages from other bots (or even itself)
|
||||||
|
if (message.author.bot) return
|
||||||
|
|
||||||
|
const responseMessage = await sendEmbeddedMessage(message.channel, {
|
||||||
|
title: "❗ DM commands are not supported.",
|
||||||
|
description: `Add a broom (🧹) emoji to a messages sent by me to delete them.
|
||||||
|
This message will deleted in ${this.autoDeleteSeconds} seconds.`,
|
||||||
|
})
|
||||||
|
|
||||||
|
await new Promise((resolve) =>
|
||||||
|
setTimeout(resolve, this.autoDeleteSeconds * 1000)
|
||||||
|
)
|
||||||
|
|
||||||
|
responseMessage.delete()
|
||||||
|
}
|
||||||
|
}
|
31
src/listeners/messages/messageReactionAdd.ts
Normal file
31
src/listeners/messages/messageReactionAdd.ts
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
import { Listener } from "@sapphire/framework"
|
||||||
|
|
||||||
|
import type { MessageReaction, PartialMessageReaction } from "discord.js"
|
||||||
|
|
||||||
|
import { settings, fetchSettings } from "../../DB"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets executed when a reaction is added to a message
|
||||||
|
*/
|
||||||
|
export class MessageReactionAdd extends Listener {
|
||||||
|
run(reaction: MessageReaction | PartialMessageReaction) {
|
||||||
|
if (reaction.message.channel.type === "DM") this.handleDMs(reaction)
|
||||||
|
}
|
||||||
|
|
||||||
|
async handleDMs(
|
||||||
|
reaction: MessageReaction | PartialMessageReaction
|
||||||
|
): Promise<void> {
|
||||||
|
const message = await reaction.message.fetch()
|
||||||
|
|
||||||
|
if (
|
||||||
|
!reaction.emoji.name ||
|
||||||
|
!this.container.client.id ||
|
||||||
|
this.container.client.id !== message.author.id
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
if (!settings.clear_emojis) await fetchSettings()
|
||||||
|
|
||||||
|
if (settings.clear_emojis?.includes(reaction.emoji.name)) message.delete()
|
||||||
|
}
|
||||||
|
}
|
34
src/listeners/ready.ts
Normal file
34
src/listeners/ready.ts
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
import { ApplyOptions } from "@sapphire/decorators"
|
||||||
|
import { Listener } from "@sapphire/framework"
|
||||||
|
import { gray, yellow } from "colorette"
|
||||||
|
|
||||||
|
import type { ListenerOptions } from "@sapphire/framework"
|
||||||
|
|
||||||
|
import { globalObject } from ".."
|
||||||
|
|
||||||
|
const y = yellow
|
||||||
|
|
||||||
|
@ApplyOptions<ListenerOptions>({
|
||||||
|
once: true,
|
||||||
|
})
|
||||||
|
export class Ready extends Listener {
|
||||||
|
run() {
|
||||||
|
globalObject.startTime = Date.now()
|
||||||
|
|
||||||
|
this.printReady()
|
||||||
|
}
|
||||||
|
|
||||||
|
printReady(): void {
|
||||||
|
const botTag = this.container.client.user?.tag || "unknown bot tag"
|
||||||
|
const botID = this.container.client.user?.id || "unknown bot ID"
|
||||||
|
const botMode =
|
||||||
|
process.env.TESTING === "true" ? "DEVELOPMENT" : "PRODUCTION"
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
gray(`
|
||||||
|
${y(botTag)} (ID: ${y(botID)}) is Ready!
|
||||||
|
Mode: ${y(botMode)}
|
||||||
|
`)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
18
src/preconditions/AdminsOnly.ts
Normal file
18
src/preconditions/AdminsOnly.ts
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
import { Precondition } from "@sapphire/framework"
|
||||||
|
|
||||||
|
import type { Message } from "discord.js"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Only allow commands sent in non-DM channels
|
||||||
|
*/
|
||||||
|
export default class AdminsOnlyPrecondition extends Precondition {
|
||||||
|
async run(message: Message) {
|
||||||
|
if (!message.guild) return this.error()
|
||||||
|
|
||||||
|
const guild = await message.guild.fetch()
|
||||||
|
const member = await guild.members.fetch(message.author.id)
|
||||||
|
|
||||||
|
if (member.permissions.has("ADMINISTRATOR")) return this.ok()
|
||||||
|
return this.error()
|
||||||
|
}
|
||||||
|
}
|
14
src/preconditions/NoDM.ts
Normal file
14
src/preconditions/NoDM.ts
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
import { Precondition } from "@sapphire/framework"
|
||||||
|
|
||||||
|
import type { Message } from "discord.js"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Only allow commands sent in non-DM channels
|
||||||
|
*/
|
||||||
|
export default class NoDMPrecondition extends Precondition {
|
||||||
|
run(message: Message) {
|
||||||
|
if (message.channel.type === "DM") return this.error()
|
||||||
|
|
||||||
|
return this.ok()
|
||||||
|
}
|
||||||
|
}
|
31
src/preconditions/OwnersOnly.ts
Normal file
31
src/preconditions/OwnersOnly.ts
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
import { Precondition } from "@sapphire/framework"
|
||||||
|
|
||||||
|
import type { Snowflake } from "discord-api-types"
|
||||||
|
import type { Message } from "discord.js"
|
||||||
|
|
||||||
|
import { sendEmbeddedMessage } from "../util"
|
||||||
|
|
||||||
|
export default class OwnersOnlyPrecondition extends Precondition {
|
||||||
|
// IDs of users who can run owners only commands
|
||||||
|
owners: Snowflake[] = []
|
||||||
|
|
||||||
|
run(message: Message) {
|
||||||
|
if (this.owners.length <= 0) {
|
||||||
|
// convert comma separated string to array and remove empty values
|
||||||
|
// trailing comma and double comma can result in empty values
|
||||||
|
this.owners = process.env.OWNER_IDS.split(",").filter((elem) => elem)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.owners.includes(message.author.id)) {
|
||||||
|
return this.ok()
|
||||||
|
}
|
||||||
|
|
||||||
|
sendEmbeddedMessage(message.channel, {
|
||||||
|
title: "Permission Error!",
|
||||||
|
description: `Only the bot owners can use this command!
|
||||||
|
[message](${message.url})`,
|
||||||
|
})
|
||||||
|
|
||||||
|
return this.error()
|
||||||
|
}
|
||||||
|
}
|
27
src/types/bot.d.ts
vendored
Normal file
27
src/types/bot.d.ts
vendored
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
export type Snowflake = string
|
||||||
|
// structure: "<snowflake>/<snowflake>"
|
||||||
|
export type MessageSelector = string
|
||||||
|
|
||||||
|
export interface Settings {
|
||||||
|
// emoji names
|
||||||
|
clear_emojis?: string[]
|
||||||
|
// quotes to be used in the `llama` command
|
||||||
|
quotes?: string[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ServerData {
|
||||||
|
settings: {
|
||||||
|
// todo: enable/disable commands by default
|
||||||
|
// todo: enabled/disabled commands/categories
|
||||||
|
}
|
||||||
|
|
||||||
|
vars: {
|
||||||
|
channels: { [key: string]: Snowflake }
|
||||||
|
messages: { [key: string]: MessageSelector }
|
||||||
|
roles: { [key: string]: Snowflake }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Servers {
|
||||||
|
[key: Snowflake]: ServerData
|
||||||
|
}
|
15
src/types/process.d.ts
vendored
Normal file
15
src/types/process.d.ts
vendored
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
declare namespace NodeJS {
|
||||||
|
interface ProcessEnv {
|
||||||
|
[key: string]: string | undefined
|
||||||
|
|
||||||
|
// .env values
|
||||||
|
TOKEN: string
|
||||||
|
TESTING: string
|
||||||
|
PREFIX_PROD: string
|
||||||
|
PREFIX_DEV: string
|
||||||
|
OWNER_IDS: string // ID1,ID2,ID3,...
|
||||||
|
|
||||||
|
// default prefix currently being used
|
||||||
|
PREFIX: string
|
||||||
|
}
|
||||||
|
}
|
13
src/types/sapphire.d.ts
vendored
Normal file
13
src/types/sapphire.d.ts
vendored
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
import "@sapphire/framework"
|
||||||
|
|
||||||
|
declare module "@sapphire/framework" {
|
||||||
|
abstract class Command {
|
||||||
|
usage?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Preconditions {
|
||||||
|
AdminsOnly: never
|
||||||
|
OwnersOnly: never
|
||||||
|
NoDM: never
|
||||||
|
}
|
||||||
|
}
|
11
src/util/caseInsensitiveIndexOf.ts
Normal file
11
src/util/caseInsensitiveIndexOf.ts
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
/**
|
||||||
|
* Finds the index of {@link query} in {@link input}.
|
||||||
|
*
|
||||||
|
* - Returns -1 if {@link query} was not found in {@link input}.
|
||||||
|
*
|
||||||
|
* @param input - An array of string
|
||||||
|
* @param query - String to find
|
||||||
|
*/
|
||||||
|
export default function (input: string[], query: string): number {
|
||||||
|
return input.findIndex((elem) => query.toLowerCase() === elem.toLowerCase())
|
||||||
|
}
|
12
src/util/countDays.ts
Normal file
12
src/util/countDays.ts
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
/**
|
||||||
|
* Counts the number of days in between {@link startDate} and {@link endDate}.
|
||||||
|
*
|
||||||
|
* - Returns NaN if either of the arguments are NaN.
|
||||||
|
* - Result could be a negative number if the {@link startDate} is greater than {@link endDate}.
|
||||||
|
*
|
||||||
|
* @param startDate - Starting date in milliseconds
|
||||||
|
* @param endDate - Ending date in milliseconds
|
||||||
|
*/
|
||||||
|
export default function (startDate: number, endDate: number): number {
|
||||||
|
return Math.ceil((endDate - startDate) / (1000 * 60 * 60 * 24))
|
||||||
|
}
|
15
src/util/extractUrlsFromString.ts
Normal file
15
src/util/extractUrlsFromString.ts
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
/**
|
||||||
|
* From [URI.js](https://github.com/medialize/URI.js).
|
||||||
|
* More information can be found in [this stackOverflow answer](https://stackoverflow.com/a/11209098).
|
||||||
|
*/
|
||||||
|
const pattern =
|
||||||
|
/\b((?:[a-z][\w-]+:(?:\/{1,3}|[a-z0-9%])|www\d{0,3}[.]|[a-z0-9.-]+[.][a-z]{2,4}\/)(?:[^\s()<>]+|\(([^\s()<>]+|(\([^\s()<>]+\)))*\))+(?:\(([^\s()<>]+|(\([^\s()<>]+\)))*\)|[^\s`!()[\]{};:'".,<>?«»“”‘’]))/gi
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extract URLs from string
|
||||||
|
*
|
||||||
|
* @param input - Raw string to check
|
||||||
|
*/
|
||||||
|
export default function (input: string): string[] {
|
||||||
|
return input.match(pattern) || []
|
||||||
|
}
|
16
src/util/formatDate.ts
Normal file
16
src/util/formatDate.ts
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
/**
|
||||||
|
* Formats {@link date} to `YYYY-MM-DD hh:mm:ss`.
|
||||||
|
*
|
||||||
|
* @param date - Raw date object
|
||||||
|
*/
|
||||||
|
export default function (date: Date): string {
|
||||||
|
const YYYY = date.getFullYear()
|
||||||
|
const MM = date.getMonth() + 1 // starts from 0 for some reason
|
||||||
|
const DD = date.getDate()
|
||||||
|
|
||||||
|
const hh = date.getHours()
|
||||||
|
const mm = date.getMinutes()
|
||||||
|
const ss = date.getSeconds()
|
||||||
|
|
||||||
|
return `${YYYY}-${MM}-${DD} ${hh}:${mm}:${ss}`
|
||||||
|
}
|
12
src/util/formatNumber.ts
Normal file
12
src/util/formatNumber.ts
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
/**
|
||||||
|
* Add commas to a long, positive number. Does not add comma to negative numbers.
|
||||||
|
*
|
||||||
|
* @param num - raw number
|
||||||
|
*/
|
||||||
|
export default function (num: number | undefined | null): string {
|
||||||
|
if (num === undefined || num === null) return "None"
|
||||||
|
|
||||||
|
if (num <= 999) return String(num)
|
||||||
|
|
||||||
|
return String(num).replace(/(.)(?=(\d{3})+$)/g, "$1,")
|
||||||
|
}
|
43
src/util/formatTimeDiff.ts
Normal file
43
src/util/formatTimeDiff.ts
Normal file
|
@ -0,0 +1,43 @@
|
||||||
|
const SECONDS_IN_A_YEAR = 60 * 60 * 24 * 365
|
||||||
|
const SECONDS_IN_A_DAY = 60 * 60 * 24
|
||||||
|
const SECONDS_IN_A_HOUR = 60 * 60
|
||||||
|
const SECONDS_IN_A_MINUTE = 60
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Formats difference in time in a readable format.
|
||||||
|
*
|
||||||
|
* @param startTime - Start date in millisecond
|
||||||
|
* @param endTime - End date in millisecond
|
||||||
|
*/
|
||||||
|
export default function (startTime: number, endTime: number): string {
|
||||||
|
let result = ""
|
||||||
|
|
||||||
|
function addToResult(num: number, unit: string) {
|
||||||
|
if (num) result += ` ${num} ${unit}` + (num > 1 ? "s" : "")
|
||||||
|
}
|
||||||
|
|
||||||
|
let diffSec = (endTime - startTime) / 1000
|
||||||
|
|
||||||
|
// prevent empty response
|
||||||
|
if (diffSec === 0) return "0 second"
|
||||||
|
|
||||||
|
const years = Math.floor(diffSec / SECONDS_IN_A_YEAR)
|
||||||
|
diffSec -= years * SECONDS_IN_A_YEAR
|
||||||
|
addToResult(years, "year")
|
||||||
|
|
||||||
|
const days = Math.floor(diffSec / SECONDS_IN_A_DAY)
|
||||||
|
diffSec -= days * SECONDS_IN_A_DAY
|
||||||
|
addToResult(days, "day")
|
||||||
|
|
||||||
|
const hours = Math.floor(diffSec / SECONDS_IN_A_HOUR) % 24
|
||||||
|
diffSec -= hours * SECONDS_IN_A_HOUR
|
||||||
|
addToResult(hours, "hour")
|
||||||
|
|
||||||
|
const minutes = Math.floor(diffSec / SECONDS_IN_A_MINUTE) % 60
|
||||||
|
diffSec -= minutes * SECONDS_IN_A_MINUTE
|
||||||
|
addToResult(minutes, "minute")
|
||||||
|
|
||||||
|
addToResult(diffSec, "second")
|
||||||
|
|
||||||
|
return result.trim()
|
||||||
|
}
|
20
src/util/highlightIndex.ts
Normal file
20
src/util/highlightIndex.ts
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
/**
|
||||||
|
* Highlight a string in a list of entries using markdown syntax.
|
||||||
|
*
|
||||||
|
* - If {@link index} is -1 or is greater than the last index of {@link entries}, it will leave all entries un-highlighted.
|
||||||
|
* - If {@link entries} does not contain any element, the resulting string will be empty.
|
||||||
|
*
|
||||||
|
* @param index - Index of string to highlight
|
||||||
|
* @param entries - An array of strings that can be highlighted
|
||||||
|
* @param separator - What to put between the entries (defaults to " / ")
|
||||||
|
*/
|
||||||
|
export default function (
|
||||||
|
entries: string[],
|
||||||
|
index: number,
|
||||||
|
separator = " / "
|
||||||
|
): string {
|
||||||
|
entries = entries.map((elem, i) => {
|
||||||
|
return i === index ? `**${elem}**` : elem
|
||||||
|
})
|
||||||
|
return entries.join(separator)
|
||||||
|
}
|
9
src/util/index.ts
Normal file
9
src/util/index.ts
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
export { default as caseInsensitiveIndexOf } from "./caseInsensitiveIndexOf"
|
||||||
|
export { default as countDays } from "./countDays"
|
||||||
|
export { default as formatDate } from "./formatDate"
|
||||||
|
export { default as formatNumber } from "./formatNumber"
|
||||||
|
export { default as highlightIndex } from "./highlightIndex"
|
||||||
|
export { default as isChannelInMessageNSFW } from "./isChannelInMessageNSFW"
|
||||||
|
export { default as formatTimeDiff } from "./formatTimeDiff"
|
||||||
|
export { default as sendEmbeddedMessage } from "./sendEmbeddedMessage"
|
||||||
|
export { default as extractUrlsFromString } from "./extractUrlsFromString"
|
10
src/util/isChannelInMessageNSFW.ts
Normal file
10
src/util/isChannelInMessageNSFW.ts
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
import type { Message } from "discord.js"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if channel where the message sent is a NSFW channel.
|
||||||
|
*
|
||||||
|
* @param message - Message to get the channel
|
||||||
|
*/
|
||||||
|
export default function (message: Message): boolean {
|
||||||
|
return Reflect.get(message.channel, "nsfw") === true
|
||||||
|
}
|
23
src/util/sendEmbeddedMessage.ts
Normal file
23
src/util/sendEmbeddedMessage.ts
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
import { MessageEmbed } from "discord.js"
|
||||||
|
|
||||||
|
import type {
|
||||||
|
BaseGuildTextChannel,
|
||||||
|
Message,
|
||||||
|
MessageEmbedOptions,
|
||||||
|
TextBasedChannel,
|
||||||
|
} from "discord.js"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sends message with one embed.
|
||||||
|
*
|
||||||
|
* @param channel - Text channel to send the embedded message
|
||||||
|
* @param data - Message content to send
|
||||||
|
*/
|
||||||
|
export default function (
|
||||||
|
channel: BaseGuildTextChannel | TextBasedChannel,
|
||||||
|
data: MessageEmbed | MessageEmbedOptions
|
||||||
|
): Promise<Message<boolean>> {
|
||||||
|
return channel.send({
|
||||||
|
embeds: [new MessageEmbed(data)],
|
||||||
|
})
|
||||||
|
}
|
17
src/util/tests/caseInsensitiveIndexOf.test.ts
Normal file
17
src/util/tests/caseInsensitiveIndexOf.test.ts
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
import caseInsensitiveIndexOf from "../caseInsensitiveIndexOf"
|
||||||
|
|
||||||
|
test("Correctly identifies index", () => {
|
||||||
|
const array = ["A", "B", "C", "D"]
|
||||||
|
|
||||||
|
array.map((entry, index) => {
|
||||||
|
expect(caseInsensitiveIndexOf(array, entry.toLowerCase())).toStrictEqual(
|
||||||
|
index
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(
|
||||||
|
caseInsensitiveIndexOf(array, "this does not exist in the array")
|
||||||
|
).toStrictEqual(-1)
|
||||||
|
|
||||||
|
expect(caseInsensitiveIndexOf([], "testing empty array")).toStrictEqual(-1)
|
||||||
|
})
|
24
src/util/tests/countDays.test.ts
Normal file
24
src/util/tests/countDays.test.ts
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
import countDays from "../countDays"
|
||||||
|
|
||||||
|
test("Accurately counts days between dates", () => {
|
||||||
|
expect(
|
||||||
|
countDays(
|
||||||
|
new Date("2022/02/22").getTime(),
|
||||||
|
new Date("2022/02/23").getTime()
|
||||||
|
)
|
||||||
|
).toStrictEqual(1)
|
||||||
|
|
||||||
|
expect(
|
||||||
|
countDays(
|
||||||
|
new Date("2022/02/01").getTime(),
|
||||||
|
new Date("2022/03/01").getTime()
|
||||||
|
)
|
||||||
|
).toStrictEqual(28)
|
||||||
|
|
||||||
|
expect(
|
||||||
|
countDays(
|
||||||
|
new Date("this string").getTime(), // NaN
|
||||||
|
new Date("is not a valid date").getTime() // NaN
|
||||||
|
)
|
||||||
|
).toStrictEqual(NaN)
|
||||||
|
})
|
11
src/util/tests/formatDate.test.ts
Normal file
11
src/util/tests/formatDate.test.ts
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
import formatDate from "../formatDate"
|
||||||
|
|
||||||
|
test("Properly formats date", () => {
|
||||||
|
expect(formatDate(new Date("Feb 22, 2022 22:22:22"))).toStrictEqual(
|
||||||
|
"2022-2-22 22:22:22"
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(formatDate(new Date("2022/02/22 22:22:22"))).toStrictEqual(
|
||||||
|
"2022-2-22 22:22:22"
|
||||||
|
)
|
||||||
|
})
|
10
src/util/tests/formatNumber.test.ts
Normal file
10
src/util/tests/formatNumber.test.ts
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
import formatNumber from "../formatNumber"
|
||||||
|
|
||||||
|
test("Correctly formats numbers", () => {
|
||||||
|
expect(formatNumber(1_000)).toStrictEqual("1,000")
|
||||||
|
expect(formatNumber(999_999_999)).toStrictEqual("999,999,999")
|
||||||
|
expect(formatNumber(0)).toStrictEqual("0")
|
||||||
|
expect(formatNumber(-0)).toStrictEqual("0")
|
||||||
|
expect(formatNumber(-1_000)).toStrictEqual("-1000")
|
||||||
|
expect(formatNumber(-999_999_999)).toStrictEqual("-999999999")
|
||||||
|
})
|
24
src/util/tests/formatTimeDiff.test.ts
Normal file
24
src/util/tests/formatTimeDiff.test.ts
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
import formatTimeDiff from "../formatTimeDiff"
|
||||||
|
|
||||||
|
test("Correctly formats time", () => {
|
||||||
|
expect(
|
||||||
|
formatTimeDiff(
|
||||||
|
new Date("2022/02/22").getTime(),
|
||||||
|
new Date("2023/02/23 01:01:01").getTime()
|
||||||
|
)
|
||||||
|
).toStrictEqual("1 year 1 day 1 hour 1 minute 1 second")
|
||||||
|
|
||||||
|
expect(
|
||||||
|
formatTimeDiff(
|
||||||
|
new Date("2022/02/22").getTime(),
|
||||||
|
new Date("2024/02/24 02:02:02").getTime()
|
||||||
|
)
|
||||||
|
).toStrictEqual("2 years 2 days 2 hours 2 minutes 2 seconds")
|
||||||
|
|
||||||
|
expect(
|
||||||
|
formatTimeDiff(
|
||||||
|
new Date("2022/02/22").getTime(),
|
||||||
|
new Date("2022/02/22").getTime()
|
||||||
|
)
|
||||||
|
).toStrictEqual("0 second")
|
||||||
|
})
|
13
src/util/tests/highlightIndex.test.ts
Normal file
13
src/util/tests/highlightIndex.test.ts
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
import highlightIndex from "../highlightIndex"
|
||||||
|
|
||||||
|
test("Correctly highlights entry", () => {
|
||||||
|
const array = ["A", "B", "C"]
|
||||||
|
|
||||||
|
expect(highlightIndex(array, -1)).toStrictEqual("A / B / C")
|
||||||
|
expect(highlightIndex(array, 0)).toStrictEqual("**A** / B / C")
|
||||||
|
expect(highlightIndex(array, 1)).toStrictEqual("A / **B** / C")
|
||||||
|
expect(highlightIndex(array, 2)).toStrictEqual("A / B / **C**")
|
||||||
|
expect(highlightIndex(array, 3)).toStrictEqual("A / B / C")
|
||||||
|
|
||||||
|
expect(highlightIndex([], 0)).toStrictEqual("")
|
||||||
|
})
|
23
tsconfig.json
Normal file
23
tsconfig.json
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"outDir": "dist",
|
||||||
|
"rootDir": "src",
|
||||||
|
|
||||||
|
"moduleResolution": "Node",
|
||||||
|
"target": "ES6",
|
||||||
|
"module": "CommonJS",
|
||||||
|
"lib": ["ES6"],
|
||||||
|
|
||||||
|
"removeComments": true,
|
||||||
|
"allowSyntheticDefaultImports": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"experimentalDecorators": true,
|
||||||
|
"noFallthroughCasesInSwitch": true,
|
||||||
|
"noImplicitAny": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"strict": true
|
||||||
|
},
|
||||||
|
"include": ["src"],
|
||||||
|
"exclude": ["_/", "**/*test.ts"]
|
||||||
|
}
|
Loading…
Add table
Add a link
Reference in a new issue