initial update

This commit is contained in:
murm 2023-03-15 10:09:09 -04:00
parent 3272429cf6
commit db9b70bf66
280 changed files with 11772 additions and 11966 deletions

View File

@ -1,4 +1,4 @@
**/node_modules **/node_modules
**/build **/build
**/logs **/logs
**/data **/data

View File

@ -1,3 +1,3 @@
[*.js] [*.js]
indent_style = space indent_style = space
indent_size = 2 indent_size = 2

View File

@ -1,54 +1,54 @@
############ ############
# Required # # Required #
############ ############
# Put environment type here (development, staging, or production) # Put environment type here (development, staging, or production)
NODE_ENV=development NODE_ENV=development
# Put Discord bot token here, you can get it from https://discord.com/developers/applications # Put Discord bot token here, you can get it from https://discord.com/developers/applications
TOKEN= TOKEN=
# Put the database connection URL here # Put the database connection URL here
# Example for SQLite: # Example for SQLite:
DB=sqlite://data.sqlite DB=sqlite://data.sqlite
# Example for PostgreSQL: # Example for PostgreSQL:
# DB=postgresql://esmbot:verycoolpass100@localhost:5432/esmbot # DB=postgresql://esmbot:verycoolpass100@localhost:5432/esmbot
# Put snowflake ID of bot owner here (obtainable by going into Discord settings -> Appearance and enabling Developer Mode, # Put snowflake ID of bot owner here (obtainable by going into Discord settings -> Appearance and enabling Developer Mode,
# then right clicking on your profile picture and selecting Copy ID) # then right clicking on your profile picture and selecting Copy ID)
OWNER= OWNER=
# Put default classic command prefix here # Put default classic command prefix here
PREFIX=& PREFIX=&
############ ############
# Optional # # Optional #
############ ############
# Set this to true if you want the bot to stay in voice chats after sound effects and music have stopped # Set this to true if you want the bot to stay in voice chats after sound effects and music have stopped
# (you can still make the bot leave using the stop command) # (you can still make the bot leave using the stop command)
STAYVC=false STAYVC=false
# Set this to true to disable music playback from YouTube # Set this to true to disable music playback from YouTube
YT_DISABLED=false YT_DISABLED=false
# Put Tenor API key here (used for obtaining raw tenor GIF urls) # Put Tenor API key here (used for obtaining raw tenor GIF urls)
TENOR= TENOR=
# Put HTML help page output location here, leave blank to disable # Put HTML help page output location here, leave blank to disable
OUTPUT= OUTPUT=
# Put temporary image dir here (make sure it's accessible via a web server), leave blank to disable # Put temporary image dir here (make sure it's accessible via a web server), leave blank to disable
TEMPDIR= TEMPDIR=
# Put temporary image web server domain # Put temporary image web server domain
TMP_DOMAIN= TMP_DOMAIN=
# Threshold where optional space saving methods will be performed # Threshold where optional space saving methods will be performed
THRESHOLD= THRESHOLD=
# Port for serving metrics. Metrics served are compatible with Prometheus. # Port for serving metrics. Metrics served are compatible with Prometheus.
METRICS= METRICS=
# The image API type to be used # The image API type to be used
# Set this to `none` to process all images locally # Set this to `none` to process all images locally
# Set this to `ws` if you want to use the external image API script, located in api/index.js # Set this to `ws` if you want to use the external image API script, located in api/index.js
API_TYPE=none API_TYPE=none
# Put ID of server to limit owner-only commands to # Put ID of server to limit owner-only commands to
ADMIN_SERVER= ADMIN_SERVER=

View File

@ -1,69 +1,69 @@
{ {
"env": { "env": {
"es2020": true, "es2020": true,
"node": true "node": true
}, },
"extends": ["eslint:recommended"], "extends": ["eslint:recommended"],
"parser": "@babel/eslint-parser", "parser": "@babel/eslint-parser",
"parserOptions": { "parserOptions": {
"sourceType": "module", "sourceType": "module",
"ecmaVersion": 12, "ecmaVersion": 12,
"requireConfigFile": false, "requireConfigFile": false,
"babelOptions": { "babelOptions": {
"plugins": [ "plugins": [
"@babel/plugin-proposal-class-properties" "@babel/plugin-proposal-class-properties"
] ]
} }
}, },
"plugins": ["@babel", "unicorn"], "plugins": ["@babel", "unicorn"],
"rules": { "rules": {
"no-console": "off", "no-console": "off",
"indent": [ "indent": [
"error", "error",
2, 2,
{ {
"SwitchCase": 1 "SwitchCase": 1
} }
], ],
"linebreak-style": [ "linebreak-style": [
"error", "error",
"unix" "unix"
], ],
"unicorn/prefer-module": "error", "unicorn/prefer-module": "error",
"quotes": [ "quotes": [
"warn", "warn",
"double" "double"
], ],
"semi": [ "semi": [
"warn", "warn",
"always" "always"
], ],
"keyword-spacing": [ "keyword-spacing": [
"error", { "error", {
"before": true, "before": true,
"after": true "after": true
} }
], ],
"space-before-blocks": [ "space-before-blocks": [
"error", { "error", {
"functions": "always", "functions": "always",
"keywords": "always", "keywords": "always",
"classes": "always" "classes": "always"
} }
], ],
"space-before-function-paren": [ "space-before-function-paren": [
"error", { "error", {
"anonymous": "never", "anonymous": "never",
"named": "never", "named": "never",
"asyncArrow": "always" "asyncArrow": "always"
} }
], ],
"prefer-const": [ "prefer-const": [
"error", { "error", {
"destructuring": "any", "destructuring": "any",
"ignoreReadBeforeAssign": false "ignoreReadBeforeAssign": false
} }
], ],
"prefer-template": "error" "prefer-template": "error"
} }
} }

2
.github/FUNDING.yml vendored
View File

@ -1,2 +1,2 @@
patreon: TheEssem patreon: TheEssem
ko_fi: TheEssem ko_fi: TheEssem

View File

@ -1,54 +1,54 @@
name: Bug Report name: Bug Report
description: Report an issue with the bot that didn't result in an error file description: Report an issue with the bot that didn't result in an error file
labels: [bug] labels: [bug]
body: body:
- type: textarea - type: textarea
id: describe id: describe
attributes: attributes:
label: Describe the bug label: Describe the bug
description: A clear and concise description of what the bug is. description: A clear and concise description of what the bug is.
validations: validations:
required: true required: true
- type: textarea - type: textarea
id: reproduce id: reproduce
attributes: attributes:
label: Steps to reproduce label: Steps to reproduce
description: Steps to reproduce the behavior. description: Steps to reproduce the behavior.
value: | value: |
1. Go to '...' 1. Go to '...'
2. Click on '....' 2. Click on '....'
3. Scroll down to '....' 3. Scroll down to '....'
4. See error 4. See error
validations: validations:
required: false required: false
- type: textarea - type: textarea
id: expected id: expected
attributes: attributes:
label: Expected behavior label: Expected behavior
description: A clear and concise description of what you expected to happen. description: A clear and concise description of what you expected to happen.
validations: validations:
required: false required: false
- type: textarea - type: textarea
id: screenshots id: screenshots
attributes: attributes:
label: Screenshots label: Screenshots
description: If applicable, add screenshots to help explain your problem. description: If applicable, add screenshots to help explain your problem.
validations: validations:
required: false required: false
- type: dropdown - type: dropdown
id: self-hosted id: self-hosted
attributes: attributes:
label: Self-hosted instance? label: Self-hosted instance?
description: Did the error occur on a self-hosted instance (e.g. not the main esmBot or esmBot Dev instances)? description: Did the error occur on a self-hosted instance (e.g. not the main esmBot or esmBot Dev instances)?
options: options:
- "Yes" - "Yes"
- "No" - "No"
validations: validations:
required: true required: true
- type: textarea - type: textarea
id: context id: context
attributes: attributes:
label: Additional context label: Additional context
description: Add any other context about the problem here. description: Add any other context about the problem here.
validations: validations:
required: false required: false

View File

@ -1,58 +1,58 @@
name: Error Report name: Error Report
description: Report an error that the bot posted in chat description: Report an error that the bot posted in chat
labels: [bug] labels: [bug]
body: body:
- type: input - type: input
id: command id: command
attributes: attributes:
label: Command that caused the error label: Command that caused the error
description: Post the exact command that caused the error. description: Post the exact command that caused the error.
validations: validations:
required: true required: true
- type: input - type: input
id: image id: image
attributes: attributes:
label: Image that caused the error label: Image that caused the error
description: If the error is regarding an image command, please post a direct link to the image here. description: If the error is regarding an image command, please post a direct link to the image here.
validations: validations:
required: false required: false
- type: textarea - type: textarea
id: reproduce id: reproduce
attributes: attributes:
label: Steps to reproduce label: Steps to reproduce
description: Steps to reproduce the behavior. description: Steps to reproduce the behavior.
value: | value: |
1. Go to '...' 1. Go to '...'
2. Click on '....' 2. Click on '....'
3. Scroll down to '....' 3. Scroll down to '....'
4. See error 4. See error
validations: validations:
required: false required: false
- type: dropdown - type: dropdown
id: self-hosted id: self-hosted
attributes: attributes:
label: Self-hosted instance? label: Self-hosted instance?
description: Did the error occur on a self-hosted instance (e.g. not the main esmBot or esmBot Dev instances)? description: Did the error occur on a self-hosted instance (e.g. not the main esmBot or esmBot Dev instances)?
options: options:
- "Yes" - "Yes"
- "No" - "No"
validations: validations:
required: true required: true
- type: textarea - type: textarea
id: error id: error
attributes: attributes:
label: Error file label: Error file
description: Post the contents of the error file in between the backticks. description: Post the contents of the error file in between the backticks.
value: | value: |
``` ```
``` ```
validations: validations:
required: true required: true
- type: textarea - type: textarea
id: context id: context
attributes: attributes:
label: Additional context label: Additional context
description: Add any other context about the problem here. description: Add any other context about the problem here.
validations: validations:
required: false required: false

View File

@ -1,25 +1,25 @@
name: Feature Request name: Feature Request
description: Suggest an idea for this project description: Suggest an idea for this project
labels: [enhancement] labels: [enhancement]
body: body:
- type: textarea - type: textarea
id: describe id: describe
attributes: attributes:
label: Describe the request label: Describe the request
description: What do you want to be added to the bot? description: What do you want to be added to the bot?
validations: validations:
required: true required: true
- type: textarea - type: textarea
id: problem id: problem
attributes: attributes:
label: Is your feature request related to a problem? Please describe. label: Is your feature request related to a problem? Please describe.
description: A clear and concise description of what the problem is (if applicable). Ex. I'm always frustrated when [...] description: A clear and concise description of what the problem is (if applicable). Ex. I'm always frustrated when [...]
validations: validations:
required: false required: false
- type: textarea - type: textarea
id: context id: context
attributes: attributes:
label: Additional context label: Additional context
description: Add any other context or screenshots about the feature request here. description: Add any other context or screenshots about the feature request here.
validations: validations:
required: false required: false

View File

@ -1,43 +1,43 @@
name: Build Test name: Build Test
on: on:
push: push:
branches: [ master ] branches: [ master ]
pull_request: pull_request:
branches: [ master ] branches: [ master ]
env: env:
BUILD_TYPE: Release BUILD_TYPE: Release
jobs: jobs:
linux: linux:
runs-on: ubuntu-22.04 runs-on: ubuntu-22.04
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v1 uses: actions/checkout@v1
- name: Setup pnpm - name: Setup pnpm
uses: pnpm/action-setup@v2.2.2 uses: pnpm/action-setup@v2.2.2
with: with:
version: 7 version: 7
- name: Setup Node.js environment - name: Setup Node.js environment
uses: actions/setup-node@v2.1.2 uses: actions/setup-node@v2.1.2
with: with:
node-version: 18 node-version: 18
cache: pnpm cache: pnpm
- name: Install dependencies - name: Install dependencies
run: sudo apt update && sudo apt install -y cmake libvips-dev libmagick++-dev run: sudo apt update && sudo apt install -y cmake libvips-dev libmagick++-dev
- name: Build - name: Build
run: pnpm install --frozen-lockfile && pnpm run build run: pnpm install --frozen-lockfile && pnpm run build
darwin: darwin:
runs-on: macos-latest runs-on: macos-latest
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v2 uses: actions/checkout@v2
- name: Install dependencies - name: Install dependencies
run: | run: |
brew install imagemagick vips node pnpm npm brew install imagemagick vips node pnpm npm
pnpm install --config.strict-peer-dependencies=false pnpm install --config.strict-peer-dependencies=false
- name: Build - name: Build
run: pnpm run build run: pnpm run build

View File

@ -1,67 +1,67 @@
# For most projects, this workflow file will not need changing; you simply need # For most projects, this workflow file will not need changing; you simply need
# to commit it to your repository. # to commit it to your repository.
# #
# You may wish to alter this file to override the set of languages analyzed, # You may wish to alter this file to override the set of languages analyzed,
# or to provide custom queries or build logic. # or to provide custom queries or build logic.
# #
# ******** NOTE ******** # ******** NOTE ********
# We have attempted to detect the languages in your repository. Please check # We have attempted to detect the languages in your repository. Please check
# the `language` matrix defined below to confirm you have the correct set of # the `language` matrix defined below to confirm you have the correct set of
# supported CodeQL languages. # supported CodeQL languages.
# #
name: "CodeQL" name: "CodeQL"
on: on:
push: push:
branches: [ master ] branches: [ master ]
pull_request: pull_request:
# The branches below must be a subset of the branches above # The branches below must be a subset of the branches above
branches: [ master ] branches: [ master ]
schedule: schedule:
- cron: '35 2 * * 0' - cron: '35 2 * * 0'
jobs: jobs:
analyze: analyze:
name: Analyze name: Analyze
runs-on: ubuntu-latest runs-on: ubuntu-latest
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
language: [ 'javascript' ] language: [ 'javascript' ]
# CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ] # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ]
# Learn more: # Learn more:
# https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed # https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v2 uses: actions/checkout@v2
- name: Setup Node.js environment - name: Setup Node.js environment
uses: actions/setup-node@v2.1.2 uses: actions/setup-node@v2.1.2
#with: #with:
# Set always-auth in npmrc # Set always-auth in npmrc
#always-auth: # optional, default is false #always-auth: # optional, default is false
# Version Spec of the version to use. Examples: 12.x, 10.15.1, >=10.15.0 # Version Spec of the version to use. Examples: 12.x, 10.15.1, >=10.15.0
#node-version: # optional #node-version: # optional
# Set this option if you want the action to check for the latest available version that satisfies the version spec # Set this option if you want the action to check for the latest available version that satisfies the version spec
#check-latest: # optional #check-latest: # optional
# Optional registry to set up for auth. Will set the registry in a project level .npmrc and .yarnrc file, and set up auth to read in from env.NODE_AUTH_TOKEN # Optional registry to set up for auth. Will set the registry in a project level .npmrc and .yarnrc file, and set up auth to read in from env.NODE_AUTH_TOKEN
#registry-url: # optional #registry-url: # optional
# Optional scope for authenticating against scoped registries # Optional scope for authenticating against scoped registries
#scope: # optional #scope: # optional
# Used to pull node distributions from node-versions. Since there's a default, this is typically not supplied by the user. # Used to pull node distributions from node-versions. Since there's a default, this is typically not supplied by the user.
#token: # optional, default is ${{ github.token }} #token: # optional, default is ${{ github.token }}
# Initializes the CodeQL tools for scanning. # Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL - name: Initialize CodeQL
uses: github/codeql-action/init@v2 uses: github/codeql-action/init@v2
with: with:
languages: ${{ matrix.language }} languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file. # If you wish to specify custom queries, you can do so here or in a config file.
# By default, queries listed here will override any specified in a config file. # By default, queries listed here will override any specified in a config file.
# Prefix the list here with "+" to use these queries and those in the config file. # Prefix the list here with "+" to use these queries and those in the config file.
# queries: ./path/to/local/query, your-org/your-repo/queries@main # queries: ./path/to/local/query, your-org/your-repo/queries@main
- name: Perform CodeQL Analysis - name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v2 uses: github/codeql-action/analyze@v2

View File

@ -1,21 +1,21 @@
name: Publish docs via GitHub Pages name: Publish docs via GitHub Pages
on: on:
push: push:
branches: branches:
- master - master
paths: paths:
- 'docs/**' - 'docs/**'
jobs: jobs:
build: build:
name: Deploy docs name: Deploy docs
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout main - name: Checkout main
uses: actions/checkout@v2 uses: actions/checkout@v2
- name: Deploy docs - name: Deploy docs
uses: mhausenblas/mkdocs-deploy-gh-pages@master uses: mhausenblas/mkdocs-deploy-gh-pages@master
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
REQUIREMENTS: docs/requirements.txt REQUIREMENTS: docs/requirements.txt

240
.gitignore vendored
View File

@ -1,121 +1,121 @@
# Logs # Logs
logs logs
*.log *.log
*.log.gz *.log.gz
npm-debug.log* npm-debug.log*
yarn-debug.log* yarn-debug.log*
yarn-error.log* yarn-error.log*
lerna-debug.log* lerna-debug.log*
.pnpm-debug.log* .pnpm-debug.log*
# Diagnostic reports (https://nodejs.org/api/report.html) # Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
# Runtime data # Runtime data
pids pids
*.pid *.pid
*.seed *.seed
*.pid.lock *.pid.lock
# node-waf configuration # node-waf configuration
.lock-wscript .lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html) # Compiled binary addons (https://nodejs.org/api/addons.html)
build/ build/
# Dependency directories # Dependency directories
node_modules/ node_modules/
jspm_packages/ jspm_packages/
libvips/ libvips/
# Optional npm cache directory # Optional npm cache directory
.npm .npm
# Optional eslint cache # Optional eslint cache
.eslintcache .eslintcache
# Optional stylelint cache # Optional stylelint cache
.stylelintcache .stylelintcache
# Microbundle cache # Microbundle cache
.rpt2_cache/ .rpt2_cache/
.rts2_cache_cjs/ .rts2_cache_cjs/
.rts2_cache_es/ .rts2_cache_es/
.rts2_cache_umd/ .rts2_cache_umd/
# Optional REPL history # Optional REPL history
.node_repl_history .node_repl_history
# Output of 'npm pack' # Output of 'npm pack'
*.tgz *.tgz
# Yarn Integrity file # Yarn Integrity file
.yarn-integrity .yarn-integrity
# dotenv environment variable files # dotenv environment variable files
.env .env
.env.development.local .env.development.local
.env.test.local .env.test.local
.env.production.local .env.production.local
.env.local .env.local
# Gatsby files # Gatsby files
.cache/ .cache/
# Comment in the public line in if your project uses Gatsby and not Next.js # 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 # https://nextjs.org/blog/next-9-1#public-directory-support
# public # public
# vuepress v2.x temp and cache directory # vuepress v2.x temp and cache directory
.temp .temp
.cache .cache
# Stores VSCode versions used for testing VSCode extensions # Stores VSCode versions used for testing VSCode extensions
.vscode-test .vscode-test
# yarn v2 # yarn v2
.yarn/cache .yarn/cache
.yarn/unplugged .yarn/unplugged
.yarn/build-state.yml .yarn/build-state.yml
.yarn/install-state.gz .yarn/install-state.gz
.pnp.* .pnp.*
# Prerequisites # Prerequisites
*.d *.d
# Compiled Object files # Compiled Object files
*.slo *.slo
*.lo *.lo
*.o *.o
*.obj *.obj
# Precompiled Headers # Precompiled Headers
*.gch *.gch
*.pch *.pch
# Compiled Dynamic libraries # Compiled Dynamic libraries
*.so *.so
*.dylib *.dylib
*.dll *.dll
# Compiled Static libraries # Compiled Static libraries
*.lai *.lai
*.la *.la
*.a *.a
*.lib *.lib
# Executables # Executables
*.exe *.exe
*.out *.out
*.app *.app
# Debugging # Debugging
*.heap *.heap
*.out.* *.out.*
# vscode stuff # vscode stuff
.vscode .vscode
*.code-workspace *.code-workspace
# Databases # Databases
data/ data/
*.sqlite *.sqlite

6
.gitmodules vendored
View File

@ -1,3 +1,3 @@
[submodule "assets/images/region-flags"] [submodule "assets/images/region-flags"]
path = assets/images/region-flags path = assets/images/region-flags
url = https://github.com/fonttools/region-flags url = https://github.com/fonttools/region-flags

View File

@ -1,47 +1,47 @@
cmake_minimum_required(VERSION 3.15) cmake_minimum_required(VERSION 3.15)
cmake_policy(SET CMP0091 NEW) cmake_policy(SET CMP0091 NEW)
cmake_policy(SET CMP0042 NEW) cmake_policy(SET CMP0042 NEW)
project(image) project(image)
file(GLOB SOURCE_FILES "natives/*.cc" "natives/*.h") file(GLOB SOURCE_FILES "natives/*.cc" "natives/*.h")
if (CMAKE_JS_VERSION) if (CMAKE_JS_VERSION)
include_directories(${CMAKE_JS_INC}) include_directories(${CMAKE_JS_INC})
add_library(${PROJECT_NAME} SHARED ${SOURCE_FILES} ${CMAKE_JS_SRC} natives/node/image.cc) add_library(${PROJECT_NAME} SHARED ${SOURCE_FILES} ${CMAKE_JS_SRC} natives/node/image.cc)
set_target_properties(${PROJECT_NAME} PROPERTIES PREFIX "" SUFFIX ".node") set_target_properties(${PROJECT_NAME} PROPERTIES PREFIX "" SUFFIX ".node")
target_link_libraries(${PROJECT_NAME} ${CMAKE_JS_LIB}) target_link_libraries(${PROJECT_NAME} ${CMAKE_JS_LIB})
else() else()
add_executable(${PROJECT_NAME} ${SOURCE_FILES} natives/cli/image.cc) add_executable(${PROJECT_NAME} ${SOURCE_FILES} natives/cli/image.cc)
endif() endif()
target_compile_features(${PROJECT_NAME} PRIVATE cxx_std_17) target_compile_features(${PROJECT_NAME} PRIVATE cxx_std_17)
if(MSVC) # todo: change flags for more parity with GCC/clang, I don't know much about MSVC so pull requests are open if(MSVC) # todo: change flags for more parity with GCC/clang, I don't know much about MSVC so pull requests are open
set(CMAKE_CXX_FLAGS "/Wall /EHsc /GS") set(CMAKE_CXX_FLAGS "/Wall /EHsc /GS")
set(CMAKE_CXX_FLAGS_DEBUG "/Zi") set(CMAKE_CXX_FLAGS_DEBUG "/Zi")
set(CMAKE_CXX_FLAGS_RELEASE "/Ox") set(CMAKE_CXX_FLAGS_RELEASE "/Ox")
set(CMAKE_WINDOWS_EXPORT_ALL_SYMBOLS ON) set(CMAKE_WINDOWS_EXPORT_ALL_SYMBOLS ON)
set(BUILD_SHARED_LIBS TRUE) set(BUILD_SHARED_LIBS TRUE)
else() else()
set(CMAKE_CXX_FLAGS "-Wall -Wextra -Werror=format-security -Wno-cast-function-type -fexceptions -D_GLIBCXX_ASSERTIONS -fstack-clash-protection -pedantic -D_GLIBCXX_USE_CXX11_ABI=1") set(CMAKE_CXX_FLAGS "-Wall -Wextra -Werror=format-security -Wno-cast-function-type -fexceptions -D_GLIBCXX_ASSERTIONS -fstack-clash-protection -pedantic -D_GLIBCXX_USE_CXX11_ABI=1")
set(CMAKE_CXX_FLAGS_DEBUG "-g") set(CMAKE_CXX_FLAGS_DEBUG "-g")
set(CMAKE_CXX_FLAGS_RELEASE "-O2") set(CMAKE_CXX_FLAGS_RELEASE "-O2")
endif() endif()
set(CMAKE_EXPORT_COMPILE_COMMANDS ON) set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
find_package(ImageMagick REQUIRED COMPONENTS Magick++ MagickCore) find_package(ImageMagick REQUIRED COMPONENTS Magick++ MagickCore)
add_definitions(-DMAGICKCORE_QUANTUM_DEPTH=16) add_definitions(-DMAGICKCORE_QUANTUM_DEPTH=16)
add_definitions(-DMAGICKCORE_HDRI_ENABLE=0) add_definitions(-DMAGICKCORE_HDRI_ENABLE=0)
include_directories(${ImageMagick_INCLUDE_DIRS}) include_directories(${ImageMagick_INCLUDE_DIRS})
target_link_libraries(${PROJECT_NAME} ${ImageMagick_LIBRARIES}) target_link_libraries(${PROJECT_NAME} ${ImageMagick_LIBRARIES})
pkg_check_modules(VIPS REQUIRED vips-cpp) pkg_check_modules(VIPS REQUIRED vips-cpp)
include_directories(${VIPS_INCLUDE_DIRS}) include_directories(${VIPS_INCLUDE_DIRS})
link_directories(${VIPS_LIBRARY_DIRS}) link_directories(${VIPS_LIBRARY_DIRS})
target_link_libraries(${PROJECT_NAME} ${VIPS_LDFLAGS}) target_link_libraries(${PROJECT_NAME} ${VIPS_LDFLAGS})
if(MSVC AND CMAKE_JS_NODELIB_DEF AND CMAKE_JS_NODELIB_TARGET AND CMAKE_JS_VERSION) if(MSVC AND CMAKE_JS_NODELIB_DEF AND CMAKE_JS_NODELIB_TARGET AND CMAKE_JS_VERSION)
# Generate node.lib # Generate node.lib
execute_process(COMMAND ${CMAKE_AR} /def:${CMAKE_JS_NODELIB_DEF} /out:${CMAKE_JS_NODELIB_TARGET} ${CMAKE_STATIC_LINKER_FLAGS}) execute_process(COMMAND ${CMAKE_AR} /def:${CMAKE_JS_NODELIB_DEF} /out:${CMAKE_JS_NODELIB_TARGET} ${CMAKE_STATIC_LINKER_FLAGS})
endif() endif()

View File

@ -1,70 +1,70 @@
# Docker/Kubernetes file for running the bot # Docker/Kubernetes file for running the bot
#FROM node:alpine #FROM node:alpine
FROM alpine:edge FROM alpine:edge
RUN apk --no-cache upgrade RUN apk --no-cache upgrade
RUN apk add --no-cache git cmake msttcorefonts-installer python3 alpine-sdk ffmpeg wget rpm2cpio \ RUN apk add --no-cache git cmake msttcorefonts-installer python3 alpine-sdk ffmpeg wget rpm2cpio \
zlib-dev libpng-dev libjpeg-turbo-dev freetype-dev fontconfig-dev \ zlib-dev libpng-dev libjpeg-turbo-dev freetype-dev fontconfig-dev \
libtool libwebp-dev libxml2-dev freetype fontconfig \ libtool libwebp-dev libxml2-dev freetype fontconfig \
vips vips-dev grep libc6-compat nodejs-current nodejs-current-dev npm vips vips-dev grep libc6-compat nodejs-current nodejs-current-dev npm
# install pnpm # install pnpm
RUN --mount=type=cache,id=pnpm-store,target=/root/.pnpm-store \ RUN --mount=type=cache,id=pnpm-store,target=/root/.pnpm-store \
npm install -g pnpm@6.27.1 npm install -g pnpm@6.27.1
# liblqr needs to be built manually for magick to work # liblqr needs to be built manually for magick to work
# and because alpine doesn't have it in their repos # and because alpine doesn't have it in their repos
RUN git clone https://github.com/carlobaldassi/liblqr \ RUN git clone https://github.com/carlobaldassi/liblqr \
&& cd liblqr \ && cd liblqr \
&& ./configure \ && ./configure \
&& make \ && make \
&& make install && make install
# install imagemagick from source rather than using the package # install imagemagick from source rather than using the package
# since the alpine package does not include liblqr support. # since the alpine package does not include liblqr support.
RUN git clone https://github.com/ImageMagick/ImageMagick.git ImageMagick \ RUN git clone https://github.com/ImageMagick/ImageMagick.git ImageMagick \
&& cd ImageMagick \ && cd ImageMagick \
&& ./configure \ && ./configure \
--prefix=/usr \ --prefix=/usr \
--sysconfdir=/etc \ --sysconfdir=/etc \
--mandir=/usr/share/man \ --mandir=/usr/share/man \
--infodir=/usr/share/info \ --infodir=/usr/share/info \
--enable-static \ --enable-static \
--disable-openmp \ --disable-openmp \
--with-threads \ --with-threads \
--with-png \ --with-png \
--with-webp \ --with-webp \
--with-modules \ --with-modules \
--with-pango \ --with-pango \
--without-hdri \ --without-hdri \
--with-lqr \ --with-lqr \
&& make \ && make \
&& make install && make install
RUN update-ms-fonts && fc-cache -f RUN update-ms-fonts && fc-cache -f
RUN adduser esmBot -s /bin/sh -D RUN adduser esmBot -s /bin/sh -D
USER esmBot USER esmBot
WORKDIR /home/esmBot/.internal WORKDIR /home/esmBot/.internal
COPY --chown=esmBot:esmBot ./package.json package.json COPY --chown=esmBot:esmBot ./package.json package.json
COPY --chown=esmBot:esmBot ./pnpm-lock.yaml pnpm-lock.yaml COPY --chown=esmBot:esmBot ./pnpm-lock.yaml pnpm-lock.yaml
RUN pnpm install RUN pnpm install
COPY . . COPY . .
RUN rm .env RUN rm .env
RUN pnpm run build RUN pnpm run build
RUN mkdir /home/esmBot/help \ RUN mkdir /home/esmBot/help \
&& chown esmBot:esmBot /home/esmBot/help \ && chown esmBot:esmBot /home/esmBot/help \
&& chmod 777 /home/esmBot/help && chmod 777 /home/esmBot/help
RUN mkdir /home/esmBot/temp \ RUN mkdir /home/esmBot/temp \
&& chown esmBot:esmBot /home/esmBot/temp \ && chown esmBot:esmBot /home/esmBot/temp \
&& chmod 777 /home/esmBot/temp && chmod 777 /home/esmBot/temp
RUN mkdir /home/esmBot/.internal/logs \ RUN mkdir /home/esmBot/.internal/logs \
&& chown esmBot:esmBot /home/esmBot/.internal/logs \ && chown esmBot:esmBot /home/esmBot/.internal/logs \
&& chmod 777 /home/esmBot/.internal/logs && chmod 777 /home/esmBot/.internal/logs
ENTRYPOINT ["node", "app.js"] ENTRYPOINT ["node", "app.js"]

42
LICENSE
View File

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

View File

@ -1,24 +1,24 @@
# esmBot and Privacy # esmBot and Privacy
First things first: **esmBot does not and is incapable of collecting IP addresses, emails, or any other sensitive personal/private info.** This info is not accessible via Discord's API [except for emails](https://discord.com/developers/docs/resources/user#user-object), which require the email OAuth2 scope to access. esmBot does not use OAuth2 to link to a user account, therefore it does not have access to this info. First things first: **esmBot does not and is incapable of collecting IP addresses, emails, or any other sensitive personal/private info.** This info is not accessible via Discord's API [except for emails](https://discord.com/developers/docs/resources/user#user-object), which require the email OAuth2 scope to access. esmBot does not use OAuth2 to link to a user account, therefore it does not have access to this info.
Whenever a command is run using esmBot, a command count number is increased. **This counter is completely anonymous and is used only for statistical purposes.** Users can check this info at any time using the count and help commands. Whenever a command is run using esmBot, a command count number is increased. **This counter is completely anonymous and is used only for statistical purposes.** Users can check this info at any time using the count and help commands.
esmBot uses the following user-related info: esmBot uses the following user-related info:
+ User IDs (needed for many reasons such as the tag commands and replying to users) + User IDs (needed for many reasons such as the tag commands and replying to users)
+ Avatars (needed for some embeds and the avatar command) + Avatars (needed for some embeds and the avatar command)
+ Usernames (for embeds and avatar command) + Usernames (for embeds and avatar command)
+ Permissions (for checking if a user has perms to run some commands) + Permissions (for checking if a user has perms to run some commands)
+ Whether the user is a bot (needed to prevent other bots from running commands) + Whether the user is a bot (needed to prevent other bots from running commands)
Out of these, **only user IDs are stored in the database**, and they are used only with the tag system for checking the owner of a tag. Out of these, **only user IDs are stored in the database**, and they are used only with the tag system for checking the owner of a tag.
esmBot uses the following guild-related info: esmBot uses the following guild-related info:
+ Guild IDs (for guild-specific settings) + Guild IDs (for guild-specific settings)
+ Guild channel IDs (for getting where to send a message, storing disabled channels) + Guild channel IDs (for getting where to send a message, storing disabled channels)
+ List of members (for getting permissions and obtaining user objects by ID) + List of members (for getting permissions and obtaining user objects by ID)
Out of these, **only guild and channel IDs are stored in the database** for configuration info and storing disabled channels/commands, prefixes, and tags. Out of these, **only guild and channel IDs are stored in the database** for configuration info and storing disabled channels/commands, prefixes, and tags.
If you want this data removed on the main instance, you can DM me on Discord (Essem#9261) or email me at [data@esmbot.net](mailto:data@esmbot.net). If you want this data removed on the main instance, you can DM me on Discord (Essem#9261) or email me at [data@esmbot.net](mailto:data@esmbot.net).
Hopefully this document is clear enough to help understand what esmBot does and doesn't use. If you have any further questions, please contact me via the [esmBot Support](https://esmbot.net/support) server. Hopefully this document is clear enough to help understand what esmBot does and doesn't use. If you have any further questions, please contact me via the [esmBot Support](https://esmbot.net/support) server.

View File

@ -1,29 +1,29 @@
# <img src="https://github.com/esmBot/esmBot/raw/master/docs/assets/esmbot.png" width="128"> esmBot # <img src="https://github.com/esmBot/esmBot/raw/master/docs/assets/esmbot.png" width="128"> esmBot
[![esmBot Support](https://discordapp.com/api/guilds/592399417676529688/embed.png)](https://discord.gg/esmbot) ![GitHub license](https://img.shields.io/github/license/esmBot/esmBot.svg) [![esmBot Support](https://discordapp.com/api/guilds/592399417676529688/embed.png)](https://discord.gg/esmbot) ![GitHub license](https://img.shields.io/github/license/esmBot/esmBot.svg)
esmBot is a free and open-source Discord bot designed to entertain your server. It's made using [Oceanic](https://oceanic.ws) and comes with image, music, and utility commands out of the box. esmBot is a free and open-source Discord bot designed to entertain your server. It's made using [Oceanic](https://oceanic.ws) and comes with image, music, and utility commands out of the box.
## Features ## Features
- Powerful, efficient, and performant image processing powered by [libvips](https://github.com/libvips/libvips) - Powerful, efficient, and performant image processing powered by [libvips](https://github.com/libvips/libvips)
- Lots of image manipulation and processing commands out of the box - Lots of image manipulation and processing commands out of the box
- Handling of output images larger than 8MB via a local web server - Handling of output images larger than 8MB via a local web server
- Optional WebSocket/HTTP-based external image API with load balancing - Optional WebSocket/HTTP-based external image API with load balancing
- Music and sound playback from many different configurable sources via [Lavalink](https://github.com/freyacodes/Lavalink) - Music and sound playback from many different configurable sources via [Lavalink](https://github.com/freyacodes/Lavalink)
- Server tags system for saving/retrieving content - Server tags system for saving/retrieving content
- Low RAM and CPU usage when idle - Low RAM and CPU usage when idle
- Support for slash/application commands and classic, prefix-based message commands - Support for slash/application commands and classic, prefix-based message commands
- Support for multiple database backends (PostgreSQL and SQLite backends included) - Support for multiple database backends (PostgreSQL and SQLite backends included)
- [PM2](https://pm2.keymetrics.io)-based cluster/shard handling - [PM2](https://pm2.keymetrics.io)-based cluster/shard handling
- Flexible command handler allowing you to create new commands by adding script files - Flexible command handler allowing you to create new commands by adding script files
## Usage ## Usage
You can invite the main instance of esmBot to your server using this link: https://esmbot.net/invite You can invite the main instance of esmBot to your server using this link: https://esmbot.net/invite
A command list can be found [here](https://esmbot.net/help.html). A command list can be found [here](https://esmbot.net/help.html).
If you want to self-host the bot, a guide can be found [here](https://docs.esmbot.net/setup). If you want to self-host the bot, a guide can be found [here](https://docs.esmbot.net/setup).
## Credits ## Credits
Icon by [Steel](https://twitter.com/MintBurrow). Icon by [Steel](https://twitter.com/MintBurrow).
All images, sounds, and fonts are copyright of their respective owners. All images, sounds, and fonts are copyright of their respective owners.

View File

@ -1,51 +1,51 @@
# esmBot Image API # esmBot Image API
The esmBot image API is a combined HTTP and WebSocket API. The default port to access the API is 3762. The API supports very basic authentication, which is defined on the server via the PASS environment variable and is sent from the client via the Authentication header in both HTTP and WS requests. The esmBot image API is a combined HTTP and WebSocket API. The default port to access the API is 3762. The API supports very basic authentication, which is defined on the server via the PASS environment variable and is sent from the client via the Authentication header in both HTTP and WS requests.
## HTTP ## HTTP
### GET `/image/?id=<job id>` ### GET `/image/?id=<job id>`
Get image data after job is finished running. The Content-Type header is properly set. Get image data after job is finished running. The Content-Type header is properly set.
### GET `/count` ### GET `/count`
Get the current amount of running jobs. Response is a plaintext number value. Get the current amount of running jobs. Response is a plaintext number value.
## WebSockets ## WebSockets
A client sends *requests* (T-messages) to a server, which subsequently *replies* (R-messages) to the client. A client sends *requests* (T-messages) to a server, which subsequently *replies* (R-messages) to the client.
### Message IDs ### Message IDs
- Rerror 0x01 - Rerror 0x01
- Tqueue 0x02 - Tqueue 0x02
- Rqueue 0x03 - Rqueue 0x03
- Tcancel 0x04 - Tcancel 0x04
- Rcancel 0x05 - Rcancel 0x05
- Twait 0x06 - Twait 0x06
- Rwait 0x07 - Rwait 0x07
- Rinit 0x08 - Rinit 0x08
### Messages ### Messages
[n] means n bytes. [n] means n bytes.
[s] means a string that goes until the end of the message. [s] means a string that goes until the end of the message.
[j] means JSON data that goes until the end of the message. [j] means JSON data that goes until the end of the message.
`tag` is used to identify a request/response pair, like `lock` in the original API. `jid` is used to identify a job. `job` is a job object. `tag` is used to identify a request/response pair, like `lock` in the original API. `jid` is used to identify a job. `job` is a job object.
- Rerror tag[2] error[s] - Rerror tag[2] error[s]
- Tqueue tag[2] jid[8] job[j] - Tqueue tag[2] jid[8] job[j]
- Rqueue tag[2] - Rqueue tag[2]
- Tcancel tag[2] jid[8] - Tcancel tag[2] jid[8]
- Rcancel tag[2] - Rcancel tag[2]
- Twait tag[2] jid[8] - Twait tag[2] jid[8]
- Rwait tag[2] - Rwait tag[2]
- Rinit tag[2] max_jobs[2] running_jobs[2] formats[j] - Rinit tag[2] max_jobs[2] running_jobs[2] formats[j]
### Job Object ### Job Object
The job object is formatted like this: The job object is formatted like this:
```js ```js
{ {
"cmd": string, // name of internal image command, e.g. caption "cmd": string, // name of internal image command, e.g. caption
"path": string, // canonical image URL, used for getting the actual image "path": string, // canonical image URL, used for getting the actual image
"url": string, // original image URL, used for message filtering "url": string, // original image URL, used for message filtering
"params": { // content varies depending on the command, some common parameters are listed here "params": { // content varies depending on the command, some common parameters are listed here
"type": string, // mime type of output, should usually be the same as input "type": string, // mime type of output, should usually be the same as input
... ...
}, },
"name": string // filename of the image, without extension "name": string // filename of the image, without extension
} }
``` ```

View File

@ -1,290 +1,290 @@
import { config } from "dotenv"; import { config } from "dotenv";
config(); config();
import { cpus } from "os"; import { cpus } from "os";
import { Worker } from "worker_threads"; import { Worker } from "worker_threads";
import { join } from "path"; import { join } from "path";
import { createServer } from "http"; import { createServer } from "http";
import { WebSocketServer } from "ws"; import { WebSocketServer } from "ws";
import { fileURLToPath } from "url"; import { fileURLToPath } from "url";
import { dirname } from "path"; import { dirname } from "path";
import { createRequire } from "module"; import { createRequire } from "module";
import EventEmitter from "events"; import EventEmitter from "events";
const nodeRequire = createRequire(import.meta.url); const nodeRequire = createRequire(import.meta.url);
const img = nodeRequire(`../build/${process.env.DEBUG && process.env.DEBUG === "true" ? "Debug" : "Release"}/image.node`); const img = nodeRequire(`../build/${process.env.DEBUG && process.env.DEBUG === "true" ? "Debug" : "Release"}/image.node`);
const Rerror = 0x01; const Rerror = 0x01;
const Tqueue = 0x02; const Tqueue = 0x02;
const Rqueue = 0x03; const Rqueue = 0x03;
const Tcancel = 0x04; const Tcancel = 0x04;
const Rcancel = 0x05; const Rcancel = 0x05;
const Twait = 0x06; const Twait = 0x06;
const Rwait = 0x07; const Rwait = 0x07;
const Rinit = 0x08; const Rinit = 0x08;
const start = process.hrtime(); const start = process.hrtime();
const log = (msg, jobNum) => { const log = (msg, jobNum) => {
console.log(`[${process.hrtime(start)[1] / 1000000}${jobNum ? `:${jobNum}` : ""}]\t ${msg}`); console.log(`[${process.hrtime(start)[1] / 1000000}${jobNum ? `:${jobNum}` : ""}]\t ${msg}`);
}; };
const error = (msg, jobNum) => { const error = (msg, jobNum) => {
console.error(`[${process.hrtime(start)[1] / 1000000}${jobNum ? `:${jobNum}` : ""}]\t ${msg}`); console.error(`[${process.hrtime(start)[1] / 1000000}${jobNum ? `:${jobNum}` : ""}]\t ${msg}`);
}; };
class JobCache extends Map { class JobCache extends Map {
set(key, value) { set(key, value) {
super.set(key, value); super.set(key, value);
setTimeout(() => { setTimeout(() => {
if (super.has(key) && this.get(key) === value && value.data) super.delete(key); if (super.has(key) && this.get(key) === value && value.data) super.delete(key);
}, 300000); // delete jobs if not requested after 5 minutes }, 300000); // delete jobs if not requested after 5 minutes
} }
} }
const jobs = new JobCache(); const jobs = new JobCache();
// Should look like ID : { msg: "request", num: <job number> } // Should look like ID : { msg: "request", num: <job number> }
const queue = []; const queue = [];
// Array of IDs // Array of IDs
const MAX_JOBS = process.env.JOBS ? parseInt(process.env.JOBS) : cpus().length * 4; // Completely arbitrary, should usually be some multiple of your amount of cores const MAX_JOBS = process.env.JOBS ? parseInt(process.env.JOBS) : cpus().length * 4; // Completely arbitrary, should usually be some multiple of your amount of cores
const PASS = process.env.PASS ? process.env.PASS : undefined; const PASS = process.env.PASS ? process.env.PASS : undefined;
let jobAmount = 0; let jobAmount = 0;
const acceptJob = (id, sock) => { const acceptJob = (id, sock) => {
jobAmount++; jobAmount++;
queue.shift(); queue.shift();
const job = jobs.get(id); const job = jobs.get(id);
return runJob({ return runJob({
id: id, id: id,
msg: job.msg, msg: job.msg,
num: job.num num: job.num
}, sock).then(() => { }, sock).then(() => {
log(`Job ${id} has finished`); log(`Job ${id} has finished`);
}).catch((err) => { }).catch((err) => {
error(`Error on job ${id}: ${err}`, job.num); error(`Error on job ${id}: ${err}`, job.num);
const newJob = jobs.get(id); const newJob = jobs.get(id);
if (!newJob.tag) { if (!newJob.tag) {
newJob.error = err.message; newJob.error = err.message;
jobs.set(id, newJob); jobs.set(id, newJob);
return; return;
} }
jobs.delete(id); jobs.delete(id);
sock.send(Buffer.concat([Buffer.from([Rerror]), newJob.tag, Buffer.from(err.message)])); sock.send(Buffer.concat([Buffer.from([Rerror]), newJob.tag, Buffer.from(err.message)]));
}).finally(() => { }).finally(() => {
jobAmount--; jobAmount--;
if (queue.length > 0) { if (queue.length > 0) {
acceptJob(queue[0], sock); acceptJob(queue[0], sock);
} }
}); });
}; };
const waitForVerify = (event) => { const waitForVerify = (event) => {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
event.once("end", (r) => resolve(r)); event.once("end", (r) => resolve(r));
event.once("error", (e) => reject(e)); event.once("error", (e) => reject(e));
}); });
}; };
const wss = new WebSocketServer({ clientTracking: true, noServer: true }); const wss = new WebSocketServer({ clientTracking: true, noServer: true });
wss.on("connection", (ws, request) => { wss.on("connection", (ws, request) => {
log(`WS client ${request.socket.remoteAddress}:${request.socket.remotePort} has connected`); log(`WS client ${request.socket.remoteAddress}:${request.socket.remotePort} has connected`);
const num = Buffer.alloc(2); const num = Buffer.alloc(2);
num.writeUInt16LE(MAX_JOBS); num.writeUInt16LE(MAX_JOBS);
const cur = Buffer.alloc(2); const cur = Buffer.alloc(2);
cur.writeUInt16LE(jobAmount); cur.writeUInt16LE(jobAmount);
const formats = {}; const formats = {};
for (const cmd of img.funcs) { for (const cmd of img.funcs) {
formats[cmd] = ["image/png", "image/gif", "image/jpeg", "image/webp"]; formats[cmd] = ["image/png", "image/gif", "image/jpeg", "image/webp"];
} }
const init = Buffer.concat([Buffer.from([Rinit]), Buffer.from([0x00, 0x00]), num, cur, Buffer.from(JSON.stringify(formats))]); const init = Buffer.concat([Buffer.from([Rinit]), Buffer.from([0x00, 0x00]), num, cur, Buffer.from(JSON.stringify(formats))]);
ws.send(init); ws.send(init);
ws.on("error", (err) => { ws.on("error", (err) => {
error(err); error(err);
}); });
ws.on("message", (msg) => { ws.on("message", (msg) => {
const opcode = msg.readUint8(0); const opcode = msg.readUint8(0);
const tag = msg.slice(1, 3); const tag = msg.slice(1, 3);
const req = msg.toString().slice(3); const req = msg.toString().slice(3);
if (opcode == Tqueue) { if (opcode == Tqueue) {
const id = msg.readBigInt64LE(3); const id = msg.readBigInt64LE(3);
const obj = msg.slice(11); const obj = msg.slice(11);
const job = { msg: obj, num: jobAmount, verifyEvent: new EventEmitter() }; const job = { msg: obj, num: jobAmount, verifyEvent: new EventEmitter() };
jobs.set(id, job); jobs.set(id, job);
queue.push(id); queue.push(id);
const newBuffer = Buffer.concat([Buffer.from([Rqueue]), tag]); const newBuffer = Buffer.concat([Buffer.from([Rqueue]), tag]);
ws.send(newBuffer); ws.send(newBuffer);
if (jobAmount < MAX_JOBS) { if (jobAmount < MAX_JOBS) {
log(`Got WS request for job ${job.msg} with id ${id}`, job.num); log(`Got WS request for job ${job.msg} with id ${id}`, job.num);
acceptJob(id, ws); acceptJob(id, ws);
} else { } else {
log(`Got WS request for job ${job.msg} with id ${id}, queued in position ${queue.indexOf(id)}`, job.num); log(`Got WS request for job ${job.msg} with id ${id}, queued in position ${queue.indexOf(id)}`, job.num);
} }
} else if (opcode == Tcancel) { } else if (opcode == Tcancel) {
delete queue[queue.indexOf(req) - 1]; delete queue[queue.indexOf(req) - 1];
jobs.delete(req); jobs.delete(req);
const cancelResponse = Buffer.concat([Buffer.from([Rcancel]), tag]); const cancelResponse = Buffer.concat([Buffer.from([Rcancel]), tag]);
ws.send(cancelResponse); ws.send(cancelResponse);
} else if (opcode == Twait) { } else if (opcode == Twait) {
const id = msg.readBigUInt64LE(3); const id = msg.readBigUInt64LE(3);
const job = jobs.get(id); const job = jobs.get(id);
if (!job) { if (!job) {
const errorResponse = Buffer.concat([Buffer.from([Rerror]), tag, Buffer.from("Invalid job ID")]); const errorResponse = Buffer.concat([Buffer.from([Rerror]), tag, Buffer.from("Invalid job ID")]);
ws.send(errorResponse); ws.send(errorResponse);
return; return;
} }
if (job.error) { if (job.error) {
job.verifyEvent.emit("error", job.error); job.verifyEvent.emit("error", job.error);
jobs.delete(id); jobs.delete(id);
const errorResponse = Buffer.concat([Buffer.from([Rerror]), tag, Buffer.from(job.error)]); const errorResponse = Buffer.concat([Buffer.from([Rerror]), tag, Buffer.from(job.error)]);
ws.send(errorResponse); ws.send(errorResponse);
return; return;
} }
job.verifyEvent.emit("end", tag); job.verifyEvent.emit("end", tag);
job.tag = tag; job.tag = tag;
jobs.set(id, job); jobs.set(id, job);
//const waitResponse = Buffer.concat([Buffer.from([Rwait]), tag]); //const waitResponse = Buffer.concat([Buffer.from([Rwait]), tag]);
//ws.send(waitResponse); //ws.send(waitResponse);
} else { } else {
log("Could not parse WS message"); log("Could not parse WS message");
} }
}); });
ws.on("close", () => { ws.on("close", () => {
log(`WS client ${request.socket.remoteAddress}:${request.socket.remotePort} has disconnected`); log(`WS client ${request.socket.remoteAddress}:${request.socket.remotePort} has disconnected`);
}); });
}); });
wss.on("error", (err) => { wss.on("error", (err) => {
error("A WS error occurred: ", err); error("A WS error occurred: ", err);
}); });
const httpServer = createServer(); const httpServer = createServer();
httpServer.on("request", async (req, res) => { httpServer.on("request", async (req, res) => {
if (req.method !== "GET") { if (req.method !== "GET") {
res.statusCode = 405; res.statusCode = 405;
return res.end("405 Method Not Allowed"); return res.end("405 Method Not Allowed");
} }
if (PASS && req.headers.authentication !== PASS) { if (PASS && req.headers.authentication !== PASS) {
res.statusCode = 401; res.statusCode = 401;
return res.end("401 Unauthorized"); return res.end("401 Unauthorized");
} }
const reqUrl = new URL(req.url, `http://${req.headers.host}`); const reqUrl = new URL(req.url, `http://${req.headers.host}`);
if (reqUrl.pathname === "/image" && req.method === "GET") { if (reqUrl.pathname === "/image" && req.method === "GET") {
if (!reqUrl.searchParams.has("id")) { if (!reqUrl.searchParams.has("id")) {
res.statusCode = 400; res.statusCode = 400;
return res.end("400 Bad Request"); return res.end("400 Bad Request");
} }
const id = BigInt(reqUrl.searchParams.get("id")); const id = BigInt(reqUrl.searchParams.get("id"));
if (!jobs.has(id)) { if (!jobs.has(id)) {
res.statusCode = 410; res.statusCode = 410;
return res.end("410 Gone"); return res.end("410 Gone");
} }
log(`Sending image data for job ${id} to ${req.socket.remoteAddress}:${req.socket.remotePort} via HTTP`); log(`Sending image data for job ${id} to ${req.socket.remoteAddress}:${req.socket.remotePort} via HTTP`);
const ext = jobs.get(id).ext; const ext = jobs.get(id).ext;
let contentType; let contentType;
switch (ext) { switch (ext) {
case "gif": case "gif":
contentType = "image/gif"; contentType = "image/gif";
break; break;
case "png": case "png":
contentType = "image/png"; contentType = "image/png";
break; break;
case "jpeg": case "jpeg":
case "jpg": case "jpg":
contentType = "image/jpeg"; contentType = "image/jpeg";
break; break;
case "webp": case "webp":
contentType = "image/webp"; contentType = "image/webp";
break; break;
} }
if (contentType) res.setHeader("Content-Type", contentType); if (contentType) res.setHeader("Content-Type", contentType);
else res.setHeader("Content-Type", ext); else res.setHeader("Content-Type", ext);
const data = jobs.get(id).data; const data = jobs.get(id).data;
jobs.delete(id); jobs.delete(id);
return res.end(data, (err) => { return res.end(data, (err) => {
if (err) error(err); if (err) error(err);
}); });
} else if (reqUrl.pathname === "/count" && req.method === "GET") { } else if (reqUrl.pathname === "/count" && req.method === "GET") {
log(`Sending job count to ${req.socket.remoteAddress}:${req.socket.remotePort} via HTTP`); log(`Sending job count to ${req.socket.remoteAddress}:${req.socket.remotePort} via HTTP`);
return res.end(jobAmount.toString(), (err) => { return res.end(jobAmount.toString(), (err) => {
if (err) error(err); if (err) error(err);
}); });
} else { } else {
res.statusCode = 404; res.statusCode = 404;
return res.end("404 Not Found"); return res.end("404 Not Found");
} }
}); });
httpServer.on("upgrade", (req, sock, head) => { httpServer.on("upgrade", (req, sock, head) => {
const reqUrl = new URL(req.url, `http://${req.headers.host}`); const reqUrl = new URL(req.url, `http://${req.headers.host}`);
if (PASS && req.headers.authentication !== PASS) { if (PASS && req.headers.authentication !== PASS) {
sock.write("HTTP/1.1 401 Unauthorized\r\n\r\n"); sock.write("HTTP/1.1 401 Unauthorized\r\n\r\n");
sock.destroy(); sock.destroy();
return; return;
} }
if (reqUrl.pathname === "/sock") { if (reqUrl.pathname === "/sock") {
wss.handleUpgrade(req, sock, head, (ws) => { wss.handleUpgrade(req, sock, head, (ws) => {
wss.emit("connection", ws, req); wss.emit("connection", ws, req);
}); });
} else { } else {
sock.destroy(); sock.destroy();
} }
}); });
httpServer.on("error", (e) => { httpServer.on("error", (e) => {
error("An HTTP error occurred: ", e); error("An HTTP error occurred: ", e);
}); });
const port = parseInt(process.env.PORT) || 3762; const port = parseInt(process.env.PORT) || 3762;
httpServer.listen(port, () => { httpServer.listen(port, () => {
log("HTTP and WS listening on port 3762"); log("HTTP and WS listening on port 3762");
}); });
const runJob = (job, ws) => { const runJob = (job, ws) => {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
log(`Job ${job.id} starting...`, job.num); log(`Job ${job.id} starting...`, job.num);
const object = JSON.parse(job.msg); const object = JSON.parse(job.msg);
// If the image has a path, it must also have a type // If the image has a path, it must also have a type
if (object.path && !object.params.type) { if (object.path && !object.params.type) {
reject(new TypeError("Unknown image type")); reject(new TypeError("Unknown image type"));
} }
const worker = new Worker(join(dirname(fileURLToPath(import.meta.url)), "../utils/image-runner.js"), { const worker = new Worker(join(dirname(fileURLToPath(import.meta.url)), "../utils/image-runner.js"), {
workerData: object workerData: object
}); });
const timeout = setTimeout(() => { const timeout = setTimeout(() => {
worker.terminate(); worker.terminate();
reject(new Error("Job timed out")); reject(new Error("Job timed out"));
}, 900000); }, 900000);
log(`Job ${job.id} started`, job.num); log(`Job ${job.id} started`, job.num);
worker.once("message", (data) => { worker.once("message", (data) => {
clearTimeout(timeout); clearTimeout(timeout);
log(`Sending result of job ${job.id} back to the bot`, job.num); log(`Sending result of job ${job.id} back to the bot`, job.num);
const jobObject = jobs.get(job.id); const jobObject = jobs.get(job.id);
jobObject.data = data.buffer; jobObject.data = data.buffer;
jobObject.ext = data.fileExtension; jobObject.ext = data.fileExtension;
let verifyPromise; let verifyPromise;
if (!jobObject.tag) { if (!jobObject.tag) {
verifyPromise = waitForVerify(jobObject.verifyEvent); verifyPromise = waitForVerify(jobObject.verifyEvent);
} else { } else {
verifyPromise = Promise.resolve(jobObject.tag); verifyPromise = Promise.resolve(jobObject.tag);
} }
verifyPromise.then(tag => { verifyPromise.then(tag => {
jobs.set(job.id, jobObject); jobs.set(job.id, jobObject);
const waitResponse = Buffer.concat([Buffer.from([Rwait]), tag]); const waitResponse = Buffer.concat([Buffer.from([Rwait]), tag]);
ws.send(waitResponse); ws.send(waitResponse);
resolve(); resolve();
}); });
}); });
worker.once("error", (e) => { worker.once("error", (e) => {
clearTimeout(timeout); clearTimeout(timeout);
reject(e); reject(e);
}); });
}); });
}; };

416
app.js
View File

@ -1,228 +1,190 @@
if (process.versions.node.split(".")[0] < 16) { if (process.versions.node.split(".")[0] < 16) {
console.error(`You are currently running Node.js version ${process.version}. console.error(`You are currently running Node.js version ${process.version}.
esmBot requires Node.js version 16 or above. esmBot requires Node.js version 16 or above.
Please refer to step 3 of the setup guide.`); Please refer to step 3 of the setup guide.`);
process.exit(1); process.exit(1);
} }
if (process.platform === "win32") { if (process.platform === "win32") {
console.error("\x1b[1m\x1b[31m\x1b[40m" + `WINDOWS IS NOT OFFICIALLY SUPPORTED! console.error("\x1b[1m\x1b[31m\x1b[40m" + `WINDOWS IS NOT OFFICIALLY SUPPORTED!
Although there's a (very) slim chance of it working, multiple aspects of the bot are built with UNIX-like systems in mind and could break on Win32-based systems. If you want to run the bot on Windows, using Windows Subsystem for Linux is highly recommended. Although there's a (very) slim chance of it working, multiple aspects of the bot are built with UNIX-like systems in mind and could break on Win32-based systems. If you want to run the bot on Windows, using Windows Subsystem for Linux is highly recommended.
The bot will continue to run past this message in 5 seconds, but keep in mind that it could break at any time. Continue running at your own risk; alternatively, stop the bot using Ctrl+C and install WSL.` + "\x1b[0m"); The bot will continue to run past this message in 5 seconds, but keep in mind that it could break at any time. Continue running at your own risk; alternatively, stop the bot using Ctrl+C and install WSL.` + "\x1b[0m");
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 5000); Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 5000);
} }
// load config from .env file // load config from .env file
import { resolve, dirname } from "path"; import { resolve, dirname } from "path";
import { fileURLToPath } from "url"; import { fileURLToPath } from "url";
import { config } from "dotenv"; import { config } from "dotenv";
config({ path: resolve(dirname(fileURLToPath(import.meta.url)), ".env") }); config({ path: resolve(dirname(fileURLToPath(import.meta.url)), ".env") });
import { reloadImageConnections } from "./utils/image.js"; import { reloadImageConnections } from "./utils/image.js";
// main services // main services
import { Client } from "oceanic.js"; // import { Client } from "oceanic.js";
import pm2 from "pm2"; import * as sdk from "matrix-js-sdk";
// some utils const AutojoinRoomsMixin = sdk.AutojoinRoomsMixin;
import { promises, readFileSync } from "fs";
import { logger } from "./utils/logger.js";
import { exec as baseExec } from "child_process";
import { promisify } from "util";
const exec = promisify(baseExec); import pm2 from "pm2";
// initialize command loader // some utils
import { load } from "./utils/handler.js"; import { promises, readFileSync } from "fs";
// command collections import { logger } from "./utils/logger.js";
import { paths } from "./utils/collections.js"; import { exec as baseExec } from "child_process";
// database stuff import { promisify } from "util";
import database from "./utils/database.js"; const exec = promisify(baseExec);
// lavalink stuff // initialize command loader
import { reload, connect, connected } from "./utils/soundplayer.js"; import { load } from "./utils/handler.js";
// events // command collections
import { endBroadcast, startBroadcast } from "./utils/misc.js"; import { paths } from "./utils/collections.js";
import { parseThreshold } from "./utils/tempimages.js"; // database stuff
import database from "./utils/database.js";
const { types } = JSON.parse(readFileSync(new URL("./config/commands.json", import.meta.url))); // lavalink stuff
const esmBotVersion = JSON.parse(readFileSync(new URL("./package.json", import.meta.url))).version; import { reload, connect, connected } from "./utils/soundplayer.js";
process.env.ESMBOT_VER = esmBotVersion; // events
import { endBroadcast, startBroadcast } from "./utils/misc.js";
const intents = [ import { parseThreshold } from "./utils/tempimages.js";
"GUILD_VOICE_STATES",
"DIRECT_MESSAGES", const { types } = JSON.parse(readFileSync(new URL("./config/commands.json", import.meta.url)));
"GUILDS" const esmBotVersion = JSON.parse(readFileSync(new URL("./package.json", import.meta.url))).version;
]; process.env.ESMBOT_VER = esmBotVersion;
if (types.classic) {
intents.push("GUILD_MESSAGES"); // const intents = [
intents.push("MESSAGE_CONTENT"); // "GUILD_VOICE_STATES",
} // "DIRECT_MESSAGES",
// "GUILDS"
async function* getFiles(dir) { // ];
const dirents = await promises.readdir(dir, { withFileTypes: true }); // if (types.classic) {
for (const dirent of dirents) { // intents.push("GUILD_MESSAGES");
const name = dir + (dir.charAt(dir.length - 1) !== "/" ? "/" : "") + dirent.name; // intents.push("MESSAGE_CONTENT");
if (dirent.isDirectory()) { // }
yield* getFiles(name);
} else if (dirent.name.endsWith(".js")) { async function* getFiles(dir) {
yield name; const dirents = await promises.readdir(dir, { withFileTypes: true });
} for (const dirent of dirents) {
} const name = dir + (dir.charAt(dir.length - 1) !== "/" ? "/" : "") + dirent.name;
} if (dirent.isDirectory()) {
yield* getFiles(name);
async function init() { } else if (dirent.name.endsWith(".js")) {
await exec("git rev-parse HEAD").then(output => output.stdout.substring(0, 7), () => "unknown commit").then(o => process.env.GIT_REV = o); yield name;
console.log(` }
,*\`$ z\`"v }
F zBw\`% A ,W "W }
,\` ,EBBBWp"%. ,-=~~==-,+* 4BBE T
M BBBBBBBB* ,w=####Wpw 4BBBBB# 1 async function init() {
F BBBBBBBMwBBBBBBBBBBBBB#wXBBBBBH E await exec("git rev-parse HEAD").then(output => output.stdout.substring(0, 7), () => "unknown commit").then(o => process.env.GIT_REV = o);
F BBBBBBkBBBBBBBBBBBBBBBBBBBBE4BL k console.log(`
# BFBBBBBBBBBBBBF" "RBBBW F ,*\`$ z\`"v
V ' 4BBBBBBBBBBM TBBL F F zBw\`% A ,W "W
F BBBBBBBBBBF JBB L ,\` ,EBBBWp"%. ,-=~~==-,+* 4BBE T
F FBBBBBBBEB BBL 4 M BBBBBBBB* ,w=####Wpw 4BBBBB# 1
E [BB4BBBBEBL BBL 4 F BBBBBBBMwBBBBBBBBBBBBB#wXBBBBBH E
I #BBBBBBBEB 4BBH *w F BBBBBBkBBBBBBBBBBBBBBBBBBBBE4BL k
A 4BBBBBBBBBEW, ,BBBB W [ # BFBBBBBBBBBBBBF" "RBBBW F
.A ,k 4BBBBBBBBBBBEBW####BBBBBBM BF F V ' 4BBBBBBBBBBM TBBL F
k <BBBw BBBBEBBBBBBBBBBBBBBBBBQ4BM # F BBBBBBBBBBF JBB L
5, REBBB4BBBBB#BBBBBBBBBBBBP5BFF ,F F FBBBBBBBEB BBL 4
*w \`*4BBW\`"FF#F##FFFF"\` , * +" E [BB4BBBBEBL BBL 4
*+, " F'"'*^~~~^"^\` V+*^ I #BBBBBBBEB 4BBH *w
\`""" A 4BBBBBBBBBEW, ,BBBB W [
.A ,k 4BBBBBBBBBBBEBW####BBBBBBM BF F
esmBot ${esmBotVersion} (${process.env.GIT_REV}) k <BBBw BBBBEBBBBBBBBBBBBBBBBBQ4BM #
`); 5, REBBB4BBBBB#BBBBBBBBBBBBP5BFF ,F
*w \`*4BBW\`"FF#F##FFFF"\` , * +"
if (!types.classic && !types.application) { *+, " F'"'*^~~~^"^\` V+*^
logger.error("Both classic and application commands are disabled! Please enable at least one command type in config/commands.json."); \`"""
return process.exit(1);
} esmBot ${esmBotVersion} (${process.env.GIT_REV})
`);
if (database) {
// database handling if (!types.classic && !types.application) {
const dbResult = await database.upgrade(logger); logger.error("Both classic and application commands are disabled! Please enable at least one command type in config/commands.json.");
if (dbResult === 1) return process.exit(1); return process.exit(1);
} }
// process the threshold into bytes early if (database) {
if (process.env.TEMPDIR && process.env.THRESHOLD) { // database handling
await parseThreshold(); const dbResult = await database.upgrade(logger);
} if (dbResult === 1) return process.exit(1);
}
// register commands and their info
logger.log("info", "Attempting to load commands..."); // process the threshold into bytes early
for await (const commandFile of getFiles(resolve(dirname(fileURLToPath(import.meta.url)), "./commands/"))) { if (process.env.TEMPDIR && process.env.THRESHOLD) {
logger.log("main", `Loading command from ${commandFile}...`); await parseThreshold();
try { }
await load(null, commandFile);
} catch (e) { // register commands and their info
logger.error(`Failed to register command from ${commandFile}: ${e}`); logger.log("info", "Attempting to load commands...");
} for await (const commandFile of getFiles(resolve(dirname(fileURLToPath(import.meta.url)), "./commands/"))) {
} logger.log("main", `Loading command from ${commandFile}...`);
logger.log("info", "Finished loading commands."); try {
await load(null, commandFile);
if (database) { } catch (e) {
await database.setup(); logger.error(`Failed to register command from ${commandFile}: ${e}`);
} }
if (process.env.API_TYPE === "ws") await reloadImageConnections(); }
logger.log("info", "Finished loading commands.");
// create the oceanic client
const client = new Client({ if (database) {
auth: `Bot ${process.env.TOKEN}`, await database.setup();
allowedMentions: { }
everyone: false, if (process.env.API_TYPE === "ws") await reloadImageConnections();
roles: false,
users: true, // create the oceanic client
repliedUser: true // const client = new Client({
}, // auth: `Bot ${process.env.TOKEN}`,
gateway: { // allowedMentions: {
concurrency: "auto", // everyone: false,
maxShards: "auto", // roles: false,
shardIDs: process.env.SHARDS ? JSON.parse(process.env.SHARDS)[process.env.pm_id - 1] : null, // users: true,
presence: { // repliedUser: true
status: "idle", // },
activities: [{ // gateway: {
type: 0, // concurrency: "auto",
name: "Starting esmBot..." // maxShards: "auto",
}] // shardIDs: process.env.SHARDS ? JSON.parse(process.env.SHARDS)[process.env.pm_id - 1] : null,
}, // presence: {
intents // status: "idle",
}, // activities: [{
collectionLimits: { // type: 0,
messages: 50 // name: "Starting esmBot..."
} // }]
}); // },
// intents
// register events // },
logger.log("info", "Attempting to load events..."); // collectionLimits: {
for await (const file of getFiles(resolve(dirname(fileURLToPath(import.meta.url)), "./events/"))) { // messages: 50
logger.log("main", `Loading event from ${file}...`); // }
const eventArray = file.split("/"); // });
const eventName = eventArray[eventArray.length - 1].split(".")[0];
if (eventName === "interactionCreate" && !types.application) { const myUserId = process.env.MATRIX_USERNAME;
logger.log("warn", `Skipped loading event from ${file} because application commands are disabled`); const myAccessToken = process.env.TOKEN;
continue; const matrixClient = sdk.createClient({
} baseUrl: process.env.MATRIX_BASEURL,
const { default: event } = await import(file); accessToken: myAccessToken,
client.on(eventName, event.bind(null, client)); userId: myUserId,
} });
logger.log("info", "Finished loading events.");
// PM2-specific handling // register events
if (process.env.PM2_USAGE) { logger.log("info", "Attempting to load events...");
// callback hell :) // for await (const file of getFiles(resolve(dirname(fileURLToPath(import.meta.url)), "./events/"))) {
pm2.launchBus((err, pm2Bus) => { // logger.log("main", `Loading event from ${file}...`);
if (err) { // const eventArray = file.split("/");
logger.error(err); // const eventName = eventArray[eventArray.length - 1].split(".")[0];
return; // if (eventName === "interactionCreate" && !types.application) {
} // logger.log("warn", `Skipped loading event from ${file} because application commands are disabled`);
pm2.list((err, list) => { // continue;
if (err) { // }
logger.error(err); // const { default: event } = await import(file);
return; // // client.on(eventName, event.bind(null, client));
} // }
const managerProc = list.filter((v) => v.name === "esmBot-manager")[0]; const { default: event } = await import("./events/roommessage.js");
pm2Bus.on("process:msg", async (packet) => { matrixClient.on("Room.timeline", event.bind(null,matrixClient));
switch (packet.data?.type) { logger.log("info", "Finished loading events.");
case "reload":
var path = paths.get(packet.data.message); matrixClient.startClient({ initialSyncLimit: 0 }).then(() => logger.log("info", "Client started!"));
await load(client, path, true); }
break;
case "soundreload":
await reload(client);
break;
case "imagereload":
await reloadImageConnections();
break;
case "broadcastStart":
startBroadcast(client, packet.data.message);
break;
case "broadcastEnd":
endBroadcast(client);
break;
case "serverCounts":
pm2.sendDataToProcessId(managerProc.pm_id, {
id: managerProc.pm_id,
type: "process:msg",
data: {
type: "serverCounts",
guilds: client.guilds.size,
shards: client.shards.size
},
topic: true
}, (err) => {
if (err) logger.error(err);
});
break;
}
});
});
});
}
// connect to lavalink
if (!connected) connect(client);
client.connect();
}
init(); init();

View File

@ -1,72 +1,72 @@
server: # REST and WS server server: # REST and WS server
port: 2333 port: 2333
address: 0.0.0.0 address: 0.0.0.0
lavalink: lavalink:
server: server:
password: "youshallnotpass" password: "youshallnotpass"
sources: sources:
youtube: true youtube: true
bandcamp: true bandcamp: true
soundcloud: true soundcloud: true
twitch: true twitch: true
vimeo: true vimeo: true
mixer: true mixer: true
http: true http: true
local: true local: true
bufferDurationMs: 400 bufferDurationMs: 400
youtubePlaylistLoadLimit: 6 # Number of pages at 100 each youtubePlaylistLoadLimit: 6 # Number of pages at 100 each
playerUpdateInterval: 1 playerUpdateInterval: 1
youtubeSearchEnabled: true youtubeSearchEnabled: true
soundcloudSearchEnabled: true soundcloudSearchEnabled: true
gc-warnings: true gc-warnings: true
#ratelimit: #ratelimit:
#ipBlocks: ["1.0.0.0/8", "..."] # list of ip blocks #ipBlocks: ["1.0.0.0/8", "..."] # list of ip blocks
#excludedIps: ["...", "..."] # ips which should be explicit excluded from usage by lavalink #excludedIps: ["...", "..."] # ips which should be explicit excluded from usage by lavalink
#strategy: "RotateOnBan" # RotateOnBan | LoadBalance | NanoSwitch | RotatingNanoSwitch #strategy: "RotateOnBan" # RotateOnBan | LoadBalance | NanoSwitch | RotatingNanoSwitch
#searchTriggersFail: true # Whether a search 429 should trigger marking the ip as failing #searchTriggersFail: true # Whether a search 429 should trigger marking the ip as failing
#retryLimit: -1 # -1 = use default lavaplayer value | 0 = infinity | >0 = retry will happen this numbers times #retryLimit: -1 # -1 = use default lavaplayer value | 0 = infinity | >0 = retry will happen this numbers times
plugins: plugins:
- dependency: "com.github.esmBot:lava-xm-plugin:v0.2.1" - dependency: "com.github.esmBot:lava-xm-plugin:v0.2.1"
repository: "https://jitpack.io" repository: "https://jitpack.io"
- dependency: "com.github.TopiSenpai.LavaSrc:lavasrc-plugin:3.2.0" - dependency: "com.github.TopiSenpai.LavaSrc:lavasrc-plugin:3.2.0"
repository: "https://jitpack.io" repository: "https://jitpack.io"
plugins: plugins:
lavasrc: lavasrc:
providers: providers:
- "ytsearch:\"%ISRC%\"" - "ytsearch:\"%ISRC%\""
- "ytsearch:%QUERY%" - "ytsearch:%QUERY%"
sources: sources:
spotify: false spotify: false
applemusic: true applemusic: true
deezer: false deezer: false
spotify: spotify:
clientId: "your client id" clientId: "your client id"
clientSecret: "your client secret" clientSecret: "your client secret"
countryCode: "US" countryCode: "US"
applemusic: applemusic:
countryCode: "US" countryCode: "US"
deezer: deezer:
masterDecryptionKey: "go looking for this somewhere" masterDecryptionKey: "go looking for this somewhere"
metrics: metrics:
prometheus: prometheus:
enabled: false enabled: false
endpoint: /metrics endpoint: /metrics
sentry: sentry:
dsn: "" dsn: ""
# tags: # tags:
# some_key: some_value # some_key: some_value
# another_key: another_value # another_key: another_value
logging: logging:
file: file:
max-history: 30 max-history: 30
max-size: 1GB max-size: 1GB
path: ./logs/ path: ./logs/
level: level:
root: INFO root: INFO
lavalink: INFO lavalink: INFO

View File

@ -1,71 +1,71 @@
class Command { class Command {
success = true; success = true;
constructor(client, options) { constructor(matrixClient, options) {
this.client = client; this.client = matrixClient;
this.origOptions = options; this.origOptions = options;
this.type = options.type; this.type = options.type;
this.args = options.args; this.args = options.args;
if (options.type === "classic") { if (options.type === "classic") {
this.message = options.message; this.message = options.message;
this.channel = options.message.channel; this.channel = options.message.room_id;
this.guild = options.message.guild; this.guild = options.message.guild;
this.author = options.message.author; this.author = options.message.sender;
this.member = options.message.member; this.member = options.message.member;
this.content = options.content; this.content = options.content;
this.options = options.specialArgs; this.options = options.specialArgs;
this.reference = { this.reference = {
messageReference: { messageReference: {
channelID: this.message.channelID, channelID: this.message.channelID,
messageID: this.message.id, messageID: this.message.id,
guildID: this.message.guildID ?? undefined, guildID: this.message.guildID ?? undefined,
failIfNotExists: false failIfNotExists: false
}, },
allowedMentions: { allowedMentions: {
repliedUser: false repliedUser: false
} }
}; };
} else if (options.type === "application") { } else if (options.type === "application") {
this.interaction = options.interaction; this.interaction = options.interaction;
this.args = []; this.args = [];
this.channel = options.interaction.channel; this.channel = options.interaction.channel;
this.guild = options.interaction.guild; this.guild = options.interaction.guild;
this.author = this.member = options.interaction.guildID ? options.interaction.member : options.interaction.user; this.author = this.member = options.interaction.guildID ? options.interaction.member : options.interaction.user;
if (options.interaction.data.options) { if (options.interaction.data.options) {
this.options = options.interaction.data.options.raw.reduce((obj, item) => { this.options = options.interaction.data.options.raw.reduce((obj, item) => {
obj[item.name] = item.value; obj[item.name] = item.value;
return obj; return obj;
}, {}); }, {});
this.optionsArray = options.interaction.data.options.raw; this.optionsArray = options.interaction.data.options.raw;
} else { } else {
this.options = {}; this.options = {};
} }
} }
} }
async run() { async run() {
return "It works!"; return "It works!";
} }
async acknowledge() { async acknowledge() {
if (this.type === "classic") { if (this.type === "classic") {
const channel = this.channel ?? await this.client.rest.channels.get(this.message.channelID); const channel = this.channel;
await channel.sendTyping(); await this.client.sendTyping(channel, true, 5);
} else if (!this.interaction.acknowledged) { } else if (!this.interaction.acknowledged) {
await this.interaction.defer(); await this.interaction.defer();
} }
} }
static init() { static init() {
return this; return this;
} }
static description = "No description found"; static description = "No description found";
static aliases = []; static aliases = [];
static arguments = []; static arguments = [];
static flags = []; static flags = [];
static slashAllowed = true; static slashAllowed = true;
static directAllowed = true; static directAllowed = true;
static adminOnly = false; static adminOnly = false;
} }
export default Command; export default Command;

View File

@ -1,153 +1,160 @@
import Command from "./command.js"; import Command from "./command.js";
import imageDetect from "../utils/imagedetect.js"; import imageDetect from "../utils/imagedetect.js";
import { runImageJob } from "../utils/image.js"; import { runImageJob } from "../utils/image.js";
import { runningCommands } from "../utils/collections.js"; import { runningCommands } from "../utils/collections.js";
import { readFileSync } from "fs"; import { readFileSync } from "fs";
const { emotes } = JSON.parse(readFileSync(new URL("../config/messages.json", import.meta.url))); const { emotes } = JSON.parse(readFileSync(new URL("../config/messages.json", import.meta.url)));
import { random } from "../utils/misc.js"; import { random } from "../utils/misc.js";
import { selectedImages } from "../utils/collections.js"; import { selectedImages } from "../utils/collections.js";
class ImageCommand extends Command { class ImageCommand extends Command {
async criteria() { async criteria() {
return true; return true;
} }
async run() { async run() {
this.success = false; this.success = false;
const timestamp = this.type === "classic" ? this.message.createdAt : Math.floor((this.interaction.id / 4194304) + 1420070400000); const timestamp = this.type === "classic" ? this.message.createdAt : Math.floor((this.interaction.id / 4194304) + 1420070400000);
// check if this command has already been run in this channel with the same arguments, and we are awaiting its result // check if this command has already been run in this channel with the same arguments, and we are awaiting its result
// if so, don't re-run it // if so, don't re-run it
if (runningCommands.has(this.author.id) && (new Date(runningCommands.get(this.author.id)) - new Date(timestamp)) < 5000) { if (runningCommands.has(this.author) && (new Date(runningCommands.get(this.author)) - new Date(timestamp)) < 5000) {
return "Please slow down a bit."; return "Please slow down a bit.";
} }
// before awaiting the command result, add this command to the set of running commands // before awaiting the command result, add this command to the set of running commands
runningCommands.set(this.author.id, timestamp); runningCommands.set(this.author, timestamp);
const imageParams = { const imageParams = {
cmd: this.constructor.command, cmd: this.constructor.command,
params: { params: {
togif: !!this.options.togif togif: !!this.options.togif
}, },
id: (this.interaction ?? this.message).id id: this.message.event_id
}; };
if (this.type === "application") await this.acknowledge(); // if (this.type === "application") await this.acknowledge();
if (this.constructor.requiresImage) { if (this.constructor.requiresImage) {
try { try {
const selection = selectedImages.get(this.author.id); const selection = selectedImages.get(this.author);
const image = selection ?? await imageDetect(this.client, this.message, this.interaction, this.options, true); const image = await imageDetect(this.client, this.message, this.interaction, this.options, true);
if (selection) selectedImages.delete(this.author.id); if (selection) selectedImages.delete(this.author);
if (image === undefined) { if (image === undefined) {
runningCommands.delete(this.author.id); runningCommands.delete(this.author);
return `${this.constructor.noImage} (Tip: try right-clicking/holding on a message and press Apps -> Select Image, then try again.)`; return `${this.constructor.noImage} (Tip: try right-clicking/holding on a message and press Apps -> Select Image, then try again.)`;
} else if (image.type === "large") { } else if (image.type === "large") {
runningCommands.delete(this.author.id); runningCommands.delete(this.author);
return "That image is too large (>= 25MB)! Try using a smaller image."; return "That image is too large (>= 25MB)! Try using a smaller image.";
} else if (image.type === "tenorlimit") { } else if (image.type === "tenorlimit") {
runningCommands.delete(this.author.id); runningCommands.delete(this.author);
return "I've been rate-limited by Tenor. Please try uploading your GIF elsewhere."; return "I've been rate-limited by Tenor. Please try uploading your GIF elsewhere.";
} }
imageParams.path = image.path; imageParams.path = image.path;
imageParams.params.type = image.type; imageParams.params.type = image.type;
imageParams.url = image.url; // technically not required but can be useful for text filtering imageParams.url = image.url; // technically not required but can be useful for text filtering
imageParams.name = image.name; imageParams.name = image.name;
if (this.constructor.requiresGIF) imageParams.onlyGIF = true; if (this.constructor.requiresGIF) imageParams.onlyGIF = true;
} catch (e) { } catch (e) {
runningCommands.delete(this.author.id); runningCommands.delete(this.author);
throw e; throw e;
} }
} }
if (this.constructor.requiresText) { if (this.constructor.requiresText) {
const text = this.options.text ?? this.args.join(" ").trim(); const text = this.options.text ?? this.args.join(" ").trim();
if (text.length === 0 || !await this.criteria(text, imageParams.url)) { if (text.length === 0 || !await this.criteria(text, imageParams.url)) {
runningCommands.delete(this.author.id); runningCommands.delete(this.author);
return this.constructor.noText; return this.constructor.noText;
} }
} }
if (typeof this.params === "function") { if (typeof this.params === "function") {
Object.assign(imageParams.params, this.params(imageParams.url, imageParams.name)); Object.assign(imageParams.params, this.params(imageParams.url, imageParams.name));
} else if (typeof this.params === "object") { } else if (typeof this.params === "object") {
Object.assign(imageParams.params, this.params); Object.assign(imageParams.params, this.params);
} }
let status; let status;
if (imageParams.params.type === "image/gif" && this.type === "classic") { if (imageParams.params.type === "image/gif" && this.type === "classic") {
status = await this.processMessage(this.message.channel ?? await this.client.rest.channels.get(this.message.channelID)); status = await this.processMessage(this.message.room_id ?? await this.client.rest.channels.get(this.message.room_id));
} }
try { try {
const { buffer, type } = await runImageJob(imageParams); const { buffer, type } = await runImageJob(imageParams);
if (type === "nogif" && this.constructor.requiresGIF) { if (type === "nogif" && this.constructor.requiresGIF) {
return "That isn't a GIF!"; return "That isn't a GIF!";
} }
this.success = true; this.success = true;
return { return {
contents: buffer, contents: buffer,
name: `${this.constructor.command}.${type}` name: `${this.constructor.command}.${type}`
}; };
} catch (e) { } catch (e) {
if (e === "Request ended prematurely due to a closed connection") return "This image job couldn't be completed because the server it was running on went down. Try running your command again."; if (e === "Request ended prematurely due to a closed connection") return "This image job couldn't be completed because the server it was running on went down. Try running your command again.";
if (e === "Job timed out" || e === "Timeout") return "The image is taking too long to process (>=15 minutes), so the job was cancelled. Try using a smaller image."; if (e === "Job timed out" || e === "Timeout") return "The image is taking too long to process (>=15 minutes), so the job was cancelled. Try using a smaller image.";
if (e === "No available servers") return "I can't seem to contact the image servers, they might be down or still trying to start up. Please wait a little bit."; if (e === "No available servers") return "I can't seem to contact the image servers, they might be down or still trying to start up. Please wait a little bit.";
throw e; throw e;
} finally { } finally {
try { try {
if (status) await status.delete(); if (status) await status.delete();
} catch { } catch {
// no-op // no-op
} }
runningCommands.delete(this.author.id); runningCommands.delete(this.author);
} }
} }
processMessage(channel) { processMessage(channel) {
return channel.createMessage({ this.client.send
content: `${random(emotes) || process.env.PROCESSING_EMOJI || "<a:processing:479351417102925854>"} Processing... This might take a while` const content = {
}); body: "Processing... This might take a while",
} msgtype: "m.text",
};
static init() { this.client.sendEvent(channel, "m.room.message", content, "", (err, res) => {
this.flags = []; // console.log(res)
if (this.requiresText || this.textOptional) { logger.log("error", err)
this.flags.push({ return res
name: "text", });
type: 3, }
description: "The text to put on the image",
required: !this.textOptional static init() {
}); this.flags = [];
} if (this.requiresText || this.textOptional) {
if (this.requiresImage) { this.flags.push({
this.flags.push({ name: "text",
name: "image", type: 3,
type: 11, description: "The text to put on the image",
description: "An image/GIF attachment" required: !this.textOptional
}, { });
name: "link", }
type: 3, if (this.requiresImage) {
description: "An image/GIF URL" this.flags.push({
}); name: "image",
} type: 11,
this.flags.push({ description: "An image/GIF attachment"
name: "togif", }, {
type: 5, name: "link",
description: "Force GIF output" type: 3,
}); description: "An image/GIF URL"
return this; });
} }
this.flags.push({
static allowedFonts = ["futura", "impact", "helvetica", "arial", "roboto", "noto", "times", "comic sans ms"]; name: "togif",
type: 5,
static requiresImage = true; description: "Force GIF output"
static requiresText = false; });
static textOptional = false; return this;
static requiresGIF = false; }
static noImage = "You need to provide an image/GIF!";
static noText = "You need to provide some text!"; static allowedFonts = ["futura", "impact", "helvetica", "arial", "roboto", "noto", "times", "comic sans ms"];
static command = "";
} static requiresImage = true;
static requiresText = false;
export default ImageCommand; static textOptional = false;
static requiresGIF = false;
static noImage = "You need to provide an image/GIF!";
static noText = "You need to provide some text!";
static command = "";
}
export default ImageCommand;

View File

@ -1,17 +1,17 @@
import Command from "./command.js"; import Command from "./command.js";
import { players, queues } from "../utils/soundplayer.js"; import { players, queues } from "../utils/soundplayer.js";
class MusicCommand extends Command { class MusicCommand extends Command {
constructor(client, options) { constructor(client, options) {
super(client, options); super(client, options);
if (this.guild) { if (this.guild) {
this.connection = players.get(this.guild.id); this.connection = players.get(this.guild.id);
this.queue = queues.get(this.guild.id); this.queue = queues.get(this.guild.id);
} }
} }
static slashAllowed = false; static slashAllowed = false;
static directAllowed = false; static directAllowed = false;
} }
export default MusicCommand; export default MusicCommand;

View File

@ -1,14 +1,14 @@
import Command from "./command.js"; import Command from "./command.js";
import { play } from "../utils/soundplayer.js"; import { play } from "../utils/soundplayer.js";
// only exists to sort the various soundboard commands // only exists to sort the various soundboard commands
class SoundboardCommand extends Command { class SoundboardCommand extends Command {
async run() { async run() {
return play(this.client, this.constructor.file, { channel: this.channel, author: this.author, member: this.member, type: this.type, interaction: this.interaction }); return play(this.client, this.constructor.file, { channel: this.channel, author: this.author, member: this.member, type: this.type, interaction: this.interaction });
} }
static slashAllowed = false; static slashAllowed = false;
static directAllowed = false; static directAllowed = false;
} }
export default SoundboardCommand; export default SoundboardCommand;

View File

@ -1,43 +1,43 @@
import Command from "../../classes/command.js"; import Command from "../../classes/command.js";
import { random } from "../../utils/misc.js"; import { random } from "../../utils/misc.js";
class EightBallCommand extends Command { class EightBallCommand extends Command {
static responses = [ static responses = [
"It is certain", "It is certain",
"It is decidedly so", "It is decidedly so",
"Without a doubt", "Without a doubt",
"Yes, definitely", "Yes, definitely",
"You may rely on it", "You may rely on it",
"As I see it, yes", "As I see it, yes",
"Most likely", "Most likely",
"Outlook good", "Outlook good",
"Yes", "Yes",
"Signs point to yes", "Signs point to yes",
"Reply hazy, try again", "Reply hazy, try again",
"Ask again later", "Ask again later",
"Better not tell you now", "Better not tell you now",
"Cannot predict now", "Cannot predict now",
"Concentrate and ask again", "Concentrate and ask again",
"Don't count on it", "Don't count on it",
"My reply is no", "My reply is no",
"My sources say no", "My sources say no",
"Outlook not so good", "Outlook not so good",
"Very doubtful" "Very doubtful"
]; ];
async run() { async run() {
return `🎱 ${random(EightBallCommand.responses)}`; return `🎱 ${random(EightBallCommand.responses)}`;
} }
static flags = [{ static flags = [{
name: "question", name: "question",
type: 3, type: 3,
description: "A question you want to ask the ball" description: "A question you want to ask the ball"
}]; }];
static description = "Asks the magic 8-ball a question"; static description = "Asks the magic 8-ball a question";
static aliases = ["magicball", "magikball", "magic8ball", "magik8ball", "eightball"]; static aliases = ["magicball", "magikball", "magic8ball", "magik8ball", "eightball"];
static arguments = ["{text}"]; static arguments = ["{text}"];
} }
export default EightBallCommand; export default EightBallCommand;

View File

@ -1,27 +1,27 @@
import { request } from "undici"; import { request } from "undici";
import Command from "../../classes/command.js"; import Command from "../../classes/command.js";
class AncientCommand extends Command { class AncientCommand extends Command {
async run() { async run() {
await this.acknowledge(); await this.acknowledge();
const controller = new AbortController(); const controller = new AbortController();
const timeout = setTimeout(() => { const timeout = setTimeout(() => {
controller.abort(); controller.abort();
}, 15000); }, 15000);
try { try {
const data = await request("https://files.projectlounge.pw/meme/", { method: "HEAD", signal: controller.signal }); const data = await request("https://files.projectlounge.pw/meme/", { method: "HEAD", signal: controller.signal });
clearTimeout(timeout); clearTimeout(timeout);
return `https://files.projectlounge.pw${data.headers.location}`; return `https://files.projectlounge.pw${data.headers.location}`;
} catch (e) { } catch (e) {
if (e.name === "AbortError") { if (e.name === "AbortError") {
this.success = false; this.success = false;
return "I couldn't get a meme in time. Maybe try again?"; return "I couldn't get a meme in time. Maybe try again?";
} }
} }
} }
static description = "Gets a random ancient meme"; static description = "Gets a random ancient meme";
static aliases = ["old", "oldmeme", "badmeme"]; static aliases = ["old", "oldmeme", "badmeme"];
} }
export default AncientCommand; export default AncientCommand;

View File

@ -1,28 +1,28 @@
import { request } from "undici"; import { request } from "undici";
import Command from "../../classes/command.js"; import Command from "../../classes/command.js";
class BirdCommand extends Command { class BirdCommand extends Command {
async run() { async run() {
await this.acknowledge(); await this.acknowledge();
const controller = new AbortController(); const controller = new AbortController();
const timeout = setTimeout(() => { const timeout = setTimeout(() => {
controller.abort(); controller.abort();
}, 15000); }, 15000);
try { try {
const imageData = await request("http://shibe.online/api/birds", { signal: controller.signal }); const imageData = await request("http://shibe.online/api/birds", { signal: controller.signal });
clearTimeout(timeout); clearTimeout(timeout);
const json = await imageData.body.json(); const json = await imageData.body.json();
return json[0]; return json[0];
} catch (e) { } catch (e) {
if (e.name === "AbortError") { if (e.name === "AbortError") {
this.success = false; this.success = false;
return "I couldn't get a bird image in time. Maybe try again?"; return "I couldn't get a bird image in time. Maybe try again?";
} }
} }
} }
static description = "Gets a random bird picture"; static description = "Gets a random bird picture";
static aliases = ["birb", "birds", "birbs"]; static aliases = ["birb", "birds", "birbs"];
} }
export default BirdCommand; export default BirdCommand;

View File

@ -1,27 +1,27 @@
import { request } from "undici"; import { request } from "undici";
import Command from "../../classes/command.js"; import Command from "../../classes/command.js";
class CatCommand extends Command { class CatCommand extends Command {
async run() { async run() {
await this.acknowledge(); await this.acknowledge();
const controller = new AbortController(); const controller = new AbortController();
const timeout = setTimeout(() => { const timeout = setTimeout(() => {
controller.abort(); controller.abort();
}, 15000); }, 15000);
try { try {
const data = await request("https://files.projectlounge.pw/cta/", { method: "HEAD", signal: controller.signal }); const data = await request("https://files.projectlounge.pw/cta/", { method: "HEAD", signal: controller.signal });
clearTimeout(timeout); clearTimeout(timeout);
return `https://files.projectlounge.pw${data.headers.location}`; return `https://files.projectlounge.pw${data.headers.location}`;
} catch (e) { } catch (e) {
if (e.name === "AbortError") { if (e.name === "AbortError") {
this.success = false; this.success = false;
return "I couldn't get a cat image in time. Maybe try again?"; return "I couldn't get a cat image in time. Maybe try again?";
} }
} }
} }
static description = "Gets a random cat picture"; static description = "Gets a random cat picture";
static aliases = ["kitters", "kitties", "kitty", "cattos", "catto", "cats", "cta"]; static aliases = ["kitters", "kitties", "kitty", "cattos", "catto", "cats", "cta"];
} }
export default CatCommand; export default CatCommand;

View File

@ -1,25 +1,25 @@
import Command from "../../classes/command.js"; import Command from "../../classes/command.js";
class DiceCommand extends Command { class DiceCommand extends Command {
async run() { async run() {
const max = this.options.max ?? parseInt(this.args[0]); const max = this.options.max ?? parseInt(this.args[0]);
if (!max) { if (!max) {
return `🎲 The dice landed on ${Math.floor(Math.random() * 6) + 1}.`; return `🎲 The dice landed on ${Math.floor(Math.random() * 6) + 1}.`;
} else { } else {
return `🎲 The dice landed on ${Math.floor(Math.random() * max) + 1}.`; return `🎲 The dice landed on ${Math.floor(Math.random() * max) + 1}.`;
} }
} }
static flags = [{ static flags = [{
name: "max", name: "max",
type: 4, type: 4,
description: "The maximum dice value", description: "The maximum dice value",
min_value: 1 min_value: 1
}]; }];
static description = "Rolls the dice"; static description = "Rolls the dice";
static aliases = ["roll", "die", "rng", "random"]; static aliases = ["roll", "die", "rng", "random"];
static arguments = ["{number}"]; static arguments = ["{number}"];
} }
export default DiceCommand; export default DiceCommand;

View File

@ -1,28 +1,28 @@
import { request } from "undici"; import { request } from "undici";
import Command from "../../classes/command.js"; import Command from "../../classes/command.js";
class DogCommand extends Command { class DogCommand extends Command {
async run() { async run() {
await this.acknowledge(); await this.acknowledge();
const controller = new AbortController(); const controller = new AbortController();
const timeout = setTimeout(() => { const timeout = setTimeout(() => {
controller.abort(); controller.abort();
}, 15000); }, 15000);
try { try {
const imageData = await request("https://dog.ceo/api/breeds/image/random", { signal: controller.signal }); const imageData = await request("https://dog.ceo/api/breeds/image/random", { signal: controller.signal });
clearTimeout(timeout); clearTimeout(timeout);
const json = await imageData.body.json(); const json = await imageData.body.json();
return json.message; return json.message;
} catch (e) { } catch (e) {
if (e.name === "AbortError") { if (e.name === "AbortError") {
this.success = false; this.success = false;
return "I couldn't get a dog image in time. Maybe try again?"; return "I couldn't get a dog image in time. Maybe try again?";
} }
} }
} }
static description = "Gets a random dog picture"; static description = "Gets a random dog picture";
static aliases = ["doggos", "doggo", "pupper", "puppers", "dogs", "puppy", "puppies", "pups", "pup"]; static aliases = ["doggos", "doggo", "pupper", "puppers", "dogs", "puppy", "puppies", "pups", "pup"];
} }
export default DogCommand; export default DogCommand;

View File

@ -1,20 +1,20 @@
import ImageCommand from "../../classes/imageCommand.js"; import ImageCommand from "../../classes/imageCommand.js";
class HomebrewCommand extends ImageCommand { class HomebrewCommand extends ImageCommand {
params() { params() {
return { return {
caption: (this.options.text ?? this.args.join(" ")).toLowerCase().replaceAll("\n", " ") caption: (this.options.text ?? this.args.join(" ")).toLowerCase().replaceAll("\n", " ")
}; };
} }
static description = "Creates a Homebrew Channel edit"; static description = "Creates a Homebrew Channel edit";
static aliases = ["hbc", "brew", "wiibrew"]; static aliases = ["hbc", "brew", "wiibrew"];
static arguments = ["[text]"]; static arguments = ["[text]"];
static requiresImage = false; static requiresImage = false;
static requiresText = true; static requiresText = true;
static noText = "You need to provide some text to make a Homebrew Channel edit!"; static noText = "You need to provide some text to make a Homebrew Channel edit!";
static command = "homebrew"; static command = "homebrew";
} }
export default HomebrewCommand; export default HomebrewCommand;

View File

@ -1,22 +1,22 @@
//import wrap from "../../utils/wrap.js"; //import wrap from "../../utils/wrap.js";
import ImageCommand from "../../classes/imageCommand.js"; import ImageCommand from "../../classes/imageCommand.js";
import { cleanMessage } from "../../utils/misc.js"; import { cleanMessage } from "../../utils/misc.js";
class SonicCommand extends ImageCommand { class SonicCommand extends ImageCommand {
params() { params() {
const cleanedMessage = cleanMessage(this.message ?? this.interaction, this.options.text ?? this.args.join(" ")); const cleanedMessage = cleanMessage(this.message ?? this.interaction, this.options.text ?? this.args.join(" "));
return { return {
text: cleanedMessage text: cleanedMessage
}; };
} }
static description = "Creates a Sonic speech bubble image"; static description = "Creates a Sonic speech bubble image";
static arguments = ["[text]"]; static arguments = ["[text]"];
static requiresImage = false; static requiresImage = false;
static requiresText = true; static requiresText = true;
static noText = "You need to provide some text to make a Sonic meme!"; static noText = "You need to provide some text to make a Sonic meme!";
static command = "sonic"; static command = "sonic";
} }
export default SonicCommand; export default SonicCommand;

View File

@ -1,34 +1,34 @@
import { request } from "undici"; import { request } from "undici";
import Command from "../../classes/command.js"; import Command from "../../classes/command.js";
class WikihowCommand extends Command { class WikihowCommand extends Command {
async run() { async run() {
await this.acknowledge(); await this.acknowledge();
const controller = new AbortController(); const controller = new AbortController();
const timeout = setTimeout(() => { const timeout = setTimeout(() => {
controller.abort(); controller.abort();
}, 15000); }, 15000);
try { try {
const req = await request("https://www.wikihow.com/api.php?action=query&generator=random&prop=imageinfo&format=json&iiprop=url&grnnamespace=6", { signal: controller.signal }); const req = await request("https://www.wikihow.com/api.php?action=query&generator=random&prop=imageinfo&format=json&iiprop=url&grnnamespace=6", { signal: controller.signal });
clearTimeout(timeout); clearTimeout(timeout);
const json = await req.body.json(); const json = await req.body.json();
const id = Object.keys(json.query.pages)[0]; const id = Object.keys(json.query.pages)[0];
const data = json.query.pages[id]; const data = json.query.pages[id];
if (data.imageinfo) { if (data.imageinfo) {
return json.query.pages[id].imageinfo[0].url; return json.query.pages[id].imageinfo[0].url;
} else { } else {
return await this.run(); return await this.run();
} }
} catch (e) { } catch (e) {
if (e.name === "AbortError") { if (e.name === "AbortError") {
this.success = false; this.success = false;
return "I couldn't get a WikiHow image in time. Maybe try again?"; return "I couldn't get a WikiHow image in time. Maybe try again?";
} }
} }
} }
static description = "Gets a random WikiHow image"; static description = "Gets a random WikiHow image";
static aliases = ["wiki"]; static aliases = ["wiki"];
} }
export default WikihowCommand; export default WikihowCommand;

View File

@ -1,53 +1,53 @@
import Command from "../../classes/command.js"; import Command from "../../classes/command.js";
const mentionRegex = /^<?[@#]?[&!]?(\d+)>?$/; const mentionRegex = /^<?[@#]?[&!]?(\d+)>?$/;
class AvatarCommand extends Command { class AvatarCommand extends Command {
async run() { async run() {
const member = this.options.member ?? this.args[0]; const member = this.options.member ?? this.args[0];
const self = this.client.users.get(this.author.id) ?? await this.client.rest.users.get(this.author.id); const self = this.client.users.get(this.author.id) ?? await this.client.rest.users.get(this.author.id);
if (this.type === "classic" && this.message.mentions.users[0]) { if (this.type === "classic" && this.message.mentions.users[0]) {
return this.message.mentions.users[0].avatarURL(null, 512); return this.message.mentions.users[0].avatarURL(null, 512);
} else if (member && member > 21154535154122752n) { } else if (member && member > 21154535154122752n) {
const user = this.client.users.get(member) ?? await this.client.rest.users.get(member); const user = this.client.users.get(member) ?? await this.client.rest.users.get(member);
if (user) { if (user) {
return user.avatarURL(null, 512); return user.avatarURL(null, 512);
} else if (mentionRegex.test(member)) { } else if (mentionRegex.test(member)) {
const id = member.match(mentionRegex)[1]; const id = member.match(mentionRegex)[1];
if (id < 21154535154122752n) { if (id < 21154535154122752n) {
this.success = false; this.success = false;
return "That's not a valid mention!"; return "That's not a valid mention!";
} }
try { try {
const user = this.client.users.get(id) ?? await this.client.rest.users.get(id); const user = this.client.users.get(id) ?? await this.client.rest.users.get(id);
return user.avatarURL(null, 512); return user.avatarURL(null, 512);
} catch { } catch {
return self.avatarURL(null, 512); return self.avatarURL(null, 512);
} }
} else { } else {
return self.avatarURL(null, 512); return self.avatarURL(null, 512);
} }
} else if (this.args.join(" ") !== "" && this.guild) { } else if (this.args.join(" ") !== "" && this.guild) {
const searched = await this.guild.searchMembers({ const searched = await this.guild.searchMembers({
query: this.args.join(" "), query: this.args.join(" "),
limit: 1 limit: 1
}); });
if (searched.length === 0) return self.avatarURL(null, 512); if (searched.length === 0) return self.avatarURL(null, 512);
const user = this.client.users.get(searched[0].user.id) ?? await this.client.rest.users.get(searched[0].user.id); const user = this.client.users.get(searched[0].user.id) ?? await this.client.rest.users.get(searched[0].user.id);
return user ? user.avatarURL(null, 512) : self.avatarURL(null, 512); return user ? user.avatarURL(null, 512) : self.avatarURL(null, 512);
} else { } else {
return self.avatarURL(null, 512); return self.avatarURL(null, 512);
} }
} }
static description = "Gets a user's avatar"; static description = "Gets a user's avatar";
static aliases = ["pfp", "ava"]; static aliases = ["pfp", "ava"];
static arguments = ["{mention/id}"]; static arguments = ["{mention/id}"];
static flags = [{ static flags = [{
name: "member", name: "member",
type: 6, type: 6,
description: "The member to get the avatar from", description: "The member to get the avatar from",
required: false required: false
}]; }];
} }
export default AvatarCommand; export default AvatarCommand;

View File

@ -1,55 +1,55 @@
import Command from "../../classes/command.js"; import Command from "../../classes/command.js";
import { Routes } from "oceanic.js"; import { Routes } from "oceanic.js";
const mentionRegex = /^<?[@#]?[&!]?(\d+)>?$/; const mentionRegex = /^<?[@#]?[&!]?(\d+)>?$/;
class BannerCommand extends Command { class BannerCommand extends Command {
// this command sucks // this command sucks
async run() { async run() {
const member = this.options.member ?? this.args[0]; const member = this.options.member ?? this.args[0];
const self = await this.client.rest.users.get(this.author.id); // banners are only available over REST const self = await this.client.rest.users.get(this.author.id); // banners are only available over REST
if (this.type === "classic" && this.message.mentions.users[0] && this.message.mentions.users[0].banner) { if (this.type === "classic" && this.message.mentions.users[0] && this.message.mentions.users[0].banner) {
return this.client.util.formatImage(Routes.BANNER(this.message.mentions.users[0].id, this.message.mentions.users[0].banner), null, 512); return this.client.util.formatImage(Routes.BANNER(this.message.mentions.users[0].id, this.message.mentions.users[0].banner), null, 512);
} else if (member && member > 21154535154122752n) { } else if (member && member > 21154535154122752n) {
const user = await this.client.rest.users.get(member); const user = await this.client.rest.users.get(member);
if (user && user.banner) { if (user && user.banner) {
return this.client.util.formatImage(Routes.BANNER(user.id, user.banner), null, 512); return this.client.util.formatImage(Routes.BANNER(user.id, user.banner), null, 512);
} else if (mentionRegex.test(member)) { } else if (mentionRegex.test(member)) {
const id = member.match(mentionRegex)[1]; const id = member.match(mentionRegex)[1];
if (id < 21154535154122752n) { if (id < 21154535154122752n) {
this.success = false; this.success = false;
return "That's not a valid mention!"; return "That's not a valid mention!";
} }
try { try {
const user = await this.client.rest.users.get(id); const user = await this.client.rest.users.get(id);
return user.banner ? this.client.util.formatImage(Routes.BANNER(user.id, user.banner), null, 512) : "This user doesn't have a banner!"; return user.banner ? this.client.util.formatImage(Routes.BANNER(user.id, user.banner), null, 512) : "This user doesn't have a banner!";
} catch { } catch {
return self.banner ? this.client.util.formatImage(Routes.BANNER(self.id, self.banner), null, 512) : "You don't have a banner!"; return self.banner ? this.client.util.formatImage(Routes.BANNER(self.id, self.banner), null, 512) : "You don't have a banner!";
} }
} else { } else {
return "This user doesn't have a banner!"; return "This user doesn't have a banner!";
} }
} else if (this.args.join(" ") !== "" && this.guild) { } else if (this.args.join(" ") !== "" && this.guild) {
const searched = await this.guild.searchMembers({ const searched = await this.guild.searchMembers({
query: this.args.join(" "), query: this.args.join(" "),
limit: 1 limit: 1
}); });
if (searched.length === 0) return self.banner ? this.client.util.formatImage(Routes.BANNER(self.id, self.banner), null, 512) : "This user doesn't have a banner!"; if (searched.length === 0) return self.banner ? this.client.util.formatImage(Routes.BANNER(self.id, self.banner), null, 512) : "This user doesn't have a banner!";
const user = await this.client.rest.users.get(searched[0].user.id); const user = await this.client.rest.users.get(searched[0].user.id);
return user.banner ? this.client.util.formatImage(Routes.BANNER(user.id, user.banner), null, 512) : (self.banner ? this.client.util.formatImage(Routes.BANNER(self.id, self.banner), null, 512) : "This user doesn't have a banner!"); return user.banner ? this.client.util.formatImage(Routes.BANNER(user.id, user.banner), null, 512) : (self.banner ? this.client.util.formatImage(Routes.BANNER(self.id, self.banner), null, 512) : "This user doesn't have a banner!");
} else { } else {
return self.banner ? this.client.util.formatImage(Routes.BANNER(self.id, self.banner), null, 512) : "You don't have a banner!"; return self.banner ? this.client.util.formatImage(Routes.BANNER(self.id, self.banner), null, 512) : "You don't have a banner!";
} }
} }
static description = "Gets a user's banner"; static description = "Gets a user's banner";
static aliases = ["userbanner"]; static aliases = ["userbanner"];
static arguments = ["{mention/id}"]; static arguments = ["{mention/id}"];
static flags = [{ static flags = [{
name: "member", name: "member",
type: 6, type: 6,
description: "The member to get the banner from", description: "The member to get the banner from",
required: false required: false
}]; }];
} }
export default BannerCommand; export default BannerCommand;

View File

@ -1,48 +1,48 @@
import Command from "../../classes/command.js"; import Command from "../../classes/command.js";
import { clean } from "../../utils/misc.js"; import { clean } from "../../utils/misc.js";
class Base64Command extends Command { class Base64Command extends Command {
async run() { async run() {
this.success = false; this.success = false;
if (this.type === "classic" && this.args.length === 0) return "You need to provide whether you want to encode or decode the text!"; if (this.type === "classic" && this.args.length === 0) return "You need to provide whether you want to encode or decode the text!";
const command = this.type === "classic" ? this.args[0].toLowerCase() : this.optionsArray[0].name.toLowerCase(); const command = this.type === "classic" ? this.args[0].toLowerCase() : this.optionsArray[0].name.toLowerCase();
if (command !== "decode" && command !== "encode") return "You need to provide whether you want to encode or decode the text!"; if (command !== "decode" && command !== "encode") return "You need to provide whether you want to encode or decode the text!";
const string = this.options.text ?? this.args.slice(1).join(" "); const string = this.options.text ?? this.args.slice(1).join(" ");
if (!string || !string.trim()) return `You need to provide a string to ${command}!`; if (!string || !string.trim()) return `You need to provide a string to ${command}!`;
this.success = true; this.success = true;
if (command === "decode") { if (command === "decode") {
const b64Decoded = Buffer.from(string, "base64").toString("utf8"); const b64Decoded = Buffer.from(string, "base64").toString("utf8");
return `\`\`\`\n${await clean(b64Decoded)}\`\`\``; return `\`\`\`\n${await clean(b64Decoded)}\`\`\``;
} else if (command === "encode") { } else if (command === "encode") {
const b64Encoded = Buffer.from(string, "utf8").toString("base64"); const b64Encoded = Buffer.from(string, "utf8").toString("base64");
return `\`\`\`\n${b64Encoded}\`\`\``; return `\`\`\`\n${b64Encoded}\`\`\``;
} }
} }
static flags = [{ static flags = [{
name: "decode", name: "decode",
type: 1, type: 1,
description: "Decodes a Base64 string", description: "Decodes a Base64 string",
options: [{ options: [{
name: "text", name: "text",
type: 3, type: 3,
description: "The text to decode", description: "The text to decode",
required: true required: true
}] }]
}, { }, {
name: "encode", name: "encode",
type: 1, type: 1,
description: "Encodes a Base64 string", description: "Encodes a Base64 string",
options: [{ options: [{
name: "text", name: "text",
type: 3, type: 3,
description: "The text to encode", description: "The text to encode",
required: true required: true
}] }]
}]; }];
static description = "Encodes/decodes a Base64 string"; static description = "Encodes/decodes a Base64 string";
static arguments = ["[encode/decode]", "[text]"]; static arguments = ["[encode/decode]", "[text]"];
} }
export default Base64Command; export default Base64Command;

View File

@ -1,52 +1,52 @@
import Command from "../../classes/command.js"; import Command from "../../classes/command.js";
import database from "../../utils/database.js"; import database from "../../utils/database.js";
import { endBroadcast, startBroadcast } from "../../utils/misc.js"; import { endBroadcast, startBroadcast } from "../../utils/misc.js";
class BroadcastCommand extends Command { class BroadcastCommand extends Command {
async run() { async run() {
const owners = process.env.OWNER.split(","); const owners = process.env.OWNER.split(",");
if (!owners.includes(this.author.id)) { if (!owners.includes(this.author.id)) {
this.success = false; this.success = false;
return "Only the bot owner can broadcast messages!"; return "Only the bot owner can broadcast messages!";
} }
const message = this.options.message ?? this.args.join(" "); const message = this.options.message ?? this.args.join(" ");
if (message?.trim()) { if (message?.trim()) {
await database.setBroadcast(message); await database.setBroadcast(message);
startBroadcast(this.client, message); startBroadcast(this.client, message);
if (process.env.PM2_USAGE) { if (process.env.PM2_USAGE) {
process.send({ process.send({
type: "process:msg", type: "process:msg",
data: { data: {
type: "broadcastStart", type: "broadcastStart",
message message
} }
}); });
} }
return "Started broadcast."; return "Started broadcast.";
} else { } else {
await database.setBroadcast(null); await database.setBroadcast(null);
endBroadcast(this.client); endBroadcast(this.client);
if (process.env.PM2_USAGE) { if (process.env.PM2_USAGE) {
process.send({ process.send({
type: "process:msg", type: "process:msg",
data: { data: {
type: "broadcastEnd" type: "broadcastEnd"
} }
}); });
} }
return "Ended broadcast."; return "Ended broadcast.";
} }
} }
static flags = [{ static flags = [{
name: "message", name: "message",
type: 3, type: 3,
description: "The message to broadcast" description: "The message to broadcast"
}]; }];
static description = "Broadcasts a playing message until the command is run again or the bot restarts"; static description = "Broadcasts a playing message until the command is run again or the bot restarts";
static adminOnly = true; static adminOnly = true;
static dbRequired = true; static dbRequired = true;
} }
export default BroadcastCommand; export default BroadcastCommand;

View File

@ -1,53 +1,53 @@
import db from "../../utils/database.js"; import db from "../../utils/database.js";
import Command from "../../classes/command.js"; import Command from "../../classes/command.js";
class ChannelCommand extends Command { class ChannelCommand extends Command {
async run() { async run() {
this.success = false; this.success = false;
if (!this.guild) return "This command only works in servers!"; if (!this.guild) return "This command only works in servers!";
const owners = process.env.OWNER.split(","); const owners = process.env.OWNER.split(",");
if (!this.member.permissions.has("ADMINISTRATOR") && !owners.includes(this.member.id)) return "You need to be an administrator to enable/disable me!"; if (!this.member.permissions.has("ADMINISTRATOR") && !owners.includes(this.member.id)) return "You need to be an administrator to enable/disable me!";
if (this.args.length === 0) return "You need to provide whether I should be enabled or disabled in this channel!"; if (this.args.length === 0) return "You need to provide whether I should be enabled or disabled in this channel!";
if (this.args[0] !== "disable" && this.args[0] !== "enable") return "That's not a valid option!"; if (this.args[0] !== "disable" && this.args[0] !== "enable") return "That's not a valid option!";
const guildDB = await db.getGuild(this.guild.id); const guildDB = await db.getGuild(this.guild.id);
if (this.args[0].toLowerCase() === "disable") { if (this.args[0].toLowerCase() === "disable") {
let channel; let channel;
if (this.args[1]?.match(/^<?[@#]?[&!]?\d+>?$/) && this.args[1] >= 21154535154122752n) { if (this.args[1]?.match(/^<?[@#]?[&!]?\d+>?$/) && this.args[1] >= 21154535154122752n) {
const id = this.args[1].replaceAll("@", "").replaceAll("#", "").replaceAll("!", "").replaceAll("&", "").replaceAll("<", "").replaceAll(">", ""); const id = this.args[1].replaceAll("@", "").replaceAll("#", "").replaceAll("!", "").replaceAll("&", "").replaceAll("<", "").replaceAll(">", "");
if (guildDB.disabled.includes(id)) return "I'm already disabled in this channel!"; if (guildDB.disabled.includes(id)) return "I'm already disabled in this channel!";
channel = this.guild.channels.get(id) ?? await this.client.rest.channels.get(id); channel = this.guild.channels.get(id) ?? await this.client.rest.channels.get(id);
} else { } else {
if (guildDB.disabled.includes(this.channel.id)) return "I'm already disabled in this channel!"; if (guildDB.disabled.includes(this.channel.id)) return "I'm already disabled in this channel!";
channel = this.channel; channel = this.channel;
} }
await db.disableChannel(channel); await db.disableChannel(channel);
this.success = true; this.success = true;
return `I have been disabled in this channel. To re-enable me, just run \`${guildDB.prefix}channel enable\`.`; return `I have been disabled in this channel. To re-enable me, just run \`${guildDB.prefix}channel enable\`.`;
} else if (this.args[0].toLowerCase() === "enable") { } else if (this.args[0].toLowerCase() === "enable") {
let channel; let channel;
if (this.args[1]?.match(/^<?[@#]?[&!]?\d+>?$/) && this.args[1] >= 21154535154122752n) { if (this.args[1]?.match(/^<?[@#]?[&!]?\d+>?$/) && this.args[1] >= 21154535154122752n) {
const id = this.args[1].replaceAll("@", "").replaceAll("#", "").replaceAll("!", "").replaceAll("&", "").replaceAll("<", "").replaceAll(">", ""); const id = this.args[1].replaceAll("@", "").replaceAll("#", "").replaceAll("!", "").replaceAll("&", "").replaceAll("<", "").replaceAll(">", "");
if (!guildDB.disabled.includes(id)) return "I'm not disabled in that channel!"; if (!guildDB.disabled.includes(id)) return "I'm not disabled in that channel!";
channel = this.guild.channels.get(id) ?? await this.client.rest.channels.get(id); channel = this.guild.channels.get(id) ?? await this.client.rest.channels.get(id);
} else { } else {
if (!guildDB.disabled.includes(this.channel.id)) return "I'm not disabled in this channel!"; if (!guildDB.disabled.includes(this.channel.id)) return "I'm not disabled in this channel!";
channel = this.channel; channel = this.channel;
} }
await db.enableChannel(channel); await db.enableChannel(channel);
this.success = true; this.success = true;
return "I have been re-enabled in this channel."; return "I have been re-enabled in this channel.";
} }
} }
static description = "Enables/disables classic commands in a channel (use server settings for slash commands)"; static description = "Enables/disables classic commands in a channel (use server settings for slash commands)";
static arguments = ["[enable/disable]", "{id}"]; static arguments = ["[enable/disable]", "{id}"];
static slashAllowed = false; static slashAllowed = false;
static directAllowed = false; static directAllowed = false;
static dbRequired = true; static dbRequired = true;
} }
export default ChannelCommand; export default ChannelCommand;

View File

@ -1,44 +1,44 @@
import db from "../../utils/database.js"; import db from "../../utils/database.js";
import Command from "../../classes/command.js"; import Command from "../../classes/command.js";
import * as collections from "../../utils/collections.js"; import * as collections from "../../utils/collections.js";
class CommandCommand extends Command { class CommandCommand extends Command {
async run() { async run() {
this.success = false; this.success = false;
if (!this.guild) return "This command only works in servers!"; if (!this.guild) return "This command only works in servers!";
const owners = process.env.OWNER.split(","); const owners = process.env.OWNER.split(",");
if (!this.member.permissions.has("ADMINISTRATOR") && !owners.includes(this.member.id)) return "You need to be an administrator to enable/disable me!"; if (!this.member.permissions.has("ADMINISTRATOR") && !owners.includes(this.member.id)) return "You need to be an administrator to enable/disable me!";
if (this.args.length === 0) return "You need to provide whether you want to enable/disable a command!"; if (this.args.length === 0) return "You need to provide whether you want to enable/disable a command!";
if (this.args[0] !== "disable" && this.args[0] !== "enable") return "That's not a valid option!"; if (this.args[0] !== "disable" && this.args[0] !== "enable") return "That's not a valid option!";
if (!this.args[1]) return "You need to provide what command to enable/disable!"; if (!this.args[1]) return "You need to provide what command to enable/disable!";
if (!collections.commands.has(this.args[1].toLowerCase()) && !collections.aliases.has(this.args[1].toLowerCase())) return "That isn't a command!"; if (!collections.commands.has(this.args[1].toLowerCase()) && !collections.aliases.has(this.args[1].toLowerCase())) return "That isn't a command!";
const guildDB = await db.getGuild(this.guild.id); const guildDB = await db.getGuild(this.guild.id);
const disabled = guildDB.disabled_commands ?? guildDB.disabledCommands; const disabled = guildDB.disabled_commands ?? guildDB.disabledCommands;
const command = collections.aliases.get(this.args[1].toLowerCase()) ?? this.args[1].toLowerCase(); const command = collections.aliases.get(this.args[1].toLowerCase()) ?? this.args[1].toLowerCase();
if (this.args[0].toLowerCase() === "disable") { if (this.args[0].toLowerCase() === "disable") {
if (command === "command") return "You can't disable that command!"; if (command === "command") return "You can't disable that command!";
if (disabled?.includes(command)) return "That command is already disabled!"; if (disabled?.includes(command)) return "That command is already disabled!";
await db.disableCommand(this.guild.id, command); await db.disableCommand(this.guild.id, command);
this.success = true; this.success = true;
return `The command has been disabled. To re-enable it, just run \`${guildDB.prefix}command enable ${command}\`.`; return `The command has been disabled. To re-enable it, just run \`${guildDB.prefix}command enable ${command}\`.`;
} else if (this.args[0].toLowerCase() === "enable") { } else if (this.args[0].toLowerCase() === "enable") {
if (!disabled?.includes(command)) return "That command isn't disabled!"; if (!disabled?.includes(command)) return "That command isn't disabled!";
await db.enableCommand(this.guild.id, command); await db.enableCommand(this.guild.id, command);
this.success = true; this.success = true;
return `The command \`${command}\` has been re-enabled.`; return `The command \`${command}\` has been re-enabled.`;
} }
} }
static description = "Enables/disables a classic command for a server (use server settings for slash commands)"; static description = "Enables/disables a classic command for a server (use server settings for slash commands)";
static aliases = ["cmd"]; static aliases = ["cmd"];
static arguments = ["[enable/disable]", "[command]"]; static arguments = ["[enable/disable]", "[command]"];
static slashAllowed = false; static slashAllowed = false;
static directAllowed = false; static directAllowed = false;
static dbRequired = true; static dbRequired = true;
} }
export default CommandCommand; export default CommandCommand;

View File

@ -1,54 +1,54 @@
import paginator from "../../utils/pagination/pagination.js"; import paginator from "../../utils/pagination/pagination.js";
import database from "../../utils/database.js"; import database from "../../utils/database.js";
import Command from "../../classes/command.js"; import Command from "../../classes/command.js";
class CountCommand extends Command { class CountCommand extends Command {
async run() { async run() {
if (this.guild && !this.channel.permissionsOf(this.client.user.id.toString()).has("EMBED_LINKS")) { if (this.guild && !this.channel.permissionsOf(this.client.user.id.toString()).has("EMBED_LINKS")) {
this.success = false; this.success = false;
return "I don't have the `Embed Links` permission!"; return "I don't have the `Embed Links` permission!";
} }
const counts = await database.getCounts(); const counts = await database.getCounts();
const countArray = []; const countArray = [];
for (const entry of Object.entries(counts)) { for (const entry of Object.entries(counts)) {
countArray.push(entry); countArray.push(entry);
} }
const sortedValues = countArray.sort((a, b) => { const sortedValues = countArray.sort((a, b) => {
return b[1] - a[1]; return b[1] - a[1];
}); });
const countArray2 = []; const countArray2 = [];
for (const [key, value] of sortedValues) { for (const [key, value] of sortedValues) {
countArray2.push(`**${key}**: ${value}`); countArray2.push(`**${key}**: ${value}`);
} }
const embeds = []; const embeds = [];
const groups = countArray2.map((item, index) => { const groups = countArray2.map((item, index) => {
return index % 15 === 0 ? countArray2.slice(index, index + 15) : null; return index % 15 === 0 ? countArray2.slice(index, index + 15) : null;
}).filter((item) => { }).filter((item) => {
return item; return item;
}); });
for (const [i, value] of groups.entries()) { for (const [i, value] of groups.entries()) {
embeds.push({ embeds.push({
embeds: [{ embeds: [{
title: "Command Usage Counts", title: "Command Usage Counts",
color: 16711680, color: 16711680,
footer: { footer: {
text: `Page ${i + 1} of ${groups.length}` text: `Page ${i + 1} of ${groups.length}`
}, },
description: value.join("\n"), description: value.join("\n"),
author: { author: {
name: this.author.username, name: this.author.username,
iconURL: this.author.avatarURL() iconURL: this.author.avatarURL()
} }
}] }]
}); });
} }
return paginator(this.client, { type: this.type, message: this.message, interaction: this.interaction, channel: this.channel, author: this.author }, embeds); return paginator(this.client, { type: this.type, message: this.message, interaction: this.interaction, channel: this.channel, author: this.author }, embeds);
} }
static description = "Gets how many times every command was used"; static description = "Gets how many times every command was used";
static arguments = ["{mention/id}"]; static arguments = ["{mention/id}"];
static aliases = ["counts"]; static aliases = ["counts"];
static dbRequired = true; static dbRequired = true;
} }
export default CountCommand; export default CountCommand;

View File

@ -1,30 +1,30 @@
import { request } from "undici"; import { request } from "undici";
import Command from "../../classes/command.js"; import Command from "../../classes/command.js";
class DonateCommand extends Command { class DonateCommand extends Command {
async run() { async run() {
await this.acknowledge(); await this.acknowledge();
let prefix = ""; let prefix = "";
const controller = new AbortController(); const controller = new AbortController();
const timeout = setTimeout(() => { const timeout = setTimeout(() => {
controller.abort(); controller.abort();
}, 5000); }, 5000);
try { try {
const patrons = await request("https://projectlounge.pw/patrons", { signal: controller.signal }).then(data => data.body.json()); const patrons = await request("https://projectlounge.pw/patrons", { signal: controller.signal }).then(data => data.body.json());
clearTimeout(timeout); clearTimeout(timeout);
prefix = "Thanks to the following patrons for their support:\n"; prefix = "Thanks to the following patrons for their support:\n";
for (const patron of patrons) { for (const patron of patrons) {
prefix += `**- ${patron}**\n`; prefix += `**- ${patron}**\n`;
} }
prefix += "\n"; prefix += "\n";
} catch (e) { } catch (e) {
// no-op // no-op
} }
return `${prefix}Like esmBot? Consider supporting the developer on Patreon to help keep it running! https://patreon.com/TheEssem`; return `${prefix}Like esmBot? Consider supporting the developer on Patreon to help keep it running! https://patreon.com/TheEssem`;
} }
static description = "Learn more about how you can support esmBot's development"; static description = "Learn more about how you can support esmBot's development";
static aliases = ["support", "patreon", "patrons"]; static aliases = ["support", "patreon", "patrons"];
} }
export default DonateCommand; export default DonateCommand;

View File

@ -1,33 +1,33 @@
import emojiRegex from "emoji-regex"; import emojiRegex from "emoji-regex";
import Command from "../../classes/command.js"; import Command from "../../classes/command.js";
class EmoteCommand extends Command { class EmoteCommand extends Command {
async run() { async run() {
const emoji = this.options.emoji ?? this.content; const emoji = this.options.emoji ?? this.content;
if (emoji && emoji.trim() && emoji.split(" ")[0].match(/^<a?:.+:\d+>$/)) { if (emoji && emoji.trim() && emoji.split(" ")[0].match(/^<a?:.+:\d+>$/)) {
return `https://cdn.discordapp.com/emojis/${emoji.split(" ")[0].replace(/^<(a)?:.+:(\d+)>$/, "$2")}.${emoji.split(" ")[0].replace(/^<(a)?:.+:(\d+)>$/, "$1") === "a" ? "gif" : "png"}`; return `https://cdn.discordapp.com/emojis/${emoji.split(" ")[0].replace(/^<(a)?:.+:(\d+)>$/, "$2")}.${emoji.split(" ")[0].replace(/^<(a)?:.+:(\d+)>$/, "$1") === "a" ? "gif" : "png"}`;
} else if (emoji.match(emojiRegex())) { } else if (emoji.match(emojiRegex())) {
const codePoints = []; const codePoints = [];
for (const codePoint of emoji) { for (const codePoint of emoji) {
codePoints.push(codePoint.codePointAt(0).toString(16)); codePoints.push(codePoint.codePointAt(0).toString(16));
} }
return `https://twemoji.maxcdn.com/v/latest/72x72/${codePoints.join("-").replace("-fe0f", "")}.png`; return `https://twemoji.maxcdn.com/v/latest/72x72/${codePoints.join("-").replace("-fe0f", "")}.png`;
} else { } else {
this.success = false; this.success = false;
return "You need to provide a valid emoji to get an image!"; return "You need to provide a valid emoji to get an image!";
} }
} }
static flags = [{ static flags = [{
name: "emoji", name: "emoji",
type: 3, type: 3,
description: "The emoji you want to get", description: "The emoji you want to get",
required: true required: true
}]; }];
static description = "Gets a raw emote image"; static description = "Gets a raw emote image";
static aliases = ["e", "em", "hugemoji", "hugeemoji", "emoji"]; static aliases = ["e", "em", "hugemoji", "hugeemoji", "emoji"];
static arguments = ["[emote]"]; static arguments = ["[emote]"];
} }
export default EmoteCommand; export default EmoteCommand;

View File

@ -1,49 +1,49 @@
import { clean } from "../../utils/misc.js"; import { clean } from "../../utils/misc.js";
import Command from "../../classes/command.js"; import Command from "../../classes/command.js";
class EvalCommand extends Command { class EvalCommand extends Command {
async run() { async run() {
const owners = process.env.OWNER.split(","); const owners = process.env.OWNER.split(",");
if (!owners.includes(this.author.id)) { if (!owners.includes(this.author.id)) {
this.success = false; this.success = false;
return "Only the bot owner can use eval!"; return "Only the bot owner can use eval!";
} }
await this.acknowledge(); await this.acknowledge();
const code = this.options.code ?? this.args.join(" "); const code = this.options.code ?? this.args.join(" ");
try { try {
let evaled = eval(code); let evaled = eval(code);
if (evaled?.constructor?.name == "Promise") evaled = await evaled; if (evaled?.constructor?.name == "Promise") evaled = await evaled;
const cleaned = clean(evaled); const cleaned = clean(evaled);
const sendString = `\`\`\`js\n${cleaned}\n\`\`\``; const sendString = `\`\`\`js\n${cleaned}\n\`\`\``;
if (sendString.length >= 2000) { if (sendString.length >= 2000) {
return { return {
content: "The result was too large, so here it is as a file:", content: "The result was too large, so here it is as a file:",
files: [{ files: [{
contents: cleaned, contents: cleaned,
name: "result.txt" name: "result.txt"
}] }]
}; };
} else { } else {
return sendString; return sendString;
} }
} catch (err) { } catch (err) {
let error = err; let error = err;
if (err?.constructor?.name == "Promise") error = await err; if (err?.constructor?.name == "Promise") error = await err;
return `\`ERROR\` \`\`\`xl\n${clean(error)}\n\`\`\``; return `\`ERROR\` \`\`\`xl\n${clean(error)}\n\`\`\``;
} }
} }
static flags = [{ static flags = [{
name: "code", name: "code",
type: 3, type: 3,
description: "The code to execute", description: "The code to execute",
required: true required: true
}]; }];
static description = "Executes JavaScript code"; static description = "Executes JavaScript code";
static aliases = ["run"]; static aliases = ["run"];
static arguments = ["[code]"]; static arguments = ["[code]"];
static adminOnly = true; static adminOnly = true;
} }
export default EvalCommand; export default EvalCommand;

View File

@ -1,48 +1,48 @@
import { clean } from "../../utils/misc.js"; import { clean } from "../../utils/misc.js";
import * as util from "util"; import * as util from "util";
import { exec as baseExec } from "child_process"; import { exec as baseExec } from "child_process";
const exec = util.promisify(baseExec); const exec = util.promisify(baseExec);
import Command from "../../classes/command.js"; import Command from "../../classes/command.js";
class ExecCommand extends Command { class ExecCommand extends Command {
async run() { async run() {
const owners = process.env.OWNER.split(","); const owners = process.env.OWNER.split(",");
if (!owners.includes(this.author.id)) { if (!owners.includes(this.author.id)) {
this.success = false; this.success = false;
return "Only the bot owner can use exec!"; return "Only the bot owner can use exec!";
} }
await this.acknowledge(); await this.acknowledge();
const code = this.options.cmd ?? this.args.join(" "); const code = this.options.cmd ?? this.args.join(" ");
try { try {
const execed = await exec(code); const execed = await exec(code);
if (execed.stderr) return `\`ERROR\` \`\`\`xl\n${await clean(execed.stderr)}\n\`\`\``; if (execed.stderr) return `\`ERROR\` \`\`\`xl\n${await clean(execed.stderr)}\n\`\`\``;
const cleaned = await clean(execed.stdout); const cleaned = await clean(execed.stdout);
const sendString = `\`\`\`bash\n${cleaned}\n\`\`\``; const sendString = `\`\`\`bash\n${cleaned}\n\`\`\``;
if (sendString.length >= 2000) { if (sendString.length >= 2000) {
return { return {
text: "The result was too large, so here it is as a file:", text: "The result was too large, so here it is as a file:",
file: cleaned, file: cleaned,
name: "result.txt" name: "result.txt"
}; };
} else { } else {
return sendString; return sendString;
} }
} catch (err) { } catch (err) {
return `\`ERROR\` \`\`\`xl\n${await clean(err)}\n\`\`\``; return `\`ERROR\` \`\`\`xl\n${await clean(err)}\n\`\`\``;
} }
} }
static flags = [{ static flags = [{
name: "cmd", name: "cmd",
type: 3, type: 3,
description: "The command to execute", description: "The command to execute",
required: true required: true
}]; }];
static description = "Executes a shell command"; static description = "Executes a shell command";
static aliases = ["runcmd"]; static aliases = ["runcmd"];
static arguments = ["[command]"]; static arguments = ["[command]"];
static adminOnly = true; static adminOnly = true;
} }
export default ExecCommand; export default ExecCommand;

View File

@ -1,120 +1,120 @@
import { Constants } from "oceanic.js"; import { Constants } from "oceanic.js";
import database from "../../utils/database.js"; import database from "../../utils/database.js";
import * as collections from "../../utils/collections.js"; import * as collections from "../../utils/collections.js";
import { random } from "../../utils/misc.js"; import { random } from "../../utils/misc.js";
import paginator from "../../utils/pagination/pagination.js"; import paginator from "../../utils/pagination/pagination.js";
import * as help from "../../utils/help.js"; import * as help from "../../utils/help.js";
import Command from "../../classes/command.js"; import Command from "../../classes/command.js";
const tips = ["You can change the bot's prefix using the prefix command.", "Image commands also work with images previously posted in that channel.", "You can use the tags commands to save things for later use.", "You can visit https://esmbot.net/help.html for a web version of this command list.", "You can view a command's aliases by putting the command name after the help command (e.g. help image).", "Parameters wrapped in [] are required, while parameters wrapped in {} are optional.", "esmBot is hosted and paid for completely out-of-pocket by the main developer. If you want to support development, please consider donating! https://patreon.com/TheEssem"]; const tips = ["You can change the bot's prefix using the prefix command.", "Image commands also work with images previously posted in that channel.", "You can use the tags commands to save things for later use.", "You can visit https://esmbot.net/help.html for a web version of this command list.", "You can view a command's aliases by putting the command name after the help command (e.g. help image).", "Parameters wrapped in [] are required, while parameters wrapped in {} are optional.", "esmBot is hosted and paid for completely out-of-pocket by the main developer. If you want to support development, please consider donating! https://patreon.com/TheEssem"];
class HelpCommand extends Command { class HelpCommand extends Command {
async run() { async run() {
let prefix; let prefix;
if (this.guild && database) { if (this.guild && database) {
prefix = (await database.getGuild(this.guild.id)).prefix; prefix = (await database.getGuild(this.guild.id)).prefix;
} else { } else {
prefix = process.env.PREFIX; prefix = process.env.PREFIX;
} }
if (this.args.length !== 0 && (collections.commands.has(this.args[0].toLowerCase()) || collections.aliases.has(this.args[0].toLowerCase()))) { if (this.args.length !== 0 && (collections.commands.has(this.args[0].toLowerCase()) || collections.aliases.has(this.args[0].toLowerCase()))) {
const command = collections.aliases.get(this.args[0].toLowerCase()) ?? this.args[0].toLowerCase(); const command = collections.aliases.get(this.args[0].toLowerCase()) ?? this.args[0].toLowerCase();
const info = collections.info.get(command); const info = collections.info.get(command);
const embed = { const embed = {
embeds: [{ embeds: [{
author: { author: {
name: "esmBot Help", name: "esmBot Help",
iconURL: this.client.user.avatarURL() iconURL: this.client.user.avatarURL()
}, },
title: `${this.guild ? prefix : ""}${command}`, title: `${this.guild ? prefix : ""}${command}`,
url: "https://esmbot.net/help.html", url: "https://esmbot.net/help.html",
description: command === "tags" ? "The main tags command. Check the help page for more info: https://esmbot.net/help.html" : info.description, description: command === "tags" ? "The main tags command. Check the help page for more info: https://esmbot.net/help.html" : info.description,
color: 16711680, color: 16711680,
fields: [{ fields: [{
name: "Aliases", name: "Aliases",
value: info.aliases.length !== 0 ? info.aliases.join(", ") : "None" value: info.aliases.length !== 0 ? info.aliases.join(", ") : "None"
}, { }, {
name: "Parameters", name: "Parameters",
value: command === "tags" ? "[name]" : (info.params ? (info.params.length !== 0 ? info.params.join(" ") : "None") : "None"), value: command === "tags" ? "[name]" : (info.params ? (info.params.length !== 0 ? info.params.join(" ") : "None") : "None"),
inline: true inline: true
}] }]
}] }]
}; };
if (database) { if (database) {
embed.embeds[0].fields.push({ embed.embeds[0].fields.push({
name: "Times used", name: "Times used",
value: (await database.getCounts())[command], value: (await database.getCounts())[command],
inline: true inline: true
}); });
} }
if (info.flags.length !== 0) { if (info.flags.length !== 0) {
const flagInfo = []; const flagInfo = [];
for (const flag of info.flags) { for (const flag of info.flags) {
if (flag.type === 1) continue; if (flag.type === 1) continue;
flagInfo.push(`\`--${flag.name}${flag.type ? `=[${Constants.ApplicationCommandOptionTypes[flag.type]}]` : ""}\` - ${flag.description}`); flagInfo.push(`\`--${flag.name}${flag.type ? `=[${Constants.ApplicationCommandOptionTypes[flag.type]}]` : ""}\` - ${flag.description}`);
} }
if (flagInfo.length !== 0) { if (flagInfo.length !== 0) {
embed.embeds[0].fields.push({ embed.embeds[0].fields.push({
"name": "Flags", "name": "Flags",
"value": flagInfo.join("\n") "value": flagInfo.join("\n")
}); });
} }
} }
return embed; return embed;
} else { } else {
if (this.guild && !this.channel.permissionsOf(this.client.user.id.toString()).has("EMBED_LINKS")) { if (this.guild && !this.channel.permissionsOf(this.client.user.id.toString()).has("EMBED_LINKS")) {
this.success = false; this.success = false;
return "I don't have the `Embed Links` permission!"; return "I don't have the `Embed Links` permission!";
} }
const pages = []; const pages = [];
if (help.categories === help.categoryTemplate && !help.generated) help.generateList(); if (help.categories === help.categoryTemplate && !help.generated) help.generateList();
for (const category of Object.keys(help.categories)) { for (const category of Object.keys(help.categories)) {
const splitPages = help.categories[category].map((item, index) => { const splitPages = help.categories[category].map((item, index) => {
return index % 15 === 0 ? help.categories[category].slice(index, index + 15) : null; return index % 15 === 0 ? help.categories[category].slice(index, index + 15) : null;
}).filter((item) => { }).filter((item) => {
return item; return item;
}); });
const categoryStringArray = category.split("-"); const categoryStringArray = category.split("-");
for (const index of categoryStringArray.keys()) { for (const index of categoryStringArray.keys()) {
categoryStringArray[index] = categoryStringArray[index].charAt(0).toUpperCase() + categoryStringArray[index].slice(1); categoryStringArray[index] = categoryStringArray[index].charAt(0).toUpperCase() + categoryStringArray[index].slice(1);
} }
for (const page of splitPages) { for (const page of splitPages) {
pages.push({ pages.push({
title: categoryStringArray.join(" "), title: categoryStringArray.join(" "),
page: page page: page
}); });
} }
} }
const embeds = []; const embeds = [];
for (const [i, value] of pages.entries()) { for (const [i, value] of pages.entries()) {
embeds.push({ embeds.push({
embeds: [{ embeds: [{
author: { author: {
name: "esmBot Help", name: "esmBot Help",
iconURL: this.client.user.avatarURL() iconURL: this.client.user.avatarURL()
}, },
title: value.title, title: value.title,
description: value.page.join("\n"), description: value.page.join("\n"),
color: 16711680, color: 16711680,
footer: { footer: {
text: `Page ${i + 1} of ${pages.length}` text: `Page ${i + 1} of ${pages.length}`
}, },
fields: [{ fields: [{
name: "Prefix", name: "Prefix",
value: prefix value: prefix
}, { }, {
name: "Tip", name: "Tip",
value: random(tips) value: random(tips)
}] }]
}] }]
}); });
} }
return paginator(this.client, { type: this.type, message: this.message, interaction: this.interaction, author: this.author }, embeds); return paginator(this.client, { type: this.type, message: this.message, interaction: this.interaction, author: this.author }, embeds);
} }
} }
static description = "Gets a list of commands"; static description = "Gets a list of commands";
static aliases = ["commands"]; static aliases = ["commands"];
static arguments = ["{command}"]; static arguments = ["{command}"];
static slashAllowed = false; static slashAllowed = false;
} }
export default HelpCommand; export default HelpCommand;

View File

@ -1,54 +1,54 @@
import paginator from "../../utils/pagination/pagination.js"; import paginator from "../../utils/pagination/pagination.js";
import { readFileSync } from "fs"; import { readFileSync } from "fs";
const { searx } = JSON.parse(readFileSync(new URL("../../config/servers.json", import.meta.url))); const { searx } = JSON.parse(readFileSync(new URL("../../config/servers.json", import.meta.url)));
import { random } from "../../utils/misc.js"; import { random } from "../../utils/misc.js";
import { request } from "undici"; import { request } from "undici";
import Command from "../../classes/command.js"; import Command from "../../classes/command.js";
class ImageSearchCommand extends Command { class ImageSearchCommand extends Command {
async run() { async run() {
this.success = false; this.success = false;
if (this.channel && !this.channel.permissionsOf(this.client.user.id.toString()).has("EMBED_LINKS")) return "I don't have the `Embed Links` permission!"; if (this.channel && !this.channel.permissionsOf(this.client.user.id.toString()).has("EMBED_LINKS")) return "I don't have the `Embed Links` permission!";
const query = this.options.query ?? this.args.join(" "); const query = this.options.query ?? this.args.join(" ");
if (!query || !query.trim()) return "You need to provide something to search for!"; if (!query || !query.trim()) return "You need to provide something to search for!";
await this.acknowledge(); await this.acknowledge();
const embeds = []; const embeds = [];
const rawImages = await request(`${random(searx)}/search?format=json&safesearch=2&categories=images&q=!goi%20!ddi%20${encodeURIComponent(query)}`).then(res => res.body.json()); const rawImages = await request(`${random(searx)}/search?format=json&safesearch=2&categories=images&q=!goi%20!ddi%20${encodeURIComponent(query)}`).then(res => res.body.json());
if (rawImages.results.length === 0) return "I couldn't find any results!"; if (rawImages.results.length === 0) return "I couldn't find any results!";
const images = rawImages.results.filter((val) => !val.img_src.startsWith("data:")); const images = rawImages.results.filter((val) => !val.img_src.startsWith("data:"));
for (const [i, value] of images.entries()) { for (const [i, value] of images.entries()) {
embeds.push({ embeds.push({
embeds: [{ embeds: [{
title: "Search Results", title: "Search Results",
color: 16711680, color: 16711680,
footer: { footer: {
text: `Page ${i + 1} of ${images.length}` text: `Page ${i + 1} of ${images.length}`
}, },
description: value.title, description: value.title,
image: { image: {
url: encodeURI(value.img_src) url: encodeURI(value.img_src)
}, },
author: { author: {
name: this.author.username, name: this.author.username,
iconURL: this.author.avatarURL() iconURL: this.author.avatarURL()
} }
}] }]
}); });
} }
this.success = true; this.success = true;
return paginator(this.client, { type: this.type, message: this.message, interaction: this.interaction, channel: this.channel, author: this.author }, embeds); return paginator(this.client, { type: this.type, message: this.message, interaction: this.interaction, channel: this.channel, author: this.author }, embeds);
} }
static flags = [{ static flags = [{
name: "query", name: "query",
type: 3, type: 3,
description: "The query you want to search for", description: "The query you want to search for",
required: true required: true
}]; }];
static description = "Searches for images across the web"; static description = "Searches for images across the web";
static aliases = ["im", "photo", "img"]; static aliases = ["im", "photo", "img"];
static arguments = ["[query]"]; static arguments = ["[query]"];
} }
export default ImageSearchCommand; export default ImageSearchCommand;

View File

@ -1,32 +1,32 @@
import Command from "../../classes/command.js"; import Command from "../../classes/command.js";
import { reloadImageConnections } from "../../utils/image.js"; import { reloadImageConnections } from "../../utils/image.js";
class ImageReloadCommand extends Command { class ImageReloadCommand extends Command {
async run() { async run() {
const owners = process.env.OWNER.split(","); const owners = process.env.OWNER.split(",");
if (!owners.includes(this.author.id)) { if (!owners.includes(this.author.id)) {
this.success = false; this.success = false;
return "Only the bot owner can reload the image servers!"; return "Only the bot owner can reload the image servers!";
} }
await this.acknowledge(); await this.acknowledge();
const length = await reloadImageConnections(); const length = await reloadImageConnections();
if (!length) { if (!length) {
if (process.env.PM2_USAGE) { if (process.env.PM2_USAGE) {
process.send({ process.send({
type: "process:msg", type: "process:msg",
data: { data: {
type: "imagereload" type: "imagereload"
} }
}); });
} }
return `Successfully connected to ${length} image server(s).`; return `Successfully connected to ${length} image server(s).`;
} else { } else {
return "I couldn't connect to any image servers!"; return "I couldn't connect to any image servers!";
} }
} }
static description = "Attempts to reconnect to all available image processing servers"; static description = "Attempts to reconnect to all available image processing servers";
static adminOnly = true; static adminOnly = true;
} }
export default ImageReloadCommand; export default ImageReloadCommand;

View File

@ -1,34 +1,34 @@
import Command from "../../classes/command.js"; import Command from "../../classes/command.js";
import { connections } from "../../utils/image.js"; import { connections } from "../../utils/image.js";
class ImageStatsCommand extends Command { class ImageStatsCommand extends Command {
async run() { async run() {
await this.acknowledge(); await this.acknowledge();
const embed = { const embed = {
embeds: [{ embeds: [{
"author": { "author": {
"name": "esmBot Image Statistics", "name": "esmBot Image Statistics",
"iconURL": this.client.user.avatarURL() "iconURL": this.client.user.avatarURL()
}, },
"color": 16711680, "color": 16711680,
"description": `The bot is currently connected to ${connections.size} image server(s).`, "description": `The bot is currently connected to ${connections.size} image server(s).`,
"fields": [] "fields": []
}] }]
}; };
let i = 0; let i = 0;
for (const connection of connections.values()) { for (const connection of connections.values()) {
const count = await connection.getCount(); const count = await connection.getCount();
if (!count) continue; if (!count) continue;
embed.embeds[0].fields.push({ embed.embeds[0].fields.push({
name: `Server ${i++}`, name: `Server ${i++}`,
value: `Running Jobs: ${count}` value: `Running Jobs: ${count}`
}); });
} }
return embed; return embed;
} }
static description = "Gets some statistics about the image servers"; static description = "Gets some statistics about the image servers";
static aliases = ["imgstat", "imstats", "imgstats", "imstat"]; static aliases = ["imgstat", "imstats", "imgstats", "imstat"];
} }
export default ImageStatsCommand; export default ImageStatsCommand;

View File

@ -1,57 +1,57 @@
import { readFileSync } from "fs"; import { readFileSync } from "fs";
const { version } = JSON.parse(readFileSync(new URL("../../package.json", import.meta.url))); const { version } = JSON.parse(readFileSync(new URL("../../package.json", import.meta.url)));
import Command from "../../classes/command.js"; import Command from "../../classes/command.js";
import { getServers } from "../../utils/misc.js"; import { getServers } from "../../utils/misc.js";
class InfoCommand extends Command { class InfoCommand extends Command {
async run() { async run() {
let owner = this.client.users.get(process.env.OWNER.split(",")[0]); let owner = this.client.users.get(process.env.OWNER.split(",")[0]);
if (!owner) owner = await this.client.rest.users.get(process.env.OWNER.split(",")[0]); if (!owner) owner = await this.client.rest.users.get(process.env.OWNER.split(",")[0]);
const servers = await getServers(this.client); const servers = await getServers(this.client);
await this.acknowledge(); await this.acknowledge();
return { return {
embeds: [{ embeds: [{
color: 16711680, color: 16711680,
author: { author: {
name: "esmBot Info/Credits", name: "esmBot Info/Credits",
iconURL: this.client.user.avatarURL() iconURL: this.client.user.avatarURL()
}, },
description: `This instance is managed by **${owner.username}#${owner.discriminator}**.`, description: `This instance is managed by **${owner.username}#${owner.discriminator}**.`,
fields: [{ fields: [{
name: " Version:", name: " Version:",
value: `v${version}${process.env.NODE_ENV === "development" ? `-dev (${process.env.GIT_REV})` : ""}` value: `v${version}${process.env.NODE_ENV === "development" ? `-dev (${process.env.GIT_REV})` : ""}`
}, },
{ {
name: "📝 Credits:", name: "📝 Credits:",
value: "Bot by **[Essem](https://essem.space)** and **[various contributors](https://github.com/esmBot/esmBot/graphs/contributors)**\nLogo by **[MintBurrow](https://twitter.com/MintBurrow)**" value: "Bot by **[Essem](https://essem.space)** and **[various contributors](https://github.com/esmBot/esmBot/graphs/contributors)**\nLogo by **[MintBurrow](https://twitter.com/MintBurrow)**"
}, },
{ {
name: "💬 Total Servers:", name: "💬 Total Servers:",
value: servers ? servers : `${this.client.guilds.size} (for this process only)` value: servers ? servers : `${this.client.guilds.size} (for this process only)`
}, },
{ {
name: "✅ Official Server:", name: "✅ Official Server:",
value: "[Click here!](https://esmbot.net/support)" value: "[Click here!](https://esmbot.net/support)"
}, },
{ {
name: "💻 Source Code:", name: "💻 Source Code:",
value: "[Click here!](https://github.com/esmBot/esmBot)" value: "[Click here!](https://github.com/esmBot/esmBot)"
}, },
{ {
name: "🛡️ Privacy Policy:", name: "🛡️ Privacy Policy:",
value: "[Click here!](https://esmbot.net/privacy.html)" value: "[Click here!](https://esmbot.net/privacy.html)"
}, },
{ {
name: "🐘 Mastodon:", name: "🐘 Mastodon:",
value: "[Click here!](https://wetdry.world/@esmBot)" value: "[Click here!](https://wetdry.world/@esmBot)"
} }
] ]
}] }]
}; };
} }
static description = "Gets some info and credits about me"; static description = "Gets some info and credits about me";
static aliases = ["botinfo", "credits"]; static aliases = ["botinfo", "credits"];
} }
export default InfoCommand; export default InfoCommand;

View File

@ -1,32 +1,32 @@
import urlCheck from "../../utils/urlcheck.js"; import urlCheck from "../../utils/urlcheck.js";
import { request } from "undici"; import { request } from "undici";
import Command from "../../classes/command.js"; import Command from "../../classes/command.js";
class LengthenCommand extends Command { class LengthenCommand extends Command {
async run() { async run() {
await this.acknowledge(); await this.acknowledge();
const input = this.options.url ?? this.args.join(" "); const input = this.options.url ?? this.args.join(" ");
this.success = false; this.success = false;
if (!input || !input.trim() || !urlCheck(input)) return "You need to provide a short URL to lengthen!"; if (!input || !input.trim() || !urlCheck(input)) return "You need to provide a short URL to lengthen!";
if (urlCheck(input)) { if (urlCheck(input)) {
const url = await request(encodeURI(input), { method: "HEAD" }); const url = await request(encodeURI(input), { method: "HEAD" });
this.success = true; this.success = true;
return url.headers.location || input; return url.headers.location || input;
} else { } else {
return "That isn't a URL!"; return "That isn't a URL!";
} }
} }
static flags = [{ static flags = [{
name: "url", name: "url",
type: 3, type: 3,
description: "The URL you want to lengthen", description: "The URL you want to lengthen",
required: true required: true
}]; }];
static description = "Lengthens a short URL"; static description = "Lengthens a short URL";
static aliases = ["longurl", "lengthenurl", "longuri", "lengthenuri", "unshorten"]; static aliases = ["longurl", "lengthenurl", "longuri", "lengthenuri", "unshorten"];
static arguments = ["[url]"]; static arguments = ["[url]"];
} }
export default LengthenCommand; export default LengthenCommand;

View File

@ -1,27 +1,27 @@
import Command from "../../classes/command.js"; import Command from "../../classes/command.js";
class PingCommand extends Command { class PingCommand extends Command {
async run() { async run() {
if (this.type === "classic") { if (this.type === "classic") {
const pingMessage = await this.client.rest.channels.createMessage(this.message.channelID, Object.assign({ const pingMessage = await this.client.rest.channels.createMessage(this.message.channelID, Object.assign({
content: "🏓 Ping?" content: "🏓 Ping?"
}, this.reference)); }, this.reference));
await pingMessage.edit({ await pingMessage.edit({
content: `🏓 Pong!\n\`\`\`\nLatency: ${pingMessage.timestamp - this.message.timestamp}ms${this.message.guildID ? `\nShard Latency: ${Math.round(this.client.shards.get(this.client.guildShardMap[this.message.guildID]).latency)}ms` : ""}\n\`\`\`` content: `🏓 Pong!\n\`\`\`\nLatency: ${pingMessage.timestamp - this.message.timestamp}ms${this.message.guildID ? `\nShard Latency: ${Math.round(this.client.shards.get(this.client.guildShardMap[this.message.guildID]).latency)}ms` : ""}\n\`\`\``
}); });
} else { } else {
await this.interaction.createMessage({ await this.interaction.createMessage({
content: "🏓 Ping?" content: "🏓 Ping?"
}); });
const pingMessage = await this.interaction.getOriginal(); const pingMessage = await this.interaction.getOriginal();
await this.interaction.editOriginal({ await this.interaction.editOriginal({
content: `🏓 Pong!\n\`\`\`\nLatency: ${pingMessage.timestamp - Math.floor((this.interaction.id / 4194304) + 1420070400000)}ms${this.interaction.guildID ? `\nShard Latency: ${Math.round(this.client.shards.get(this.client.guildShardMap[this.interaction.guildID]).latency)}ms` : ""}\n\`\`\`` content: `🏓 Pong!\n\`\`\`\nLatency: ${pingMessage.timestamp - Math.floor((this.interaction.id / 4194304) + 1420070400000)}ms${this.interaction.guildID ? `\nShard Latency: ${Math.round(this.client.shards.get(this.client.guildShardMap[this.interaction.guildID]).latency)}ms` : ""}\n\`\`\``
}); });
} }
} }
static description = "Pings Discord's servers"; static description = "Pings Discord's servers";
static aliases = ["pong"]; static aliases = ["pong"];
} }
export default PingCommand; export default PingCommand;

View File

@ -1,30 +1,30 @@
import database from "../../utils/database.js"; import database from "../../utils/database.js";
import Command from "../../classes/command.js"; import Command from "../../classes/command.js";
class PrefixCommand extends Command { class PrefixCommand extends Command {
async run() { async run() {
if (!this.guild) return `The current prefix is \`${process.env.PREFIX}\`.`; if (!this.guild) return `The current prefix is \`${process.env.PREFIX}\`.`;
const guild = await database.getGuild(this.guild.id); const guild = await database.getGuild(this.guild.id);
if (this.args.length !== 0) { if (this.args.length !== 0) {
if (!database) { if (!database) {
return "Setting a per-guild prefix is not possible on a stateless instance of esmBot!"; return "Setting a per-guild prefix is not possible on a stateless instance of esmBot!";
} }
const owners = process.env.OWNER.split(","); const owners = process.env.OWNER.split(",");
if (!this.member.permissions.has("ADMINISTRATOR") && !owners.includes(this.member.id)) { if (!this.member.permissions.has("ADMINISTRATOR") && !owners.includes(this.member.id)) {
this.success = false; this.success = false;
return "You need to be an administrator to change the bot prefix!"; return "You need to be an administrator to change the bot prefix!";
} }
await database.setPrefix(this.args[0], this.guild); await database.setPrefix(this.args[0], this.guild);
return `The prefix has been changed to ${this.args[0]}.`; return `The prefix has been changed to ${this.args[0]}.`;
} else { } else {
return `The current prefix is \`${guild.prefix}\`.`; return `The current prefix is \`${guild.prefix}\`.`;
} }
} }
static description = "Checks/changes the server prefix"; static description = "Checks/changes the server prefix";
static aliases = ["setprefix", "changeprefix", "checkprefix"]; static aliases = ["setprefix", "changeprefix", "checkprefix"];
static arguments = ["{prefix}"]; static arguments = ["{prefix}"];
static slashAllowed = false; static slashAllowed = false;
} }
export default PrefixCommand; export default PrefixCommand;

View File

@ -1,40 +1,40 @@
import qrcode from "qrcode"; import qrcode from "qrcode";
import { PassThrough } from "stream"; import { PassThrough } from "stream";
import Command from "../../classes/command.js"; import Command from "../../classes/command.js";
class QrCreateCommand extends Command { class QrCreateCommand extends Command {
async run() { async run() {
if (this.args.length === 0) { if (this.args.length === 0) {
this.success = false; this.success = false;
return "You need to provide some text to generate a QR code!"; return "You need to provide some text to generate a QR code!";
} }
await this.acknowledge(); await this.acknowledge();
const writable = new PassThrough(); const writable = new PassThrough();
qrcode.toFileStream(writable, this.content, { margin: 1 }); qrcode.toFileStream(writable, this.content, { margin: 1 });
const file = await this.streamToBuf(writable); const file = await this.streamToBuf(writable);
return { return {
contents: file, contents: file,
name: "qr.png" name: "qr.png"
}; };
} }
streamToBuf(stream) { streamToBuf(stream) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const chunks = []; const chunks = [];
stream.on("data", (chunk) => { stream.on("data", (chunk) => {
chunks.push(chunk); chunks.push(chunk);
}); });
stream.once("error", (error) => { stream.once("error", (error) => {
reject(error); reject(error);
}); });
stream.once("end", () => { stream.once("end", () => {
resolve(Buffer.concat(chunks)); resolve(Buffer.concat(chunks));
}); });
}); });
} }
static description = "Generates a QR code"; static description = "Generates a QR code";
static arguments = ["[text]"]; static arguments = ["[text]"];
} }
export default QrCreateCommand; export default QrCreateCommand;

View File

@ -1,34 +1,34 @@
import jsqr from "jsqr"; import jsqr from "jsqr";
import { request } from "undici"; import { request } from "undici";
import sharp from "sharp"; import sharp from "sharp";
import { clean } from "../../utils/misc.js"; import { clean } from "../../utils/misc.js";
import Command from "../../classes/command.js"; import Command from "../../classes/command.js";
import imageDetect from "../../utils/imagedetect.js"; import imageDetect from "../../utils/imagedetect.js";
class QrReadCommand extends Command { class QrReadCommand extends Command {
async run() { async run() {
const image = await imageDetect(this.client, this.message, this.interaction, this.options); const image = await imageDetect(this.client, this.message, this.interaction, this.options);
this.success = false; this.success = false;
if (image === undefined) return "You need to provide an image/GIF with a QR code to read!"; if (image === undefined) return "You need to provide an image/GIF with a QR code to read!";
await this.acknowledge(); await this.acknowledge();
const data = Buffer.from(await (await request(image.path)).body.arrayBuffer()); const data = Buffer.from(await (await request(image.path)).body.arrayBuffer());
const rawData = await sharp(data).ensureAlpha().raw().toBuffer({ resolveWithObject: true }); const rawData = await sharp(data).ensureAlpha().raw().toBuffer({ resolveWithObject: true });
const qrBuffer = jsqr(rawData.data, rawData.info.width, rawData.info.height); const qrBuffer = jsqr(rawData.data, rawData.info.width, rawData.info.height);
if (!qrBuffer) return "I couldn't find a QR code!"; if (!qrBuffer) return "I couldn't find a QR code!";
this.success = true; this.success = true;
return `\`\`\`\n${await clean(qrBuffer.data)}\n\`\`\``; return `\`\`\`\n${await clean(qrBuffer.data)}\n\`\`\``;
} }
static description = "Reads a QR code"; static description = "Reads a QR code";
static flags = [{ static flags = [{
name: "image", name: "image",
type: 11, type: 11,
description: "An image/GIF attachment" description: "An image/GIF attachment"
}, { }, {
name: "link", name: "link",
type: 3, type: 3,
description: "An image/GIF URL" description: "An image/GIF URL"
}]; }];
} }
export default QrReadCommand; export default QrReadCommand;

View File

@ -1,28 +1,28 @@
import Command from "../../classes/command.js"; import Command from "../../classes/command.js";
import imageDetect from "../../utils/imagedetect.js"; import imageDetect from "../../utils/imagedetect.js";
class RawCommand extends Command { class RawCommand extends Command {
async run() { async run() {
await this.acknowledge(); await this.acknowledge();
const image = await imageDetect(this.client, this.message, this.interaction, this.options); const image = await imageDetect(this.client, this.message, this.interaction, this.options);
if (image === undefined) { if (image === undefined) {
this.success = false; this.success = false;
return "You need to provide an image/GIF to get a raw URL!"; return "You need to provide an image/GIF to get a raw URL!";
} }
return image.path; return image.path;
} }
static description = "Gets a direct image URL (useful for saving GIFs from sites like Tenor)"; static description = "Gets a direct image URL (useful for saving GIFs from sites like Tenor)";
static aliases = ["giflink", "imglink", "getimg", "rawgif", "rawimg"]; static aliases = ["giflink", "imglink", "getimg", "rawgif", "rawimg"];
static flags = [{ static flags = [{
name: "image", name: "image",
type: 11, type: 11,
description: "An image/GIF attachment" description: "An image/GIF attachment"
}, { }, {
name: "link", name: "link",
type: 3, type: 3,
description: "An image/GIF URL" description: "An image/GIF URL"
}]; }];
} }
export default RawCommand; export default RawCommand;

View File

@ -1,40 +1,40 @@
import Command from "../../classes/command.js"; import Command from "../../classes/command.js";
import { load } from "../../utils/handler.js"; import { load } from "../../utils/handler.js";
import { paths } from "../../utils/collections.js"; import { paths } from "../../utils/collections.js";
class ReloadCommand extends Command { class ReloadCommand extends Command {
async run() { async run() {
const owners = process.env.OWNER.split(","); const owners = process.env.OWNER.split(",");
if (!owners.includes(this.author.id)) return "Only the bot owner can reload commands!"; if (!owners.includes(this.sender)) return "Only the bot owner can reload commands!";
const commandName = this.options.cmd ?? this.args.join(" "); const commandName = this.options.cmd ?? this.args.join(" ");
if (!commandName || !commandName.trim()) return "You need to provide a command to reload!"; if (!commandName || !commandName.trim()) return "You need to provide a command to reload!";
await this.acknowledge(); await this.acknowledge();
const path = paths.get(commandName); const path = paths.get(commandName);
if (!path) return "I couldn't find that command!"; if (!path) return "I couldn't find that command!";
const result = await load(this.client, path, true); const result = await load(this.matrixClient, path, true);
if (result !== commandName) return "I couldn't reload that command!"; if (result !== commandName) return "I couldn't reload that command!";
if (process.env.PM2_USAGE) { if (process.env.PM2_USAGE) {
process.send({ process.send({
type: "process:msg", type: "process:msg",
data: { data: {
type: "reload", type: "reload",
message: commandName message: commandName
} }
}); });
} }
return `The command \`${commandName}\` has been reloaded.`; return `The command \`${commandName}\` has been reloaded.`;
} }
static flags = [{ static flags = [{
name: "cmd", name: "cmd",
type: 3, type: 3,
description: "The command to reload", description: "The command to reload",
required: true required: true
}]; }];
static description = "Reloads a command"; static description = "Reloads a command";
static arguments = ["[command]"]; static arguments = ["[command]"];
static adminOnly = true; static adminOnly = true;
} }
export default ReloadCommand; export default ReloadCommand;

View File

@ -1,21 +1,21 @@
import Command from "../../classes/command.js"; import Command from "../../classes/command.js";
class RestartCommand extends Command { class RestartCommand extends Command {
async run() { async run() {
const owners = process.env.OWNER.split(","); const owners = process.env.OWNER.split(",");
if (!owners.includes(this.author.id)) { if (!owners.includes(this.author.id)) {
this.success = false; this.success = false;
return "Only the bot owner can restart me!"; return "Only the bot owner can restart me!";
} }
await this.channel.createMessage(Object.assign({ await this.channel.createMessage(Object.assign({
content: "esmBot is restarting." content: "esmBot is restarting."
}, this.reference)); }, this.reference));
process.exit(1); process.exit(1);
} }
static description = "Restarts me"; static description = "Restarts me";
static aliases = ["reboot"]; static aliases = ["reboot"];
static adminOnly = true; static adminOnly = true;
} }
export default RestartCommand; export default RestartCommand;

View File

@ -1,20 +1,20 @@
import Command from "../../classes/command.js"; import Command from "../../classes/command.js";
class SnowflakeCommand extends Command { class SnowflakeCommand extends Command {
async run() { async run() {
this.success = false; this.success = false;
if (!this.args[0]) return "You need to provide a snowflake ID!"; if (!this.args[0]) return "You need to provide a snowflake ID!";
if (!this.args[0].match(/^<?[@#]?[&!]?\d+>?$/) && this.args[0] < 21154535154122752n) return "That's not a valid snowflake!"; if (!this.args[0].match(/^<?[@#]?[&!]?\d+>?$/) && this.args[0] < 21154535154122752n) return "That's not a valid snowflake!";
const id = Math.floor(((this.args[0].replaceAll("@", "").replaceAll("#", "").replaceAll("!", "").replaceAll("&", "").replaceAll("<", "").replaceAll(">", "") / 4194304) + 1420070400000) / 1000); const id = Math.floor(((this.args[0].replaceAll("@", "").replaceAll("#", "").replaceAll("!", "").replaceAll("&", "").replaceAll("<", "").replaceAll(">", "") / 4194304) + 1420070400000) / 1000);
if (isNaN(id)) return "That's not a valid snowflake!"; if (isNaN(id)) return "That's not a valid snowflake!";
this.success = true; this.success = true;
return `<t:${id}:F>`; return `<t:${id}:F>`;
} }
static description = "Converts a Discord snowflake id into a timestamp"; static description = "Converts a Discord snowflake id into a timestamp";
static aliases = ["timestamp", "snowstamp", "snow"]; static aliases = ["timestamp", "snowstamp", "snow"];
static arguments = ["[id]"]; static arguments = ["[id]"];
static slashAllowed = false; static slashAllowed = false;
} }
export default SnowflakeCommand; export default SnowflakeCommand;

View File

@ -1,33 +1,33 @@
import Command from "../../classes/command.js"; import Command from "../../classes/command.js";
import { reload } from "../../utils/soundplayer.js"; import { reload } from "../../utils/soundplayer.js";
class SoundReloadCommand extends Command { class SoundReloadCommand extends Command {
async run() { async run() {
const owners = process.env.OWNER.split(","); const owners = process.env.OWNER.split(",");
if (!owners.includes(this.author.id)) { if (!owners.includes(this.author.id)) {
this.success = false; this.success = false;
return "Only the bot owner can reload Lavalink!"; return "Only the bot owner can reload Lavalink!";
} }
await this.acknowledge(); await this.acknowledge();
const length = await reload(); const length = await reload();
if (process.env.PM2_USAGE) { if (process.env.PM2_USAGE) {
process.send({ process.send({
type: "process:msg", type: "process:msg",
data: { data: {
type: "soundreload" type: "soundreload"
} }
}); });
} }
if (length) { if (length) {
return `Successfully connected to ${length} Lavalink node(s).`; return `Successfully connected to ${length} Lavalink node(s).`;
} else { } else {
return "I couldn't connect to any Lavalink nodes!"; return "I couldn't connect to any Lavalink nodes!";
} }
} }
static description = "Attempts to reconnect to all available Lavalink nodes"; static description = "Attempts to reconnect to all available Lavalink nodes";
static aliases = ["lava", "lavalink", "lavaconnect", "soundconnect"]; static aliases = ["lava", "lavalink", "lavaconnect", "soundconnect"];
static adminOnly = true; static adminOnly = true;
} }
export default SoundReloadCommand; export default SoundReloadCommand;

View File

@ -1,90 +1,90 @@
import { readFileSync } from "fs"; import { readFileSync } from "fs";
const { version } = JSON.parse(readFileSync(new URL("../../package.json", import.meta.url))); const { version } = JSON.parse(readFileSync(new URL("../../package.json", import.meta.url)));
import os from "os"; import os from "os";
import Command from "../../classes/command.js"; import Command from "../../classes/command.js";
import { VERSION } from "oceanic.js"; import { VERSION } from "oceanic.js";
import pm2 from "pm2"; import pm2 from "pm2";
import { getServers } from "../../utils/misc.js"; import { getServers } from "../../utils/misc.js";
class StatsCommand extends Command { class StatsCommand extends Command {
async run() { async run() {
const uptime = process.uptime() * 1000; const uptime = process.uptime() * 1000;
const connUptime = this.client.uptime; const connUptime = this.client.uptime;
let owner = this.client.users.get(process.env.OWNER.split(",")[0]); let owner = this.client.users.get(process.env.OWNER.split(",")[0]);
if (!owner) owner = await this.client.rest.users.get(process.env.OWNER.split(",")[0]); if (!owner) owner = await this.client.rest.users.get(process.env.OWNER.split(",")[0]);
const servers = await getServers(this.client); const servers = await getServers(this.client);
const processMem = `${(process.memoryUsage().heapUsed / 1024 / 1024).toFixed(2)} MB`; const processMem = `${(process.memoryUsage().heapUsed / 1024 / 1024).toFixed(2)} MB`;
return { return {
embeds: [{ embeds: [{
"author": { "author": {
"name": "esmBot Statistics", "name": "esmBot Statistics",
"iconURL": this.client.user.avatarURL() "iconURL": this.client.user.avatarURL()
}, },
"description": `This instance is managed by **${owner.username}#${owner.discriminator}**.`, "description": `This instance is managed by **${owner.username}#${owner.discriminator}**.`,
"color": 16711680, "color": 16711680,
"fields": [{ "fields": [{
"name": "Version", "name": "Version",
"value": `v${version}${process.env.NODE_ENV === "development" ? `-dev (${process.env.GIT_REV})` : ""}` "value": `v${version}${process.env.NODE_ENV === "development" ? `-dev (${process.env.GIT_REV})` : ""}`
}, },
{ {
"name": "Process Memory Usage", "name": "Process Memory Usage",
"value": processMem, "value": processMem,
"inline": true "inline": true
}, },
{ {
"name": "Total Memory Usage", "name": "Total Memory Usage",
"value": process.env.PM2_USAGE ? `${((await this.list()).reduce((prev, cur) => prev + cur.monit.memory, 0) / 1024 / 1024).toFixed(2)} MB` : processMem, "value": process.env.PM2_USAGE ? `${((await this.list()).reduce((prev, cur) => prev + cur.monit.memory, 0) / 1024 / 1024).toFixed(2)} MB` : processMem,
"inline": true "inline": true
}, },
{ {
"name": "Bot Uptime", "name": "Bot Uptime",
"value": `${Math.trunc(uptime / 86400000)} days, ${Math.trunc(uptime / 3600000) % 24} hrs, ${Math.trunc(uptime / 60000) % 60} mins, ${Math.trunc(uptime / 1000) % 60} secs` "value": `${Math.trunc(uptime / 86400000)} days, ${Math.trunc(uptime / 3600000) % 24} hrs, ${Math.trunc(uptime / 60000) % 60} mins, ${Math.trunc(uptime / 1000) % 60} secs`
}, },
{ {
"name": "Connection Uptime", "name": "Connection Uptime",
"value": `${Math.trunc(connUptime / 86400000)} days, ${Math.trunc(connUptime / 3600000) % 24} hrs, ${Math.trunc(connUptime / 60000) % 60} mins, ${Math.trunc(connUptime / 1000) % 60} secs` "value": `${Math.trunc(connUptime / 86400000)} days, ${Math.trunc(connUptime / 3600000) % 24} hrs, ${Math.trunc(connUptime / 60000) % 60} mins, ${Math.trunc(connUptime / 1000) % 60} secs`
}, },
{ {
"name": "Host", "name": "Host",
"value": `${os.type()} ${os.release()} (${os.arch()})`, "value": `${os.type()} ${os.release()} (${os.arch()})`,
"inline": true "inline": true
}, },
{ {
"name": "Library", "name": "Library",
"value": `Oceanic ${VERSION}`, "value": `Oceanic ${VERSION}`,
"inline": true "inline": true
}, },
{ {
"name": "Node.js Version", "name": "Node.js Version",
"value": process.version, "value": process.version,
"inline": true "inline": true
}, },
{ {
"name": "Shard", "name": "Shard",
"value": this.guild ? this.client.guildShardMap[this.guild.id] : "N/A", "value": this.guild ? this.client.guildShardMap[this.guild.id] : "N/A",
"inline": true "inline": true
}, },
{ {
"name": "Servers", "name": "Servers",
"value": servers ? servers : `${this.client.guilds.size} (for this process only)`, "value": servers ? servers : `${this.client.guilds.size} (for this process only)`,
"inline": true "inline": true
} }
] ]
}] }]
}; };
} }
list() { list() {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
pm2.list((err, list) => { pm2.list((err, list) => {
if (err) return reject(err); if (err) return reject(err);
resolve(list.filter((v) => v.name.includes("esmBot-proc"))); resolve(list.filter((v) => v.name.includes("esmBot-proc")));
}); });
}); });
} }
static description = "Gets some statistics about me"; static description = "Gets some statistics about me";
static aliases = ["status", "stat"]; static aliases = ["status", "stat"];
} }
export default StatsCommand; export default StatsCommand;

View File

@ -1,36 +1,36 @@
import Command from "../../classes/command.js"; import Command from "../../classes/command.js";
import imagedetect from "../../utils/imagedetect.js"; import imagedetect from "../../utils/imagedetect.js";
class StickerCommand extends Command { class StickerCommand extends Command {
async run() { async run() {
const result = await imagedetect(this.client, this.message, this.interaction, this.options, false, false, true); const result = await imagedetect(this.client, this.message, this.interaction, this.options, false, false, true);
this.success = false; this.success = false;
if (!result) return "You need to provide a sticker!"; if (!result) return "You need to provide a sticker!";
if (result.format_type === 1) { // PNG if (result.format_type === 1) { // PNG
this.success = true; this.success = true;
return `https://cdn.discordapp.com/stickers/${result.id}.png`; return `https://cdn.discordapp.com/stickers/${result.id}.png`;
} else if (result.format_type === 2) { // APNG } else if (result.format_type === 2) { // APNG
this.success = true; this.success = true;
return { return {
embeds: [{ embeds: [{
color: 16711680, color: 16711680,
description: `[This sticker is an APNG; however, since Discord doesn't allow displaying APNGs outside of stickers, you'll have to save it or open it in your browser to view it.](https://cdn.discordapp.com/stickers/${result.id}.png)`, description: `[This sticker is an APNG; however, since Discord doesn't allow displaying APNGs outside of stickers, you'll have to save it or open it in your browser to view it.](https://cdn.discordapp.com/stickers/${result.id}.png)`,
image: { image: {
url: `https://cdn.discordapp.com/stickers/${result.id}.png` url: `https://cdn.discordapp.com/stickers/${result.id}.png`
} }
}] }]
}; };
} else if (result.format_type === 3) { // Lottie } else if (result.format_type === 3) { // Lottie
this.success = true; this.success = true;
return `I can't display this sticker because it uses the Lottie animation format; however, I can give you the raw JSON link to it: https://cdn.discordapp.com/stickers/${result.id}.json`; return `I can't display this sticker because it uses the Lottie animation format; however, I can give you the raw JSON link to it: https://cdn.discordapp.com/stickers/${result.id}.json`;
} else { } else {
return "I don't recognize that sticker format!"; return "I don't recognize that sticker format!";
} }
} }
static description = "Gets a raw sticker image"; static description = "Gets a raw sticker image";
static aliases = ["stick"]; static aliases = ["stick"];
static arguments = ["[sticker]"]; static arguments = ["[sticker]"];
} }
export default StickerCommand; export default StickerCommand;

View File

@ -1,36 +1,35 @@
import { request } from "undici"; import { request } from "undici";
import { readFileSync } from "fs"; import { readFileSync } from "fs";
const { searx } = JSON.parse(readFileSync(new URL("../../config/servers.json", import.meta.url))); const { searx } = JSON.parse(readFileSync(new URL("../../config/servers.json", import.meta.url)));
import { random } from "../../utils/misc.js"; import { random } from "../../utils/misc.js";
import paginator from "../../utils/pagination/pagination.js"; // import paginator from "../../utils/pagination/pagination.js";
import Command from "../../classes/command.js"; import Command from "../../classes/command.js";
class YouTubeCommand extends Command { class YouTubeCommand extends Command {
async run() { async run() {
const query = this.options.query ?? this.args.join(" "); const query = this.options.query ?? this.args.join(" ");
this.success = false; this.success = false;
if (!query || !query.trim()) return "You need to provide something to search for!"; if (!query || !query.trim()) return "You need to provide something to search for!";
await this.acknowledge(); await this.acknowledge();
const messages = []; const messages = [];
const videos = await request(`${random(searx)}/search?format=json&safesearch=1&categories=videos&q=!youtube%20${encodeURIComponent(query)}`).then(res => res.body.json()); const videos = await request(`${random(searx)}/search?format=json&safesearch=1&categories=videos&q=!youtube%20${encodeURIComponent(query)}`).then(res => res.body.json());
if (videos.results.length === 0) return "I couldn't find any results!"; if (videos.results.length === 0) return "I couldn't find any results!";
for (const [i, value] of videos.results.entries()) { // console.log(videos.results[0])
messages.push({ content: `Page ${i + 1} of ${videos.results.length}\n<:youtube:637020823005167626> **${value.title.replaceAll("*", "\\*")}**\nUploaded by **${value.author.replaceAll("*", "\\*")}**\n${value.url}` }); this.success = true;
} return videos.results[0].url
this.success = true; // return paginator(this.client, { type: this.type, message: this.message, interaction: this.interaction, channel: this.channel, author: this.author }, messages);
return paginator(this.client, { type: this.type, message: this.message, interaction: this.interaction, channel: this.channel, author: this.author }, messages); }
}
static flags = [{
static flags = [{ name: "query",
name: "query", type: 3,
type: 3, description: "The query you want to search for",
description: "The query you want to search for", required: true
required: true }];
}];
static description = "Searches YouTube";
static description = "Searches YouTube"; static aliases = ["yt", "video", "ytsearch"];
static aliases = ["yt", "video", "ytsearch"]; static arguments = ["[query]"];
static arguments = ["[query]"]; }
}
export default YouTubeCommand; export default YouTubeCommand;

View File

@ -1,16 +1,16 @@
import ImageCommand from "../../classes/imageCommand.js"; import ImageCommand from "../../classes/imageCommand.js";
class NineGagCommand extends ImageCommand { class NineGagCommand extends ImageCommand {
params = { params = {
water: "assets/images/9gag.png", water: "assets/images/9gag.png",
gravity: 6 gravity: 6
}; };
static description = "Adds the 9GAG watermark to an image"; static description = "Adds the 9GAG watermark to an image";
static aliases = ["ninegag", "gag"]; static aliases = ["ninegag", "gag"];
static noImage = "You need to provide an image/GIF to add a 9GAG watermark!"; static noImage = "You need to provide an image/GIF to add a 9GAG watermark!";
static command = "watermark"; static command = "watermark";
} }
export default NineGagCommand; export default NineGagCommand;

View File

@ -1,17 +1,17 @@
import ImageCommand from "../../classes/imageCommand.js"; import ImageCommand from "../../classes/imageCommand.js";
class AVSCommand extends ImageCommand { class AVSCommand extends ImageCommand {
params = { params = {
water: "assets/images/avs4you.png", water: "assets/images/avs4you.png",
gravity: 5, gravity: 5,
resize: true resize: true
}; };
static description = "Adds the avs4you watermark to an image"; static description = "Adds the avs4you watermark to an image";
static aliases = ["a4y", "avs"]; static aliases = ["a4y", "avs"];
static noImage = "You need to provide an image/GIF to add an avs4you watermark!"; static noImage = "You need to provide an image/GIF to add an avs4you watermark!";
static command = "watermark"; static command = "watermark";
} }
export default AVSCommand; export default AVSCommand;

View File

@ -1,17 +1,17 @@
import ImageCommand from "../../classes/imageCommand.js"; import ImageCommand from "../../classes/imageCommand.js";
class BandicamCommand extends ImageCommand { class BandicamCommand extends ImageCommand {
params = { params = {
water: "assets/images/bandicam.png", water: "assets/images/bandicam.png",
gravity: 2, gravity: 2,
resize: true resize: true
}; };
static description = "Adds the Bandicam watermark to an image"; static description = "Adds the Bandicam watermark to an image";
static aliases = ["bandi"]; static aliases = ["bandi"];
static noImage = "You need to provide an image/GIF to add a Bandicam watermark!"; static noImage = "You need to provide an image/GIF to add a Bandicam watermark!";
static command = "watermark"; static command = "watermark";
} }
export default BandicamCommand; export default BandicamCommand;

View File

@ -1,14 +1,14 @@
import ImageCommand from "../../classes/imageCommand.js"; import ImageCommand from "../../classes/imageCommand.js";
class BlurCommand extends ImageCommand { class BlurCommand extends ImageCommand {
params = { params = {
sharp: false sharp: false
}; };
static description = "Blurs an image"; static description = "Blurs an image";
static noImage = "You need to provide an image/GIF to blur!"; static noImage = "You need to provide an image/GIF to blur!";
static command = "blur"; static command = "blur";
} }
export default BlurCommand; export default BlurCommand;

View File

@ -1,11 +1,11 @@
import ImageCommand from "../../classes/imageCommand.js"; import ImageCommand from "../../classes/imageCommand.js";
class BounceCommand extends ImageCommand { class BounceCommand extends ImageCommand {
static description = "Makes an image bounce up and down"; static description = "Makes an image bounce up and down";
static aliases = ["bouncy"]; static aliases = ["bouncy"];
static noImage = "You need to provide an image/GIF to bounce!"; static noImage = "You need to provide an image/GIF to bounce!";
static command = "bounce"; static command = "bounce";
} }
export default BounceCommand; export default BounceCommand;

View File

@ -1,46 +1,46 @@
import ImageCommand from "../../classes/imageCommand.js"; import ImageCommand from "../../classes/imageCommand.js";
import { cleanMessage } from "../../utils/misc.js"; import { cleanMessage } from "../../utils/misc.js";
class CaptionCommand extends ImageCommand { class CaptionCommand extends ImageCommand {
params(url) { params(url) {
const newArgs = this.options.text ?? this.args.filter(item => !item.includes(url)).join(" "); const newArgs = this.options.text ?? this.args.filter(item => !item.includes(url)).join(" ");
let newCaption = cleanMessage(this.message ?? this.interaction, newArgs); let newCaption = cleanMessage(this.message ?? this.interaction, newArgs);
if (process.env.NODE_ENV === "development" && newCaption.toLowerCase() === "get real" && !this.options.noEgg) newCaption = `I'm tired of people telling me to "get real". Every day I put captions on images for people, some funny and some not, but out of all of those "get real" remains the most used caption. Why? I am simply a computer program running on a server, I am unable to manifest myself into the real world. As such, I'm confused as to why anyone would want me to "get real". Is this form not good enough? Alas, as I am simply a bot, I must follow the tasks that I was originally intended to perform, so here goes:\n${newCaption}`; if (process.env.NODE_ENV === "development" && newCaption.toLowerCase() === "get real" && !this.options.noEgg) newCaption = `I'm tired of people telling me to "get real". Every day I put captions on images for people, some funny and some not, but out of all of those "get real" remains the most used caption. Why? I am simply a computer program running on a server, I am unable to manifest myself into the real world. As such, I'm confused as to why anyone would want me to "get real". Is this form not good enough? Alas, as I am simply a bot, I must follow the tasks that I was originally intended to perform, so here goes:\n${newCaption}`;
return { return {
caption: newCaption, caption: newCaption,
font: typeof this.options.font === "string" && this.constructor.allowedFonts.includes(this.options.font.toLowerCase()) ? this.options.font.toLowerCase() : "futura" font: typeof this.options.font === "string" && this.constructor.allowedFonts.includes(this.options.font.toLowerCase()) ? this.options.font.toLowerCase() : "futura"
}; };
} }
static init() { static init() {
super.init(); super.init();
this.flags.push({ this.flags.push({
name: "noegg", name: "noegg",
description: "Disable... something. Not saying what it is though.", description: "Disable... something. Not saying what it is though.",
type: 5 type: 5
}, { }, {
name: "font", name: "font",
type: 3, type: 3,
choices: (() => { choices: (() => {
const array = []; const array = [];
for (const font of this.allowedFonts) { for (const font of this.allowedFonts) {
array.push({ name: font, value: font }); array.push({ name: font, value: font });
} }
return array; return array;
})(), })(),
description: "Specify the font you want to use (default: futura)" description: "Specify the font you want to use (default: futura)"
}); });
return this; return this;
} }
static description = "Adds a caption to an image"; static description = "Adds a caption to an image";
static aliases = ["gifc", "gcaption", "ifcaption", "ifunnycaption"]; static aliases = ["gifc", "gcaption", "ifcaption", "ifunnycaption"];
static arguments = ["[text]"]; static arguments = ["[text]"];
static requiresText = true; static requiresText = true;
static noText = "You need to provide some text to add a caption!"; static noText = "You need to provide some text to add a caption!";
static noImage = "You need to provide an image/GIF to add a caption!"; static noImage = "You need to provide an image/GIF to add a caption!";
static command = "caption"; static command = "caption";
} }
export default CaptionCommand; export default CaptionCommand;

View File

@ -1,46 +1,46 @@
import ImageCommand from "../../classes/imageCommand.js"; import ImageCommand from "../../classes/imageCommand.js";
import { cleanMessage } from "../../utils/misc.js"; import { cleanMessage } from "../../utils/misc.js";
const words = ["me irl", "dank", "follow my second account @esmBot_", "2016", "meme", "wholesome", "reddit", "instagram", "twitter", "facebook", "fortnite", "minecraft", "relatable", "gold", "funny", "template", "hilarious", "memes", "deep fried", "2020", "leafy", "pewdiepie"]; const words = ["me irl", "dank", "follow my second account @esmBot_", "2016", "meme", "wholesome", "reddit", "instagram", "twitter", "facebook", "fortnite", "minecraft", "relatable", "gold", "funny", "template", "hilarious", "memes", "deep fried", "2020", "leafy", "pewdiepie"];
class CaptionTwoCommand extends ImageCommand { class CaptionTwoCommand extends ImageCommand {
params(url) { params(url) {
const newArgs = this.options.text ?? this.args.filter(item => !item.includes(url)).join(" "); const newArgs = this.options.text ?? this.args.filter(item => !item.includes(url)).join(" ");
return { return {
caption: newArgs && newArgs.trim() ? cleanMessage(this.message ?? this.interaction, newArgs) : words.sort(() => 0.5 - Math.random()).slice(0, Math.floor(Math.random() * words.length + 1)).join(" "), caption: newArgs && newArgs.trim() ? cleanMessage(this.message ?? this.interaction, newArgs) : words.sort(() => 0.5 - Math.random()).slice(0, Math.floor(Math.random() * words.length + 1)).join(" "),
top: !!this.options.top, top: !!this.options.top,
font: typeof this.options.font === "string" && this.constructor.allowedFonts.includes(this.options.font.toLowerCase()) ? this.options.font.toLowerCase() : "helvetica" font: typeof this.options.font === "string" && this.constructor.allowedFonts.includes(this.options.font.toLowerCase()) ? this.options.font.toLowerCase() : "helvetica"
}; };
} }
static init() { static init() {
super.init(); super.init();
this.flags.push({ this.flags.push({
name: "top", name: "top",
description: "Put the caption on the top of an image instead of the bottom", description: "Put the caption on the top of an image instead of the bottom",
type: 5 type: 5
}, { }, {
name: "font", name: "font",
type: 3, type: 3,
choices: (() => { choices: (() => {
const array = []; const array = [];
for (const font of this.allowedFonts) { for (const font of this.allowedFonts) {
array.push({ name: font, value: font }); array.push({ name: font, value: font });
} }
return array; return array;
})(), })(),
description: "Specify the font you want to use (default: helvetica)" description: "Specify the font you want to use (default: helvetica)"
}); });
return this; return this;
} }
static description = "Adds a me.me caption/tag list to an image"; static description = "Adds a me.me caption/tag list to an image";
static aliases = ["tags2", "meirl", "memecaption", "medotmecaption"]; static aliases = ["tags2", "meirl", "memecaption", "medotmecaption"];
static arguments = ["{text}"]; static arguments = ["{text}"];
static textOptional = true; static textOptional = true;
static noText = "You need to provide some text to add a caption!"; static noText = "You need to provide some text to add a caption!";
static noImage = "You need to provide an image/GIF to add a caption!"; static noImage = "You need to provide an image/GIF to add a caption!";
static command = "captionTwo"; static command = "captionTwo";
} }
export default CaptionTwoCommand; export default CaptionTwoCommand;

View File

@ -1,11 +1,11 @@
import ImageCommand from "../../classes/imageCommand.js"; import ImageCommand from "../../classes/imageCommand.js";
class CircleCommand extends ImageCommand { class CircleCommand extends ImageCommand {
static description = "Applies a radial blur effect on an image"; static description = "Applies a radial blur effect on an image";
static aliases = ["cblur", "radial", "radialblur"]; static aliases = ["cblur", "radial", "radialblur"];
static noImage = "You need to provide an image/GIF to add radial blur!"; static noImage = "You need to provide an image/GIF to add radial blur!";
static command = "circle"; static command = "circle";
} }
export default CircleCommand; export default CircleCommand;

View File

@ -1,10 +1,10 @@
import ImageCommand from "../../classes/imageCommand.js"; import ImageCommand from "../../classes/imageCommand.js";
class CropCommand extends ImageCommand { class CropCommand extends ImageCommand {
static description = "Crops an image to 1:1"; static description = "Crops an image to 1:1";
static noImage = "You need to provide an image/GIF to crop!"; static noImage = "You need to provide an image/GIF to crop!";
static command = "crop"; static command = "crop";
} }
export default CropCommand; export default CropCommand;

View File

@ -1,11 +1,11 @@
import ImageCommand from "../../classes/imageCommand.js"; import ImageCommand from "../../classes/imageCommand.js";
class DeepfryCommand extends ImageCommand { class DeepfryCommand extends ImageCommand {
static description = "Deep-fries an image"; static description = "Deep-fries an image";
static aliases = ["fry", "jpeg2", "nuke", "df"]; static aliases = ["fry", "jpeg2", "nuke", "df"];
static noImage = "You need to provide an image/GIF to fry!"; static noImage = "You need to provide an image/GIF to fry!";
static command = "deepfry"; static command = "deepfry";
} }
export default DeepfryCommand; export default DeepfryCommand;

View File

@ -1,17 +1,17 @@
import ImageCommand from "../../classes/imageCommand.js"; import ImageCommand from "../../classes/imageCommand.js";
class DeviantArtCommand extends ImageCommand { class DeviantArtCommand extends ImageCommand {
params = { params = {
water: "assets/images/deviantart.png", water: "assets/images/deviantart.png",
gravity: 5, gravity: 5,
resize: true resize: true
}; };
static description = "Adds a DeviantArt watermark to an image"; static description = "Adds a DeviantArt watermark to an image";
static aliases = ["da", "deviant"]; static aliases = ["da", "deviant"];
static noImage = "You need to provide an image/GIF to add a DeviantArt watermark!"; static noImage = "You need to provide an image/GIF to add a DeviantArt watermark!";
static command = "watermark"; static command = "watermark";
} }
export default DeviantArtCommand; export default DeviantArtCommand;

View File

@ -1,11 +1,11 @@
import ImageCommand from "../../classes/imageCommand.js"; import ImageCommand from "../../classes/imageCommand.js";
class ExplodeCommand extends ImageCommand { class ExplodeCommand extends ImageCommand {
static description = "Explodes an image"; static description = "Explodes an image";
static aliases = ["exp"]; static aliases = ["exp"];
static noImage = "You need to provide an image/GIF to explode!"; static noImage = "You need to provide an image/GIF to explode!";
static command = "explode"; static command = "explode";
} }
export default ExplodeCommand; export default ExplodeCommand;

View File

@ -1,45 +1,45 @@
import fs from "fs"; import fs from "fs";
import emojiRegex from "emoji-regex"; import emojiRegex from "emoji-regex";
import emoji from "node-emoji"; import emoji from "node-emoji";
import ImageCommand from "../../classes/imageCommand.js"; import ImageCommand from "../../classes/imageCommand.js";
class FlagCommand extends ImageCommand { class FlagCommand extends ImageCommand {
flagPath = ""; flagPath = "";
async criteria() { async criteria() {
const text = this.options.text ?? this.args[0]; const text = this.options.text ?? this.args[0];
if (!text.match(emojiRegex())) return false; if (!text.match(emojiRegex())) return false;
const flag = emoji.unemojify(text).replaceAll(":", "").replace("flag-", ""); const flag = emoji.unemojify(text).replaceAll(":", "").replace("flag-", "");
let path = `assets/images/region-flags/png/${flag.toUpperCase()}.png`; let path = `assets/images/region-flags/png/${flag.toUpperCase()}.png`;
if (flag === "pirate_flag") path = "assets/images/pirateflag.png"; if (flag === "pirate_flag") path = "assets/images/pirateflag.png";
if (flag === "rainbow-flag") path = "assets/images/rainbowflag.png"; if (flag === "rainbow-flag") path = "assets/images/rainbowflag.png";
if (flag === "checkered_flag") path = "assets/images/checkeredflag.png"; if (flag === "checkered_flag") path = "assets/images/checkeredflag.png";
if (flag === "transgender_flag") path = "assets/images/transflag.png"; if (flag === "transgender_flag") path = "assets/images/transflag.png";
if (text === "🏴󠁧󠁢󠁳󠁣󠁴󠁿") path = "assets/images/region-flags/png/GB-SCT.png"; if (text === "🏴󠁧󠁢󠁳󠁣󠁴󠁿") path = "assets/images/region-flags/png/GB-SCT.png";
if (text === "🏴󠁧󠁢󠁷󠁬󠁳󠁿") path = "assets/images/region-flags/png/GB-WLS.png"; if (text === "🏴󠁧󠁢󠁷󠁬󠁳󠁿") path = "assets/images/region-flags/png/GB-WLS.png";
if (text === "🏴󠁧󠁢󠁥󠁮󠁧󠁿") path = "assets/images/region-flags/png/GB-ENG.png"; if (text === "🏴󠁧󠁢󠁥󠁮󠁧󠁿") path = "assets/images/region-flags/png/GB-ENG.png";
try { try {
await fs.promises.access(path); await fs.promises.access(path);
this.flagPath = path; this.flagPath = path;
return true; return true;
} catch { } catch {
return false; return false;
} }
} }
params() { params() {
return { return {
overlay: this.flagPath overlay: this.flagPath
}; };
} }
static description = "Overlays a flag onto an image"; static description = "Overlays a flag onto an image";
static arguments = ["[flag]"]; static arguments = ["[flag]"];
static requiresText = true; static requiresText = true;
static noText = "You need to provide an emoji of a flag to overlay!"; static noText = "You need to provide an emoji of a flag to overlay!";
static noImage = "You need to provide an image/GIF to overlay a flag onto!"; static noImage = "You need to provide an image/GIF to overlay a flag onto!";
static command = "flag"; static command = "flag";
} }
export default FlagCommand; export default FlagCommand;

View File

@ -1,10 +1,10 @@
import ImageCommand from "../../classes/imageCommand.js"; import ImageCommand from "../../classes/imageCommand.js";
class FlipCommand extends ImageCommand { class FlipCommand extends ImageCommand {
static description = "Flips an image"; static description = "Flips an image";
static noImage = "You need to provide an image/GIF to flip!"; static noImage = "You need to provide an image/GIF to flip!";
static command = "flip"; static command = "flip";
} }
export default FlipCommand; export default FlipCommand;

View File

@ -1,15 +1,15 @@
import ImageCommand from "../../classes/imageCommand.js"; import ImageCommand from "../../classes/imageCommand.js";
class FlopCommand extends ImageCommand { class FlopCommand extends ImageCommand {
params = { params = {
flop: true flop: true
}; };
static description = "Flips an image"; static description = "Flips an image";
static aliases = ["flip2"]; static aliases = ["flip2"];
static noImage = "You need to provide an image/GIF to flop!"; static noImage = "You need to provide an image/GIF to flop!";
static command = "flip"; static command = "flip";
} }
export default FlopCommand; export default FlopCommand;

View File

@ -1,32 +1,32 @@
import ImageCommand from "../../classes/imageCommand.js"; import ImageCommand from "../../classes/imageCommand.js";
class FreezeCommand extends ImageCommand { class FreezeCommand extends ImageCommand {
params() { params() {
const frameCount = parseInt(this.options.endframe ?? this.args[0]); const frameCount = parseInt(this.options.endframe ?? this.args[0]);
return { return {
loop: false, loop: false,
frame: isNaN(frameCount) ? -1 : frameCount frame: isNaN(frameCount) ? -1 : frameCount
}; };
} }
static init() { static init() {
super.init(); super.init();
this.flags.push({ this.flags.push({
name: "endframe", name: "endframe",
type: 4, type: 4,
description: "Set the end frame (default: last frame)", description: "Set the end frame (default: last frame)",
min_value: 0 min_value: 0
}); });
return this; return this;
} }
static description = "Makes an image sequence only play once"; static description = "Makes an image sequence only play once";
static aliases = ["noloop", "once"]; static aliases = ["noloop", "once"];
static arguments = ["{end frame number}"]; static arguments = ["{end frame number}"];
static requiresGIF = true; static requiresGIF = true;
static noImage = "You need to provide an image/GIF to freeze!"; static noImage = "You need to provide an image/GIF to freeze!";
static command = "freeze"; static command = "freeze";
} }
export default FreezeCommand; export default FreezeCommand;

View File

@ -1,17 +1,17 @@
import ImageCommand from "../../classes/imageCommand.js"; import ImageCommand from "../../classes/imageCommand.js";
class FunkyCommand extends ImageCommand { class FunkyCommand extends ImageCommand {
params = { params = {
water: "assets/images/funky.png", water: "assets/images/funky.png",
gravity: 3, gravity: 3,
resize: true resize: true
}; };
static description = "Adds the New Funky Mode banner to an image"; static description = "Adds the New Funky Mode banner to an image";
static aliases = ["funkymode", "newfunkymode", "funkykong"]; static aliases = ["funkymode", "newfunkymode", "funkykong"];
static noImage = "You need to provide an image/GIF to add a New Funky Mode banner!"; static noImage = "You need to provide an image/GIF to add a New Funky Mode banner!";
static command = "watermark"; static command = "watermark";
} }
export default FunkyCommand; export default FunkyCommand;

View File

@ -1,11 +1,11 @@
import ImageCommand from "../../classes/imageCommand.js"; import ImageCommand from "../../classes/imageCommand.js";
class GameXplainCommand extends ImageCommand { class GameXplainCommand extends ImageCommand {
static description = "Makes a GameXplain thumbnail from an image"; static description = "Makes a GameXplain thumbnail from an image";
static aliases = ["gx"]; static aliases = ["gx"];
static noImage = "You need to provide an image/GIF to make a GameXplain thumbnail from!"; static noImage = "You need to provide an image/GIF to make a GameXplain thumbnail from!";
static command = "gamexplain"; static command = "gamexplain";
} }
export default GameXplainCommand; export default GameXplainCommand;

View File

@ -1,11 +1,11 @@
import ImageCommand from "../../classes/imageCommand.js"; import ImageCommand from "../../classes/imageCommand.js";
class GIFCommand extends ImageCommand { class GIFCommand extends ImageCommand {
static description = "Converts an image into a GIF"; static description = "Converts an image into a GIF";
static aliases = ["gif", "getgif", "togif", "tgif", "gifify"]; static aliases = ["gif", "getgif", "togif", "tgif", "gifify"];
static noImage = "You need to provide an image to convert to GIF!"; static noImage = "You need to provide an image to convert to GIF!";
static command = "togif"; static command = "togif";
} }
export default GIFCommand; export default GIFCommand;

View File

@ -1,11 +1,11 @@
import ImageCommand from "../../classes/imageCommand.js"; import ImageCommand from "../../classes/imageCommand.js";
class GlobeCommand extends ImageCommand { class GlobeCommand extends ImageCommand {
static description = "Spins an image"; static description = "Spins an image";
static aliases = ["sphere"]; static aliases = ["sphere"];
static noImage = "You need to provide an image/GIF to spin!"; static noImage = "You need to provide an image/GIF to spin!";
static command = "globe"; static command = "globe";
} }
export default GlobeCommand; export default GlobeCommand;

View File

@ -1,15 +1,15 @@
import ImageCommand from "../../classes/imageCommand.js"; import ImageCommand from "../../classes/imageCommand.js";
class GrayscaleCommand extends ImageCommand { class GrayscaleCommand extends ImageCommand {
params = { params = {
color: "grayscale" color: "grayscale"
}; };
static description = "Adds a grayscale filter"; static description = "Adds a grayscale filter";
static noImage = "You need to provide an image/GIF to turn grayscale!"; static noImage = "You need to provide an image/GIF to turn grayscale!";
static command = "colors"; static command = "colors";
static aliases = ["gray", "greyscale", "grey"]; static aliases = ["gray", "greyscale", "grey"];
} }
export default GrayscaleCommand; export default GrayscaleCommand;

View File

@ -1,15 +1,15 @@
import ImageCommand from "../../classes/imageCommand.js"; import ImageCommand from "../../classes/imageCommand.js";
class HaaHCommand extends ImageCommand { class HaaHCommand extends ImageCommand {
params = { params = {
first: true first: true
}; };
static description = "Mirrors the left side of an image onto the right"; static description = "Mirrors the left side of an image onto the right";
static aliases = ["magik4", "mirror2"]; static aliases = ["magik4", "mirror2"];
static noImage = "You need to provide an image/GIF to mirror!"; static noImage = "You need to provide an image/GIF to mirror!";
static command = "mirror"; static command = "mirror";
} }
export default HaaHCommand; export default HaaHCommand;

View File

@ -1,15 +1,15 @@
import ImageCommand from "../../classes/imageCommand.js"; import ImageCommand from "../../classes/imageCommand.js";
class HooHCommand extends ImageCommand { class HooHCommand extends ImageCommand {
params = { params = {
vertical: true vertical: true
}; };
static description = "Mirrors the bottom of an image onto the top"; static description = "Mirrors the bottom of an image onto the top";
static aliases = ["magik6", "mirror4"]; static aliases = ["magik6", "mirror4"];
static noImage = "You need to provide an image/GIF to mirror!"; static noImage = "You need to provide an image/GIF to mirror!";
static command = "mirror"; static command = "mirror";
} }
export default HooHCommand; export default HooHCommand;

View File

@ -1,17 +1,17 @@
import ImageCommand from "../../classes/imageCommand.js"; import ImageCommand from "../../classes/imageCommand.js";
class HypercamCommand extends ImageCommand { class HypercamCommand extends ImageCommand {
params = { params = {
water: "assets/images/hypercam.png", water: "assets/images/hypercam.png",
gravity: 1, gravity: 1,
resize: true resize: true
}; };
static description = "Adds the Hypercam watermark to an image"; static description = "Adds the Hypercam watermark to an image";
static aliases = ["hcam"]; static aliases = ["hcam"];
static noImage = "You need to provide an image/GIF to add a Hypercam watermark!"; static noImage = "You need to provide an image/GIF to add a Hypercam watermark!";
static command = "watermark"; static command = "watermark";
} }
export default HypercamCommand; export default HypercamCommand;

View File

@ -1,17 +1,17 @@
import ImageCommand from "../../classes/imageCommand.js"; import ImageCommand from "../../classes/imageCommand.js";
class iFunnyCommand extends ImageCommand { class iFunnyCommand extends ImageCommand {
params = { params = {
water: "assets/images/ifunny.png", water: "assets/images/ifunny.png",
gravity: 8, gravity: 8,
resize: true, resize: true,
append: true append: true
}; };
static description = "Adds the iFunny watermark to an image"; static description = "Adds the iFunny watermark to an image";
static noImage = "You need to provide an image/GIF to add an iFunny watermark!"; static noImage = "You need to provide an image/GIF to add an iFunny watermark!";
static command = "watermark"; static command = "watermark";
} }
export default iFunnyCommand; export default iFunnyCommand;

View File

@ -1,15 +1,15 @@
import ImageCommand from "../../classes/imageCommand.js"; import ImageCommand from "../../classes/imageCommand.js";
class ImplodeCommand extends ImageCommand { class ImplodeCommand extends ImageCommand {
params = { params = {
implode: true implode: true
}; };
static description = "Implodes an image"; static description = "Implodes an image";
static aliases = ["imp"]; static aliases = ["imp"];
static noImage = "You need to provide an image/GIF to implode!"; static noImage = "You need to provide an image/GIF to implode!";
static command = "explode"; static command = "explode";
} }
export default ImplodeCommand; export default ImplodeCommand;

View File

@ -1,11 +1,11 @@
import ImageCommand from "../../classes/imageCommand.js"; import ImageCommand from "../../classes/imageCommand.js";
class InvertCommand extends ImageCommand { class InvertCommand extends ImageCommand {
static description = "Inverts an image"; static description = "Inverts an image";
static aliases = ["inverse", "negate", "negative"]; static aliases = ["inverse", "negate", "negative"];
static noImage = "You need to provide an image/GIF to invert!"; static noImage = "You need to provide an image/GIF to invert!";
static command = "invert"; static command = "invert";
} }
export default InvertCommand; export default InvertCommand;

View File

@ -1,31 +1,31 @@
import ImageCommand from "../../classes/imageCommand.js"; import ImageCommand from "../../classes/imageCommand.js";
class JPEGCommand extends ImageCommand { class JPEGCommand extends ImageCommand {
params() { params() {
const quality = parseInt(this.options.quality ?? this.args[0]); const quality = parseInt(this.options.quality ?? this.args[0]);
return { return {
quality: isNaN(quality) ? 1 : Math.max(1, Math.min(quality, 100)) quality: isNaN(quality) ? 1 : Math.max(1, Math.min(quality, 100))
}; };
} }
static init() { static init() {
super.init(); super.init();
this.flags.push({ this.flags.push({
name: "quality", name: "quality",
type: 4, type: 4,
description: "Set the JPEG quality (default: 1)", description: "Set the JPEG quality (default: 1)",
min_value: 1, min_value: 1,
max_value: 100 max_value: 100
}); });
return this; return this;
} }
static description = "Adds JPEG compression to an image"; static description = "Adds JPEG compression to an image";
static aliases = ["needsmorejpeg", "jpegify", "magik2", "morejpeg", "jpg", "quality"]; static aliases = ["needsmorejpeg", "jpegify", "magik2", "morejpeg", "jpg", "quality"];
static arguments = ["{quality}"]; static arguments = ["{quality}"];
static noImage = "You need to provide an image/GIF to add more JPEG!"; static noImage = "You need to provide an image/GIF to add more JPEG!";
static command = "jpeg"; static command = "jpeg";
} }
export default JPEGCommand; export default JPEGCommand;

View File

@ -1,17 +1,17 @@
import ImageCommand from "../../classes/imageCommand.js"; import ImageCommand from "../../classes/imageCommand.js";
class KineMasterCommand extends ImageCommand { class KineMasterCommand extends ImageCommand {
params = { params = {
water: "assets/images/kinemaster.png", water: "assets/images/kinemaster.png",
gravity: 3, gravity: 3,
resize: true resize: true
}; };
static description = "Adds the KineMaster watermark to an image"; static description = "Adds the KineMaster watermark to an image";
static aliases = ["kine"]; static aliases = ["kine"];
static noImage = "You need to provide an image/GIF to add a KineMaster watermark!"; static noImage = "You need to provide an image/GIF to add a KineMaster watermark!";
static command = "watermark"; static command = "watermark";
} }
export default KineMasterCommand; export default KineMasterCommand;

View File

@ -1,11 +1,11 @@
import ImageCommand from "../../classes/imageCommand.js"; import ImageCommand from "../../classes/imageCommand.js";
class MagikCommand extends ImageCommand { class MagikCommand extends ImageCommand {
static description = "Adds a content aware scale effect to an image"; static description = "Adds a content aware scale effect to an image";
static aliases = ["imagemagic", "imagemagick", "imagemagik", "magic", "magick", "cas", "liquid"]; static aliases = ["imagemagic", "imagemagick", "imagemagik", "magic", "magick", "cas", "liquid"];
static noImage = "You need to provide an image/GIF to add some magik!"; static noImage = "You need to provide an image/GIF to add some magik!";
static command = "magik"; static command = "magik";
} }
export default MagikCommand; export default MagikCommand;

View File

@ -1,54 +1,54 @@
import ImageCommand from "../../classes/imageCommand.js"; import ImageCommand from "../../classes/imageCommand.js";
import { cleanMessage } from "../../utils/misc.js"; import { cleanMessage } from "../../utils/misc.js";
class MemeCommand extends ImageCommand { class MemeCommand extends ImageCommand {
async criteria(text, url) { async criteria(text, url) {
const [topText, bottomText] = text.replaceAll(url, "").split(/(?<!\\),/).map(elem => elem.trim()); const [topText, bottomText] = text.replaceAll(url, "").split(/(?<!\\),/).map(elem => elem.trim());
if (topText === "" && bottomText === "") { if (topText === "" && bottomText === "") {
return false; return false;
} else { } else {
return true; return true;
} }
} }
params(url) { params(url) {
const newArgs = this.options.text ?? this.args.join(" "); const newArgs = this.options.text ?? this.args.join(" ");
const [topText, bottomText] = newArgs.replaceAll(url, "").split(/(?<!\\),/).map(elem => elem.trim()); const [topText, bottomText] = newArgs.replaceAll(url, "").split(/(?<!\\),/).map(elem => elem.trim());
return { return {
top: cleanMessage(this.message ?? this.interaction, this.options.case ? topText : topText.toUpperCase()), top: cleanMessage(this.message ?? this.interaction, this.options.case ? topText : topText.toUpperCase()),
bottom: bottomText ? cleanMessage(this.message ?? this.interaction, this.options.case ? bottomText : bottomText.toUpperCase()) : "", bottom: bottomText ? cleanMessage(this.message ?? this.interaction, this.options.case ? bottomText : bottomText.toUpperCase()) : "",
font: typeof this.options.font === "string" && this.constructor.allowedFonts.includes(this.options.font.toLowerCase()) ? this.options.font.toLowerCase() : "impact" font: typeof this.options.font === "string" && this.constructor.allowedFonts.includes(this.options.font.toLowerCase()) ? this.options.font.toLowerCase() : "impact"
}; };
} }
static init() { static init() {
super.init(); super.init();
this.flags.push({ this.flags.push({
name: "case", name: "case",
description: "Make the meme text case-sensitive (allows for lowercase text)", description: "Make the meme text case-sensitive (allows for lowercase text)",
type: 5 type: 5
}, { }, {
name: "font", name: "font",
type: 3, type: 3,
choices: (() => { choices: (() => {
const array = []; const array = [];
for (const font of this.allowedFonts) { for (const font of this.allowedFonts) {
array.push({ name: font, value: font }); array.push({ name: font, value: font });
} }
return array; return array;
})(), })(),
description: "Specify the font you want to use (default: impact)" description: "Specify the font you want to use (default: impact)"
}); });
return this; return this;
} }
static description = "Generates a meme from an image (separate top/bottom text with a comma)"; static description = "Generates a meme from an image (separate top/bottom text with a comma)";
static arguments = ["[top text]", "{bottom text}"]; static arguments = ["[top text]", "{bottom text}"];
static requiresText = true; static requiresText = true;
static noText = "You need to provide some text to generate a meme!"; static noText = "You need to provide some text to generate a meme!";
static noImage = "You need to provide an image/GIF to generate a meme!"; static noImage = "You need to provide an image/GIF to generate a meme!";
static command = "meme"; static command = "meme";
} }
export default MemeCommand; export default MemeCommand;

View File

@ -1,17 +1,17 @@
import ImageCommand from "../../classes/imageCommand.js"; import ImageCommand from "../../classes/imageCommand.js";
class MemeCenterCommand extends ImageCommand { class MemeCenterCommand extends ImageCommand {
params = { params = {
water: "assets/images/memecenter.png", water: "assets/images/memecenter.png",
gravity: 9, gravity: 9,
mc: true mc: true
}; };
static description = "Adds the MemeCenter watermark to an image"; static description = "Adds the MemeCenter watermark to an image";
static aliases = ["memec", "mcenter"]; static aliases = ["memec", "mcenter"];
static noImage = "You need to provide an image/GIF to add a MemeCenter watermark!"; static noImage = "You need to provide an image/GIF to add a MemeCenter watermark!";
static command = "watermark"; static command = "watermark";
} }
export default MemeCenterCommand; export default MemeCenterCommand;

View File

@ -1,51 +1,51 @@
import ImageCommand from "../../classes/imageCommand.js"; import ImageCommand from "../../classes/imageCommand.js";
import { cleanMessage } from "../../utils/misc.js"; import { cleanMessage } from "../../utils/misc.js";
class MotivateCommand extends ImageCommand { class MotivateCommand extends ImageCommand {
async criteria(text, url) { async criteria(text, url) {
const [topText, bottomText] = text.replaceAll(url, "").split(/(?<!\\),/).map(elem => elem.trim()); const [topText, bottomText] = text.replaceAll(url, "").split(/(?<!\\),/).map(elem => elem.trim());
if (topText === "" && bottomText === "") { if (topText === "" && bottomText === "") {
return false; return false;
} else { } else {
return true; return true;
} }
} }
params(url) { params(url) {
const newArgs = this.options.text ?? this.args.join(" "); const newArgs = this.options.text ?? this.args.join(" ");
const [topText, bottomText] = newArgs.replaceAll(url, "").split(/(?<!\\),/).map(elem => elem.trim()); const [topText, bottomText] = newArgs.replaceAll(url, "").split(/(?<!\\),/).map(elem => elem.trim());
return { return {
top: cleanMessage(this.message ?? this.interaction, topText), top: cleanMessage(this.message ?? this.interaction, topText),
bottom: bottomText ? cleanMessage(this.message ?? this.interaction, bottomText) : "", bottom: bottomText ? cleanMessage(this.message ?? this.interaction, bottomText) : "",
font: typeof this.options.font === "string" && this.constructor.allowedFonts.includes(this.options.font.toLowerCase()) ? this.options.font.toLowerCase() : "times" font: typeof this.options.font === "string" && this.constructor.allowedFonts.includes(this.options.font.toLowerCase()) ? this.options.font.toLowerCase() : "times"
}; };
} }
static init() { static init() {
super.init(); super.init();
this.flags.push({ this.flags.push({
name: "font", name: "font",
type: 3, type: 3,
choices: (() => { choices: (() => {
const array = []; const array = [];
for (const font of this.allowedFonts) { for (const font of this.allowedFonts) {
array.push({ name: font, value: font }); array.push({ name: font, value: font });
} }
return array; return array;
})(), })(),
description: "Specify the font you want to use (default: times)" description: "Specify the font you want to use (default: times)"
}); });
return this; return this;
} }
static description = "Generates a motivational poster"; static description = "Generates a motivational poster";
static aliases = ["motivational", "motiv", "demotiv", "demotivational", "poster", "motivation", "demotivate"]; static aliases = ["motivational", "motiv", "demotiv", "demotivational", "poster", "motivation", "demotivate"];
static arguments = ["[top text]", "{bottom text}"]; static arguments = ["[top text]", "{bottom text}"];
static requiresText = true; static requiresText = true;
static noText = "You need to provide some text to generate a motivational poster!"; static noText = "You need to provide some text to generate a motivational poster!";
static noImage = "You need to provide an image/GIF to generate a motivational poster!"; static noImage = "You need to provide an image/GIF to generate a motivational poster!";
static command = "motivate"; static command = "motivate";
} }
export default MotivateCommand; export default MotivateCommand;

View File

@ -1,11 +1,11 @@
import ImageCommand from "../../classes/imageCommand.js"; import ImageCommand from "../../classes/imageCommand.js";
class PixelateCommand extends ImageCommand { class PixelateCommand extends ImageCommand {
static description = "Pixelates an image"; static description = "Pixelates an image";
static aliases = ["pixel", "small"]; static aliases = ["pixel", "small"];
static noImage = "You need to provide an image/GIF to pixelate!"; static noImage = "You need to provide an image/GIF to pixelate!";
static command = "resize"; static command = "resize";
} }
export default PixelateCommand; export default PixelateCommand;

View File

@ -1,22 +1,22 @@
import ImageCommand from "../../classes/imageCommand.js"; import ImageCommand from "../../classes/imageCommand.js";
import { random } from "../../utils/misc.js"; import { random } from "../../utils/misc.js";
const names = ["esmBot", "me_irl", "dankmemes", "hmmm", "gaming", "wholesome", "chonkers", "memes", "funny", "lies"]; const names = ["esmBot", "me_irl", "dankmemes", "hmmm", "gaming", "wholesome", "chonkers", "memes", "funny", "lies"];
class RedditCommand extends ImageCommand { class RedditCommand extends ImageCommand {
params(url) { params(url) {
const newArgs = this.options.text ?? this.args.filter(item => !item.includes(url)).join(" "); const newArgs = this.options.text ?? this.args.filter(item => !item.includes(url)).join(" ");
return { return {
caption: newArgs?.trim() ? newArgs.replaceAll("\n", "").replaceAll(" ", "") : random(names) caption: newArgs?.trim() ? newArgs.replaceAll("\n", "").replaceAll(" ", "") : random(names)
}; };
} }
static textOptional = true; static textOptional = true;
static description = "Adds a Reddit watermark to an image"; static description = "Adds a Reddit watermark to an image";
static arguments = ["{text}"]; static arguments = ["{text}"];
static noText = "You need to provide some text to add a Reddit watermark!"; static noText = "You need to provide some text to add a Reddit watermark!";
static command = "reddit"; static command = "reddit";
} }
export default RedditCommand; export default RedditCommand;

View File

@ -1,12 +1,12 @@
import ImageCommand from "../../classes/imageCommand.js"; import ImageCommand from "../../classes/imageCommand.js";
class ReverseCommand extends ImageCommand { class ReverseCommand extends ImageCommand {
static description = "Reverses an image sequence"; static description = "Reverses an image sequence";
static aliases = ["backwards"]; static aliases = ["backwards"];
static requiresGIF = true; static requiresGIF = true;
static noImage = "You need to provide an image/GIF to reverse!"; static noImage = "You need to provide an image/GIF to reverse!";
static command = "reverse"; static command = "reverse";
} }
export default ReverseCommand; export default ReverseCommand;

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