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
**/build
**/logs
**/node_modules
**/build
**/logs
**/data

View file

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

View file

@ -1,54 +1,54 @@
############
# Required #
############
# Put environment type here (development, staging, or production)
NODE_ENV=development
# Put Discord bot token here, you can get it from https://discord.com/developers/applications
TOKEN=
# Put the database connection URL here
# Example for SQLite:
DB=sqlite://data.sqlite
# Example for PostgreSQL:
# 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,
# then right clicking on your profile picture and selecting Copy ID)
OWNER=
# Put default classic command prefix here
PREFIX=&
############
# Optional #
############
# 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)
STAYVC=false
# Set this to true to disable music playback from YouTube
YT_DISABLED=false
# Put Tenor API key here (used for obtaining raw tenor GIF urls)
TENOR=
# Put HTML help page output location here, leave blank to disable
OUTPUT=
# Put temporary image dir here (make sure it's accessible via a web server), leave blank to disable
TEMPDIR=
# Put temporary image web server domain
TMP_DOMAIN=
# Threshold where optional space saving methods will be performed
THRESHOLD=
# Port for serving metrics. Metrics served are compatible with Prometheus.
METRICS=
# The image API type to be used
# 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
API_TYPE=none
# Put ID of server to limit owner-only commands to
############
# Required #
############
# Put environment type here (development, staging, or production)
NODE_ENV=development
# Put Discord bot token here, you can get it from https://discord.com/developers/applications
TOKEN=
# Put the database connection URL here
# Example for SQLite:
DB=sqlite://data.sqlite
# Example for PostgreSQL:
# 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,
# then right clicking on your profile picture and selecting Copy ID)
OWNER=
# Put default classic command prefix here
PREFIX=&
############
# Optional #
############
# 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)
STAYVC=false
# Set this to true to disable music playback from YouTube
YT_DISABLED=false
# Put Tenor API key here (used for obtaining raw tenor GIF urls)
TENOR=
# Put HTML help page output location here, leave blank to disable
OUTPUT=
# Put temporary image dir here (make sure it's accessible via a web server), leave blank to disable
TEMPDIR=
# Put temporary image web server domain
TMP_DOMAIN=
# Threshold where optional space saving methods will be performed
THRESHOLD=
# Port for serving metrics. Metrics served are compatible with Prometheus.
METRICS=
# The image API type to be used
# 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
API_TYPE=none
# Put ID of server to limit owner-only commands to
ADMIN_SERVER=

View file

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

2
.github/FUNDING.yml vendored
View file

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

View file

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

View file

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

View file

@ -1,25 +1,25 @@
name: Feature Request
description: Suggest an idea for this project
labels: [enhancement]
body:
- type: textarea
id: describe
attributes:
label: Describe the request
description: What do you want to be added to the bot?
validations:
required: true
- type: textarea
id: problem
attributes:
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 [...]
validations:
required: false
- type: textarea
id: context
attributes:
label: Additional context
description: Add any other context or screenshots about the feature request here.
validations:
name: Feature Request
description: Suggest an idea for this project
labels: [enhancement]
body:
- type: textarea
id: describe
attributes:
label: Describe the request
description: What do you want to be added to the bot?
validations:
required: true
- type: textarea
id: problem
attributes:
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 [...]
validations:
required: false
- type: textarea
id: context
attributes:
label: Additional context
description: Add any other context or screenshots about the feature request here.
validations:
required: false

View file

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

View file

@ -1,67 +1,67 @@
# For most projects, this workflow file will not need changing; you simply need
# to commit it to your repository.
#
# You may wish to alter this file to override the set of languages analyzed,
# or to provide custom queries or build logic.
#
# ******** NOTE ********
# 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
# supported CodeQL languages.
#
name: "CodeQL"
on:
push:
branches: [ master ]
pull_request:
# The branches below must be a subset of the branches above
branches: [ master ]
schedule:
- cron: '35 2 * * 0'
jobs:
analyze:
name: Analyze
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
language: [ 'javascript' ]
# CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ]
# 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
steps:
- name: Checkout repository
uses: actions/checkout@v2
- name: Setup Node.js environment
uses: actions/setup-node@v2.1.2
#with:
# Set always-auth in npmrc
#always-auth: # optional, default is false
# Version Spec of the version to use. Examples: 12.x, 10.15.1, >=10.15.0
#node-version: # optional
# Set this option if you want the action to check for the latest available version that satisfies the version spec
#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
#registry-url: # optional
# Optional scope for authenticating against scoped registries
#scope: # optional
# 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 }}
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v2
with:
languages: ${{ matrix.language }}
# 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.
# 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
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v2
# For most projects, this workflow file will not need changing; you simply need
# to commit it to your repository.
#
# You may wish to alter this file to override the set of languages analyzed,
# or to provide custom queries or build logic.
#
# ******** NOTE ********
# 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
# supported CodeQL languages.
#
name: "CodeQL"
on:
push:
branches: [ master ]
pull_request:
# The branches below must be a subset of the branches above
branches: [ master ]
schedule:
- cron: '35 2 * * 0'
jobs:
analyze:
name: Analyze
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
language: [ 'javascript' ]
# CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ]
# 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
steps:
- name: Checkout repository
uses: actions/checkout@v2
- name: Setup Node.js environment
uses: actions/setup-node@v2.1.2
#with:
# Set always-auth in npmrc
#always-auth: # optional, default is false
# Version Spec of the version to use. Examples: 12.x, 10.15.1, >=10.15.0
#node-version: # optional
# Set this option if you want the action to check for the latest available version that satisfies the version spec
#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
#registry-url: # optional
# Optional scope for authenticating against scoped registries
#scope: # optional
# 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 }}
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v2
with:
languages: ${{ matrix.language }}
# 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.
# 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
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v2

View file

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

240
.gitignore vendored
View file

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

6
.gitmodules vendored
View file

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

View file

@ -1,47 +1,47 @@
cmake_minimum_required(VERSION 3.15)
cmake_policy(SET CMP0091 NEW)
cmake_policy(SET CMP0042 NEW)
project(image)
file(GLOB SOURCE_FILES "natives/*.cc" "natives/*.h")
if (CMAKE_JS_VERSION)
include_directories(${CMAKE_JS_INC})
add_library(${PROJECT_NAME} SHARED ${SOURCE_FILES} ${CMAKE_JS_SRC} natives/node/image.cc)
set_target_properties(${PROJECT_NAME} PROPERTIES PREFIX "" SUFFIX ".node")
target_link_libraries(${PROJECT_NAME} ${CMAKE_JS_LIB})
else()
add_executable(${PROJECT_NAME} ${SOURCE_FILES} natives/cli/image.cc)
endif()
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
set(CMAKE_CXX_FLAGS "/Wall /EHsc /GS")
set(CMAKE_CXX_FLAGS_DEBUG "/Zi")
set(CMAKE_CXX_FLAGS_RELEASE "/Ox")
set(CMAKE_WINDOWS_EXPORT_ALL_SYMBOLS ON)
set(BUILD_SHARED_LIBS TRUE)
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_DEBUG "-g")
set(CMAKE_CXX_FLAGS_RELEASE "-O2")
endif()
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
find_package(ImageMagick REQUIRED COMPONENTS Magick++ MagickCore)
add_definitions(-DMAGICKCORE_QUANTUM_DEPTH=16)
add_definitions(-DMAGICKCORE_HDRI_ENABLE=0)
include_directories(${ImageMagick_INCLUDE_DIRS})
target_link_libraries(${PROJECT_NAME} ${ImageMagick_LIBRARIES})
pkg_check_modules(VIPS REQUIRED vips-cpp)
include_directories(${VIPS_INCLUDE_DIRS})
link_directories(${VIPS_LIBRARY_DIRS})
target_link_libraries(${PROJECT_NAME} ${VIPS_LDFLAGS})
if(MSVC AND CMAKE_JS_NODELIB_DEF AND CMAKE_JS_NODELIB_TARGET AND CMAKE_JS_VERSION)
# Generate node.lib
execute_process(COMMAND ${CMAKE_AR} /def:${CMAKE_JS_NODELIB_DEF} /out:${CMAKE_JS_NODELIB_TARGET} ${CMAKE_STATIC_LINKER_FLAGS})
endif()
cmake_minimum_required(VERSION 3.15)
cmake_policy(SET CMP0091 NEW)
cmake_policy(SET CMP0042 NEW)
project(image)
file(GLOB SOURCE_FILES "natives/*.cc" "natives/*.h")
if (CMAKE_JS_VERSION)
include_directories(${CMAKE_JS_INC})
add_library(${PROJECT_NAME} SHARED ${SOURCE_FILES} ${CMAKE_JS_SRC} natives/node/image.cc)
set_target_properties(${PROJECT_NAME} PROPERTIES PREFIX "" SUFFIX ".node")
target_link_libraries(${PROJECT_NAME} ${CMAKE_JS_LIB})
else()
add_executable(${PROJECT_NAME} ${SOURCE_FILES} natives/cli/image.cc)
endif()
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
set(CMAKE_CXX_FLAGS "/Wall /EHsc /GS")
set(CMAKE_CXX_FLAGS_DEBUG "/Zi")
set(CMAKE_CXX_FLAGS_RELEASE "/Ox")
set(CMAKE_WINDOWS_EXPORT_ALL_SYMBOLS ON)
set(BUILD_SHARED_LIBS TRUE)
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_DEBUG "-g")
set(CMAKE_CXX_FLAGS_RELEASE "-O2")
endif()
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
find_package(ImageMagick REQUIRED COMPONENTS Magick++ MagickCore)
add_definitions(-DMAGICKCORE_QUANTUM_DEPTH=16)
add_definitions(-DMAGICKCORE_HDRI_ENABLE=0)
include_directories(${ImageMagick_INCLUDE_DIRS})
target_link_libraries(${PROJECT_NAME} ${ImageMagick_LIBRARIES})
pkg_check_modules(VIPS REQUIRED vips-cpp)
include_directories(${VIPS_INCLUDE_DIRS})
link_directories(${VIPS_LIBRARY_DIRS})
target_link_libraries(${PROJECT_NAME} ${VIPS_LDFLAGS})
if(MSVC AND CMAKE_JS_NODELIB_DEF AND CMAKE_JS_NODELIB_TARGET AND CMAKE_JS_VERSION)
# Generate node.lib
execute_process(COMMAND ${CMAKE_AR} /def:${CMAKE_JS_NODELIB_DEF} /out:${CMAKE_JS_NODELIB_TARGET} ${CMAKE_STATIC_LINKER_FLAGS})
endif()

View file

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

42
LICENSE
View file

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

View file

@ -1,24 +1,24 @@
# 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.
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:
+ User IDs (needed for many reasons such as the tag commands and replying to users)
+ Avatars (needed for some embeds and the avatar command)
+ Usernames (for embeds and avatar command)
+ 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)
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:
+ Guild IDs (for guild-specific settings)
+ 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)
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).
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.
# 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.
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:
+ User IDs (needed for many reasons such as the tag commands and replying to users)
+ Avatars (needed for some embeds and the avatar command)
+ Usernames (for embeds and avatar command)
+ 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)
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:
+ Guild IDs (for guild-specific settings)
+ 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)
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).
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
[![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.
## Features
- 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
- Handling of output images larger than 8MB via a local web server
- 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)
- Server tags system for saving/retrieving content
- Low RAM and CPU usage when idle
- Support for slash/application commands and classic, prefix-based message commands
- Support for multiple database backends (PostgreSQL and SQLite backends included)
- [PM2](https://pm2.keymetrics.io)-based cluster/shard handling
- Flexible command handler allowing you to create new commands by adding script files
## Usage
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).
If you want to self-host the bot, a guide can be found [here](https://docs.esmbot.net/setup).
## Credits
Icon by [Steel](https://twitter.com/MintBurrow).
All images, sounds, and fonts are copyright of their respective owners.
# <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 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
- 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
- Handling of output images larger than 8MB via a local web server
- 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)
- Server tags system for saving/retrieving content
- Low RAM and CPU usage when idle
- Support for slash/application commands and classic, prefix-based message commands
- Support for multiple database backends (PostgreSQL and SQLite backends included)
- [PM2](https://pm2.keymetrics.io)-based cluster/shard handling
- Flexible command handler allowing you to create new commands by adding script files
## Usage
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).
If you want to self-host the bot, a guide can be found [here](https://docs.esmbot.net/setup).
## Credits
Icon by [Steel](https://twitter.com/MintBurrow).
All images, sounds, and fonts are copyright of their respective owners.

View file

@ -1,51 +1,51 @@
# 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.
## HTTP
### GET `/image/?id=<job id>`
Get image data after job is finished running. The Content-Type header is properly set.
### GET `/count`
Get the current amount of running jobs. Response is a plaintext number value.
## WebSockets
A client sends *requests* (T-messages) to a server, which subsequently *replies* (R-messages) to the client.
### Message IDs
- Rerror 0x01
- Tqueue 0x02
- Rqueue 0x03
- Tcancel 0x04
- Rcancel 0x05
- Twait 0x06
- Rwait 0x07
- Rinit 0x08
### Messages
[n] means n bytes.
[s] means a string 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.
- Rerror tag[2] error[s]
- Tqueue tag[2] jid[8] job[j]
- Rqueue tag[2]
- Tcancel tag[2] jid[8]
- Rcancel tag[2]
- Twait tag[2] jid[8]
- Rwait tag[2]
- Rinit tag[2] max_jobs[2] running_jobs[2] formats[j]
### Job Object
The job object is formatted like this:
```js
{
"cmd": string, // name of internal image command, e.g. caption
"path": string, // canonical image URL, used for getting the actual image
"url": string, // original image URL, used for message filtering
"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
...
},
"name": string // filename of the image, without extension
}
```
# 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.
## HTTP
### GET `/image/?id=<job id>`
Get image data after job is finished running. The Content-Type header is properly set.
### GET `/count`
Get the current amount of running jobs. Response is a plaintext number value.
## WebSockets
A client sends *requests* (T-messages) to a server, which subsequently *replies* (R-messages) to the client.
### Message IDs
- Rerror 0x01
- Tqueue 0x02
- Rqueue 0x03
- Tcancel 0x04
- Rcancel 0x05
- Twait 0x06
- Rwait 0x07
- Rinit 0x08
### Messages
[n] means n bytes.
[s] means a string 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.
- Rerror tag[2] error[s]
- Tqueue tag[2] jid[8] job[j]
- Rqueue tag[2]
- Tcancel tag[2] jid[8]
- Rcancel tag[2]
- Twait tag[2] jid[8]
- Rwait tag[2]
- Rinit tag[2] max_jobs[2] running_jobs[2] formats[j]
### Job Object
The job object is formatted like this:
```js
{
"cmd": string, // name of internal image command, e.g. caption
"path": string, // canonical image URL, used for getting the actual image
"url": string, // original image URL, used for message filtering
"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
...
},
"name": string // filename of the image, without extension
}
```

View file

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

416
app.js
View file

@ -1,228 +1,190 @@
if (process.versions.node.split(".")[0] < 16) {
console.error(`You are currently running Node.js version ${process.version}.
esmBot requires Node.js version 16 or above.
Please refer to step 3 of the setup guide.`);
process.exit(1);
}
if (process.platform === "win32") {
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.
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);
}
// load config from .env file
import { resolve, dirname } from "path";
import { fileURLToPath } from "url";
import { config } from "dotenv";
config({ path: resolve(dirname(fileURLToPath(import.meta.url)), ".env") });
import { reloadImageConnections } from "./utils/image.js";
// main services
import { Client } from "oceanic.js";
import pm2 from "pm2";
// some utils
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);
// initialize command loader
import { load } from "./utils/handler.js";
// command collections
import { paths } from "./utils/collections.js";
// database stuff
import database from "./utils/database.js";
// lavalink stuff
import { reload, connect, connected } from "./utils/soundplayer.js";
// events
import { endBroadcast, startBroadcast } from "./utils/misc.js";
import { parseThreshold } from "./utils/tempimages.js";
const { types } = JSON.parse(readFileSync(new URL("./config/commands.json", import.meta.url)));
const esmBotVersion = JSON.parse(readFileSync(new URL("./package.json", import.meta.url))).version;
process.env.ESMBOT_VER = esmBotVersion;
const intents = [
"GUILD_VOICE_STATES",
"DIRECT_MESSAGES",
"GUILDS"
];
if (types.classic) {
intents.push("GUILD_MESSAGES");
intents.push("MESSAGE_CONTENT");
}
async function* getFiles(dir) {
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);
} else if (dirent.name.endsWith(".js")) {
yield name;
}
}
}
async function init() {
await exec("git rev-parse HEAD").then(output => output.stdout.substring(0, 7), () => "unknown commit").then(o => process.env.GIT_REV = o);
console.log(`
,*\`$ z\`"v
F zBw\`% A ,W "W
,\` ,EBBBWp"%. ,-=~~==-,+* 4BBE T
M BBBBBBBB* ,w=####Wpw 4BBBBB# 1
F BBBBBBBMwBBBBBBBBBBBBB#wXBBBBBH E
F BBBBBBkBBBBBBBBBBBBBBBBBBBBE4BL k
# BFBBBBBBBBBBBBF" "RBBBW F
V ' 4BBBBBBBBBBM TBBL F
F BBBBBBBBBBF JBB L
F FBBBBBBBEB BBL 4
E [BB4BBBBEBL BBL 4
I #BBBBBBBEB 4BBH *w
A 4BBBBBBBBBEW, ,BBBB W [
.A ,k 4BBBBBBBBBBBEBW####BBBBBBM BF F
k <BBBw BBBBEBBBBBBBBBBBBBBBBBQ4BM #
5, REBBB4BBBBB#BBBBBBBBBBBBP5BFF ,F
*w \`*4BBW\`"FF#F##FFFF"\` , * +"
*+, " F'"'*^~~~^"^\` V+*^
\`"""
esmBot ${esmBotVersion} (${process.env.GIT_REV})
`);
if (!types.classic && !types.application) {
logger.error("Both classic and application commands are disabled! Please enable at least one command type in config/commands.json.");
return process.exit(1);
}
if (database) {
// database handling
const dbResult = await database.upgrade(logger);
if (dbResult === 1) return process.exit(1);
}
// process the threshold into bytes early
if (process.env.TEMPDIR && process.env.THRESHOLD) {
await parseThreshold();
}
// register commands and their info
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}...`);
try {
await load(null, commandFile);
} catch (e) {
logger.error(`Failed to register command from ${commandFile}: ${e}`);
}
}
logger.log("info", "Finished loading commands.");
if (database) {
await database.setup();
}
if (process.env.API_TYPE === "ws") await reloadImageConnections();
// create the oceanic client
const client = new Client({
auth: `Bot ${process.env.TOKEN}`,
allowedMentions: {
everyone: false,
roles: false,
users: true,
repliedUser: true
},
gateway: {
concurrency: "auto",
maxShards: "auto",
shardIDs: process.env.SHARDS ? JSON.parse(process.env.SHARDS)[process.env.pm_id - 1] : null,
presence: {
status: "idle",
activities: [{
type: 0,
name: "Starting esmBot..."
}]
},
intents
},
collectionLimits: {
messages: 50
}
});
// register events
logger.log("info", "Attempting to load events...");
for await (const file of getFiles(resolve(dirname(fileURLToPath(import.meta.url)), "./events/"))) {
logger.log("main", `Loading event from ${file}...`);
const eventArray = file.split("/");
const eventName = eventArray[eventArray.length - 1].split(".")[0];
if (eventName === "interactionCreate" && !types.application) {
logger.log("warn", `Skipped loading event from ${file} because application commands are disabled`);
continue;
}
const { default: event } = await import(file);
client.on(eventName, event.bind(null, client));
}
logger.log("info", "Finished loading events.");
// PM2-specific handling
if (process.env.PM2_USAGE) {
// callback hell :)
pm2.launchBus((err, pm2Bus) => {
if (err) {
logger.error(err);
return;
}
pm2.list((err, list) => {
if (err) {
logger.error(err);
return;
}
const managerProc = list.filter((v) => v.name === "esmBot-manager")[0];
pm2Bus.on("process:msg", async (packet) => {
switch (packet.data?.type) {
case "reload":
var path = paths.get(packet.data.message);
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();
}
if (process.versions.node.split(".")[0] < 16) {
console.error(`You are currently running Node.js version ${process.version}.
esmBot requires Node.js version 16 or above.
Please refer to step 3 of the setup guide.`);
process.exit(1);
}
if (process.platform === "win32") {
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.
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);
}
// load config from .env file
import { resolve, dirname } from "path";
import { fileURLToPath } from "url";
import { config } from "dotenv";
config({ path: resolve(dirname(fileURLToPath(import.meta.url)), ".env") });
import { reloadImageConnections } from "./utils/image.js";
// main services
// import { Client } from "oceanic.js";
import * as sdk from "matrix-js-sdk";
const AutojoinRoomsMixin = sdk.AutojoinRoomsMixin;
import pm2 from "pm2";
// some utils
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);
// initialize command loader
import { load } from "./utils/handler.js";
// command collections
import { paths } from "./utils/collections.js";
// database stuff
import database from "./utils/database.js";
// lavalink stuff
import { reload, connect, connected } from "./utils/soundplayer.js";
// events
import { endBroadcast, startBroadcast } from "./utils/misc.js";
import { parseThreshold } from "./utils/tempimages.js";
const { types } = JSON.parse(readFileSync(new URL("./config/commands.json", import.meta.url)));
const esmBotVersion = JSON.parse(readFileSync(new URL("./package.json", import.meta.url))).version;
process.env.ESMBOT_VER = esmBotVersion;
// const intents = [
// "GUILD_VOICE_STATES",
// "DIRECT_MESSAGES",
// "GUILDS"
// ];
// if (types.classic) {
// intents.push("GUILD_MESSAGES");
// intents.push("MESSAGE_CONTENT");
// }
async function* getFiles(dir) {
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);
} else if (dirent.name.endsWith(".js")) {
yield name;
}
}
}
async function init() {
await exec("git rev-parse HEAD").then(output => output.stdout.substring(0, 7), () => "unknown commit").then(o => process.env.GIT_REV = o);
console.log(`
,*\`$ z\`"v
F zBw\`% A ,W "W
,\` ,EBBBWp"%. ,-=~~==-,+* 4BBE T
M BBBBBBBB* ,w=####Wpw 4BBBBB# 1
F BBBBBBBMwBBBBBBBBBBBBB#wXBBBBBH E
F BBBBBBkBBBBBBBBBBBBBBBBBBBBE4BL k
# BFBBBBBBBBBBBBF" "RBBBW F
V ' 4BBBBBBBBBBM TBBL F
F BBBBBBBBBBF JBB L
F FBBBBBBBEB BBL 4
E [BB4BBBBEBL BBL 4
I #BBBBBBBEB 4BBH *w
A 4BBBBBBBBBEW, ,BBBB W [
.A ,k 4BBBBBBBBBBBEBW####BBBBBBM BF F
k <BBBw BBBBEBBBBBBBBBBBBBBBBBQ4BM #
5, REBBB4BBBBB#BBBBBBBBBBBBP5BFF ,F
*w \`*4BBW\`"FF#F##FFFF"\` , * +"
*+, " F'"'*^~~~^"^\` V+*^
\`"""
esmBot ${esmBotVersion} (${process.env.GIT_REV})
`);
if (!types.classic && !types.application) {
logger.error("Both classic and application commands are disabled! Please enable at least one command type in config/commands.json.");
return process.exit(1);
}
if (database) {
// database handling
const dbResult = await database.upgrade(logger);
if (dbResult === 1) return process.exit(1);
}
// process the threshold into bytes early
if (process.env.TEMPDIR && process.env.THRESHOLD) {
await parseThreshold();
}
// register commands and their info
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}...`);
try {
await load(null, commandFile);
} catch (e) {
logger.error(`Failed to register command from ${commandFile}: ${e}`);
}
}
logger.log("info", "Finished loading commands.");
if (database) {
await database.setup();
}
if (process.env.API_TYPE === "ws") await reloadImageConnections();
// create the oceanic client
// const client = new Client({
// auth: `Bot ${process.env.TOKEN}`,
// allowedMentions: {
// everyone: false,
// roles: false,
// users: true,
// repliedUser: true
// },
// gateway: {
// concurrency: "auto",
// maxShards: "auto",
// shardIDs: process.env.SHARDS ? JSON.parse(process.env.SHARDS)[process.env.pm_id - 1] : null,
// presence: {
// status: "idle",
// activities: [{
// type: 0,
// name: "Starting esmBot..."
// }]
// },
// intents
// },
// collectionLimits: {
// messages: 50
// }
// });
const myUserId = process.env.MATRIX_USERNAME;
const myAccessToken = process.env.TOKEN;
const matrixClient = sdk.createClient({
baseUrl: process.env.MATRIX_BASEURL,
accessToken: myAccessToken,
userId: myUserId,
});
// register events
logger.log("info", "Attempting to load events...");
// for await (const file of getFiles(resolve(dirname(fileURLToPath(import.meta.url)), "./events/"))) {
// logger.log("main", `Loading event from ${file}...`);
// const eventArray = file.split("/");
// const eventName = eventArray[eventArray.length - 1].split(".")[0];
// if (eventName === "interactionCreate" && !types.application) {
// logger.log("warn", `Skipped loading event from ${file} because application commands are disabled`);
// continue;
// }
// const { default: event } = await import(file);
// // client.on(eventName, event.bind(null, client));
// }
const { default: event } = await import("./events/roommessage.js");
matrixClient.on("Room.timeline", event.bind(null,matrixClient));
logger.log("info", "Finished loading events.");
matrixClient.startClient({ initialSyncLimit: 0 }).then(() => logger.log("info", "Client started!"));
}
init();

View file

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

View file

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

View file

@ -1,153 +1,160 @@
import Command from "./command.js";
import imageDetect from "../utils/imagedetect.js";
import { runImageJob } from "../utils/image.js";
import { runningCommands } from "../utils/collections.js";
import { readFileSync } from "fs";
const { emotes } = JSON.parse(readFileSync(new URL("../config/messages.json", import.meta.url)));
import { random } from "../utils/misc.js";
import { selectedImages } from "../utils/collections.js";
class ImageCommand extends Command {
async criteria() {
return true;
}
async run() {
this.success = false;
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
// if so, don't re-run it
if (runningCommands.has(this.author.id) && (new Date(runningCommands.get(this.author.id)) - new Date(timestamp)) < 5000) {
return "Please slow down a bit.";
}
// before awaiting the command result, add this command to the set of running commands
runningCommands.set(this.author.id, timestamp);
const imageParams = {
cmd: this.constructor.command,
params: {
togif: !!this.options.togif
},
id: (this.interaction ?? this.message).id
};
if (this.type === "application") await this.acknowledge();
if (this.constructor.requiresImage) {
try {
const selection = selectedImages.get(this.author.id);
const image = selection ?? await imageDetect(this.client, this.message, this.interaction, this.options, true);
if (selection) selectedImages.delete(this.author.id);
if (image === undefined) {
runningCommands.delete(this.author.id);
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") {
runningCommands.delete(this.author.id);
return "That image is too large (>= 25MB)! Try using a smaller image.";
} else if (image.type === "tenorlimit") {
runningCommands.delete(this.author.id);
return "I've been rate-limited by Tenor. Please try uploading your GIF elsewhere.";
}
imageParams.path = image.path;
imageParams.params.type = image.type;
imageParams.url = image.url; // technically not required but can be useful for text filtering
imageParams.name = image.name;
if (this.constructor.requiresGIF) imageParams.onlyGIF = true;
} catch (e) {
runningCommands.delete(this.author.id);
throw e;
}
}
if (this.constructor.requiresText) {
const text = this.options.text ?? this.args.join(" ").trim();
if (text.length === 0 || !await this.criteria(text, imageParams.url)) {
runningCommands.delete(this.author.id);
return this.constructor.noText;
}
}
if (typeof this.params === "function") {
Object.assign(imageParams.params, this.params(imageParams.url, imageParams.name));
} else if (typeof this.params === "object") {
Object.assign(imageParams.params, this.params);
}
let status;
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));
}
try {
const { buffer, type } = await runImageJob(imageParams);
if (type === "nogif" && this.constructor.requiresGIF) {
return "That isn't a GIF!";
}
this.success = true;
return {
contents: buffer,
name: `${this.constructor.command}.${type}`
};
} 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 === "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.";
throw e;
} finally {
try {
if (status) await status.delete();
} catch {
// no-op
}
runningCommands.delete(this.author.id);
}
}
processMessage(channel) {
return channel.createMessage({
content: `${random(emotes) || process.env.PROCESSING_EMOJI || "<a:processing:479351417102925854>"} Processing... This might take a while`
});
}
static init() {
this.flags = [];
if (this.requiresText || this.textOptional) {
this.flags.push({
name: "text",
type: 3,
description: "The text to put on the image",
required: !this.textOptional
});
}
if (this.requiresImage) {
this.flags.push({
name: "image",
type: 11,
description: "An image/GIF attachment"
}, {
name: "link",
type: 3,
description: "An image/GIF URL"
});
}
this.flags.push({
name: "togif",
type: 5,
description: "Force GIF output"
});
return this;
}
static allowedFonts = ["futura", "impact", "helvetica", "arial", "roboto", "noto", "times", "comic sans ms"];
static requiresImage = true;
static requiresText = false;
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;
import Command from "./command.js";
import imageDetect from "../utils/imagedetect.js";
import { runImageJob } from "../utils/image.js";
import { runningCommands } from "../utils/collections.js";
import { readFileSync } from "fs";
const { emotes } = JSON.parse(readFileSync(new URL("../config/messages.json", import.meta.url)));
import { random } from "../utils/misc.js";
import { selectedImages } from "../utils/collections.js";
class ImageCommand extends Command {
async criteria() {
return true;
}
async run() {
this.success = false;
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
// if so, don't re-run it
if (runningCommands.has(this.author) && (new Date(runningCommands.get(this.author)) - new Date(timestamp)) < 5000) {
return "Please slow down a bit.";
}
// before awaiting the command result, add this command to the set of running commands
runningCommands.set(this.author, timestamp);
const imageParams = {
cmd: this.constructor.command,
params: {
togif: !!this.options.togif
},
id: this.message.event_id
};
// if (this.type === "application") await this.acknowledge();
if (this.constructor.requiresImage) {
try {
const selection = selectedImages.get(this.author);
const image = await imageDetect(this.client, this.message, this.interaction, this.options, true);
if (selection) selectedImages.delete(this.author);
if (image === undefined) {
runningCommands.delete(this.author);
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") {
runningCommands.delete(this.author);
return "That image is too large (>= 25MB)! Try using a smaller image.";
} else if (image.type === "tenorlimit") {
runningCommands.delete(this.author);
return "I've been rate-limited by Tenor. Please try uploading your GIF elsewhere.";
}
imageParams.path = image.path;
imageParams.params.type = image.type;
imageParams.url = image.url; // technically not required but can be useful for text filtering
imageParams.name = image.name;
if (this.constructor.requiresGIF) imageParams.onlyGIF = true;
} catch (e) {
runningCommands.delete(this.author);
throw e;
}
}
if (this.constructor.requiresText) {
const text = this.options.text ?? this.args.join(" ").trim();
if (text.length === 0 || !await this.criteria(text, imageParams.url)) {
runningCommands.delete(this.author);
return this.constructor.noText;
}
}
if (typeof this.params === "function") {
Object.assign(imageParams.params, this.params(imageParams.url, imageParams.name));
} else if (typeof this.params === "object") {
Object.assign(imageParams.params, this.params);
}
let status;
if (imageParams.params.type === "image/gif" && this.type === "classic") {
status = await this.processMessage(this.message.room_id ?? await this.client.rest.channels.get(this.message.room_id));
}
try {
const { buffer, type } = await runImageJob(imageParams);
if (type === "nogif" && this.constructor.requiresGIF) {
return "That isn't a GIF!";
}
this.success = true;
return {
contents: buffer,
name: `${this.constructor.command}.${type}`
};
} 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 === "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.";
throw e;
} finally {
try {
if (status) await status.delete();
} catch {
// no-op
}
runningCommands.delete(this.author);
}
}
processMessage(channel) {
this.client.send
const content = {
body: "Processing... This might take a while",
msgtype: "m.text",
};
this.client.sendEvent(channel, "m.room.message", content, "", (err, res) => {
// console.log(res)
logger.log("error", err)
return res
});
}
static init() {
this.flags = [];
if (this.requiresText || this.textOptional) {
this.flags.push({
name: "text",
type: 3,
description: "The text to put on the image",
required: !this.textOptional
});
}
if (this.requiresImage) {
this.flags.push({
name: "image",
type: 11,
description: "An image/GIF attachment"
}, {
name: "link",
type: 3,
description: "An image/GIF URL"
});
}
this.flags.push({
name: "togif",
type: 5,
description: "Force GIF output"
});
return this;
}
static allowedFonts = ["futura", "impact", "helvetica", "arial", "roboto", "noto", "times", "comic sans ms"];
static requiresImage = true;
static requiresText = false;
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 { players, queues } from "../utils/soundplayer.js";
class MusicCommand extends Command {
constructor(client, options) {
super(client, options);
if (this.guild) {
this.connection = players.get(this.guild.id);
this.queue = queues.get(this.guild.id);
}
}
static slashAllowed = false;
static directAllowed = false;
}
export default MusicCommand;
import Command from "./command.js";
import { players, queues } from "../utils/soundplayer.js";
class MusicCommand extends Command {
constructor(client, options) {
super(client, options);
if (this.guild) {
this.connection = players.get(this.guild.id);
this.queue = queues.get(this.guild.id);
}
}
static slashAllowed = false;
static directAllowed = false;
}
export default MusicCommand;

View file

@ -1,14 +1,14 @@
import Command from "./command.js";
import { play } from "../utils/soundplayer.js";
// only exists to sort the various soundboard commands
class SoundboardCommand extends Command {
async run() {
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 directAllowed = false;
}
export default SoundboardCommand;
import Command from "./command.js";
import { play } from "../utils/soundplayer.js";
// only exists to sort the various soundboard commands
class SoundboardCommand extends Command {
async run() {
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 directAllowed = false;
}
export default SoundboardCommand;

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,34 +1,34 @@
import { request } from "undici";
import Command from "../../classes/command.js";
class WikihowCommand extends Command {
async run() {
await this.acknowledge();
const controller = new AbortController();
const timeout = setTimeout(() => {
controller.abort();
}, 15000);
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 });
clearTimeout(timeout);
const json = await req.body.json();
const id = Object.keys(json.query.pages)[0];
const data = json.query.pages[id];
if (data.imageinfo) {
return json.query.pages[id].imageinfo[0].url;
} else {
return await this.run();
}
} catch (e) {
if (e.name === "AbortError") {
this.success = false;
return "I couldn't get a WikiHow image in time. Maybe try again?";
}
}
}
static description = "Gets a random WikiHow image";
static aliases = ["wiki"];
}
export default WikihowCommand;
import { request } from "undici";
import Command from "../../classes/command.js";
class WikihowCommand extends Command {
async run() {
await this.acknowledge();
const controller = new AbortController();
const timeout = setTimeout(() => {
controller.abort();
}, 15000);
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 });
clearTimeout(timeout);
const json = await req.body.json();
const id = Object.keys(json.query.pages)[0];
const data = json.query.pages[id];
if (data.imageinfo) {
return json.query.pages[id].imageinfo[0].url;
} else {
return await this.run();
}
} catch (e) {
if (e.name === "AbortError") {
this.success = false;
return "I couldn't get a WikiHow image in time. Maybe try again?";
}
}
}
static description = "Gets a random WikiHow image";
static aliases = ["wiki"];
}
export default WikihowCommand;

View file

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

View file

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

View file

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

View file

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

View file

@ -1,53 +1,53 @@
import db from "../../utils/database.js";
import Command from "../../classes/command.js";
class ChannelCommand extends Command {
async run() {
this.success = false;
if (!this.guild) return "This command only works in servers!";
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.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!";
const guildDB = await db.getGuild(this.guild.id);
if (this.args[0].toLowerCase() === "disable") {
let channel;
if (this.args[1]?.match(/^<?[@#]?[&!]?\d+>?$/) && this.args[1] >= 21154535154122752n) {
const id = this.args[1].replaceAll("@", "").replaceAll("#", "").replaceAll("!", "").replaceAll("&", "").replaceAll("<", "").replaceAll(">", "");
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);
} else {
if (guildDB.disabled.includes(this.channel.id)) return "I'm already disabled in this channel!";
channel = this.channel;
}
await db.disableChannel(channel);
this.success = true;
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") {
let channel;
if (this.args[1]?.match(/^<?[@#]?[&!]?\d+>?$/) && this.args[1] >= 21154535154122752n) {
const id = this.args[1].replaceAll("@", "").replaceAll("#", "").replaceAll("!", "").replaceAll("&", "").replaceAll("<", "").replaceAll(">", "");
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);
} else {
if (!guildDB.disabled.includes(this.channel.id)) return "I'm not disabled in this channel!";
channel = this.channel;
}
await db.enableChannel(channel);
this.success = true;
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 arguments = ["[enable/disable]", "{id}"];
static slashAllowed = false;
static directAllowed = false;
static dbRequired = true;
}
export default ChannelCommand;
import db from "../../utils/database.js";
import Command from "../../classes/command.js";
class ChannelCommand extends Command {
async run() {
this.success = false;
if (!this.guild) return "This command only works in servers!";
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.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!";
const guildDB = await db.getGuild(this.guild.id);
if (this.args[0].toLowerCase() === "disable") {
let channel;
if (this.args[1]?.match(/^<?[@#]?[&!]?\d+>?$/) && this.args[1] >= 21154535154122752n) {
const id = this.args[1].replaceAll("@", "").replaceAll("#", "").replaceAll("!", "").replaceAll("&", "").replaceAll("<", "").replaceAll(">", "");
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);
} else {
if (guildDB.disabled.includes(this.channel.id)) return "I'm already disabled in this channel!";
channel = this.channel;
}
await db.disableChannel(channel);
this.success = true;
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") {
let channel;
if (this.args[1]?.match(/^<?[@#]?[&!]?\d+>?$/) && this.args[1] >= 21154535154122752n) {
const id = this.args[1].replaceAll("@", "").replaceAll("#", "").replaceAll("!", "").replaceAll("&", "").replaceAll("<", "").replaceAll(">", "");
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);
} else {
if (!guildDB.disabled.includes(this.channel.id)) return "I'm not disabled in this channel!";
channel = this.channel;
}
await db.enableChannel(channel);
this.success = true;
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 arguments = ["[enable/disable]", "{id}"];
static slashAllowed = false;
static directAllowed = false;
static dbRequired = true;
}
export default ChannelCommand;

View file

@ -1,44 +1,44 @@
import db from "../../utils/database.js";
import Command from "../../classes/command.js";
import * as collections from "../../utils/collections.js";
class CommandCommand extends Command {
async run() {
this.success = false;
if (!this.guild) return "This command only works in servers!";
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.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[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!";
const guildDB = await db.getGuild(this.guild.id);
const disabled = guildDB.disabled_commands ?? guildDB.disabledCommands;
const command = collections.aliases.get(this.args[1].toLowerCase()) ?? this.args[1].toLowerCase();
if (this.args[0].toLowerCase() === "disable") {
if (command === "command") return "You can't disable that command!";
if (disabled?.includes(command)) return "That command is already disabled!";
await db.disableCommand(this.guild.id, command);
this.success = true;
return `The command has been disabled. To re-enable it, just run \`${guildDB.prefix}command enable ${command}\`.`;
} else if (this.args[0].toLowerCase() === "enable") {
if (!disabled?.includes(command)) return "That command isn't disabled!";
await db.enableCommand(this.guild.id, command);
this.success = true;
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 aliases = ["cmd"];
static arguments = ["[enable/disable]", "[command]"];
static slashAllowed = false;
static directAllowed = false;
static dbRequired = true;
}
export default CommandCommand;
import db from "../../utils/database.js";
import Command from "../../classes/command.js";
import * as collections from "../../utils/collections.js";
class CommandCommand extends Command {
async run() {
this.success = false;
if (!this.guild) return "This command only works in servers!";
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.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[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!";
const guildDB = await db.getGuild(this.guild.id);
const disabled = guildDB.disabled_commands ?? guildDB.disabledCommands;
const command = collections.aliases.get(this.args[1].toLowerCase()) ?? this.args[1].toLowerCase();
if (this.args[0].toLowerCase() === "disable") {
if (command === "command") return "You can't disable that command!";
if (disabled?.includes(command)) return "That command is already disabled!";
await db.disableCommand(this.guild.id, command);
this.success = true;
return `The command has been disabled. To re-enable it, just run \`${guildDB.prefix}command enable ${command}\`.`;
} else if (this.args[0].toLowerCase() === "enable") {
if (!disabled?.includes(command)) return "That command isn't disabled!";
await db.enableCommand(this.guild.id, command);
this.success = true;
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 aliases = ["cmd"];
static arguments = ["[enable/disable]", "[command]"];
static slashAllowed = false;
static directAllowed = false;
static dbRequired = true;
}
export default CommandCommand;

View file

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

View file

@ -1,30 +1,30 @@
import { request } from "undici";
import Command from "../../classes/command.js";
class DonateCommand extends Command {
async run() {
await this.acknowledge();
let prefix = "";
const controller = new AbortController();
const timeout = setTimeout(() => {
controller.abort();
}, 5000);
try {
const patrons = await request("https://projectlounge.pw/patrons", { signal: controller.signal }).then(data => data.body.json());
clearTimeout(timeout);
prefix = "Thanks to the following patrons for their support:\n";
for (const patron of patrons) {
prefix += `**- ${patron}**\n`;
}
prefix += "\n";
} catch (e) {
// no-op
}
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 aliases = ["support", "patreon", "patrons"];
}
import { request } from "undici";
import Command from "../../classes/command.js";
class DonateCommand extends Command {
async run() {
await this.acknowledge();
let prefix = "";
const controller = new AbortController();
const timeout = setTimeout(() => {
controller.abort();
}, 5000);
try {
const patrons = await request("https://projectlounge.pw/patrons", { signal: controller.signal }).then(data => data.body.json());
clearTimeout(timeout);
prefix = "Thanks to the following patrons for their support:\n";
for (const patron of patrons) {
prefix += `**- ${patron}**\n`;
}
prefix += "\n";
} catch (e) {
// no-op
}
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 aliases = ["support", "patreon", "patrons"];
}
export default DonateCommand;

View file

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

View file

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

View file

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

View file

@ -1,120 +1,120 @@
import { Constants } from "oceanic.js";
import database from "../../utils/database.js";
import * as collections from "../../utils/collections.js";
import { random } from "../../utils/misc.js";
import paginator from "../../utils/pagination/pagination.js";
import * as help from "../../utils/help.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"];
class HelpCommand extends Command {
async run() {
let prefix;
if (this.guild && database) {
prefix = (await database.getGuild(this.guild.id)).prefix;
} else {
prefix = process.env.PREFIX;
}
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 info = collections.info.get(command);
const embed = {
embeds: [{
author: {
name: "esmBot Help",
iconURL: this.client.user.avatarURL()
},
title: `${this.guild ? prefix : ""}${command}`,
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,
color: 16711680,
fields: [{
name: "Aliases",
value: info.aliases.length !== 0 ? info.aliases.join(", ") : "None"
}, {
name: "Parameters",
value: command === "tags" ? "[name]" : (info.params ? (info.params.length !== 0 ? info.params.join(" ") : "None") : "None"),
inline: true
}]
}]
};
if (database) {
embed.embeds[0].fields.push({
name: "Times used",
value: (await database.getCounts())[command],
inline: true
});
}
if (info.flags.length !== 0) {
const flagInfo = [];
for (const flag of info.flags) {
if (flag.type === 1) continue;
flagInfo.push(`\`--${flag.name}${flag.type ? `=[${Constants.ApplicationCommandOptionTypes[flag.type]}]` : ""}\` - ${flag.description}`);
}
if (flagInfo.length !== 0) {
embed.embeds[0].fields.push({
"name": "Flags",
"value": flagInfo.join("\n")
});
}
}
return embed;
} else {
if (this.guild && !this.channel.permissionsOf(this.client.user.id.toString()).has("EMBED_LINKS")) {
this.success = false;
return "I don't have the `Embed Links` permission!";
}
const pages = [];
if (help.categories === help.categoryTemplate && !help.generated) help.generateList();
for (const category of Object.keys(help.categories)) {
const splitPages = help.categories[category].map((item, index) => {
return index % 15 === 0 ? help.categories[category].slice(index, index + 15) : null;
}).filter((item) => {
return item;
});
const categoryStringArray = category.split("-");
for (const index of categoryStringArray.keys()) {
categoryStringArray[index] = categoryStringArray[index].charAt(0).toUpperCase() + categoryStringArray[index].slice(1);
}
for (const page of splitPages) {
pages.push({
title: categoryStringArray.join(" "),
page: page
});
}
}
const embeds = [];
for (const [i, value] of pages.entries()) {
embeds.push({
embeds: [{
author: {
name: "esmBot Help",
iconURL: this.client.user.avatarURL()
},
title: value.title,
description: value.page.join("\n"),
color: 16711680,
footer: {
text: `Page ${i + 1} of ${pages.length}`
},
fields: [{
name: "Prefix",
value: prefix
}, {
name: "Tip",
value: random(tips)
}]
}]
});
}
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 aliases = ["commands"];
static arguments = ["{command}"];
static slashAllowed = false;
}
export default HelpCommand;
import { Constants } from "oceanic.js";
import database from "../../utils/database.js";
import * as collections from "../../utils/collections.js";
import { random } from "../../utils/misc.js";
import paginator from "../../utils/pagination/pagination.js";
import * as help from "../../utils/help.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"];
class HelpCommand extends Command {
async run() {
let prefix;
if (this.guild && database) {
prefix = (await database.getGuild(this.guild.id)).prefix;
} else {
prefix = process.env.PREFIX;
}
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 info = collections.info.get(command);
const embed = {
embeds: [{
author: {
name: "esmBot Help",
iconURL: this.client.user.avatarURL()
},
title: `${this.guild ? prefix : ""}${command}`,
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,
color: 16711680,
fields: [{
name: "Aliases",
value: info.aliases.length !== 0 ? info.aliases.join(", ") : "None"
}, {
name: "Parameters",
value: command === "tags" ? "[name]" : (info.params ? (info.params.length !== 0 ? info.params.join(" ") : "None") : "None"),
inline: true
}]
}]
};
if (database) {
embed.embeds[0].fields.push({
name: "Times used",
value: (await database.getCounts())[command],
inline: true
});
}
if (info.flags.length !== 0) {
const flagInfo = [];
for (const flag of info.flags) {
if (flag.type === 1) continue;
flagInfo.push(`\`--${flag.name}${flag.type ? `=[${Constants.ApplicationCommandOptionTypes[flag.type]}]` : ""}\` - ${flag.description}`);
}
if (flagInfo.length !== 0) {
embed.embeds[0].fields.push({
"name": "Flags",
"value": flagInfo.join("\n")
});
}
}
return embed;
} else {
if (this.guild && !this.channel.permissionsOf(this.client.user.id.toString()).has("EMBED_LINKS")) {
this.success = false;
return "I don't have the `Embed Links` permission!";
}
const pages = [];
if (help.categories === help.categoryTemplate && !help.generated) help.generateList();
for (const category of Object.keys(help.categories)) {
const splitPages = help.categories[category].map((item, index) => {
return index % 15 === 0 ? help.categories[category].slice(index, index + 15) : null;
}).filter((item) => {
return item;
});
const categoryStringArray = category.split("-");
for (const index of categoryStringArray.keys()) {
categoryStringArray[index] = categoryStringArray[index].charAt(0).toUpperCase() + categoryStringArray[index].slice(1);
}
for (const page of splitPages) {
pages.push({
title: categoryStringArray.join(" "),
page: page
});
}
}
const embeds = [];
for (const [i, value] of pages.entries()) {
embeds.push({
embeds: [{
author: {
name: "esmBot Help",
iconURL: this.client.user.avatarURL()
},
title: value.title,
description: value.page.join("\n"),
color: 16711680,
footer: {
text: `Page ${i + 1} of ${pages.length}`
},
fields: [{
name: "Prefix",
value: prefix
}, {
name: "Tip",
value: random(tips)
}]
}]
});
}
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 aliases = ["commands"];
static arguments = ["{command}"];
static slashAllowed = false;
}
export default HelpCommand;

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,27 +1,27 @@
import Command from "../../classes/command.js";
class PingCommand extends Command {
async run() {
if (this.type === "classic") {
const pingMessage = await this.client.rest.channels.createMessage(this.message.channelID, Object.assign({
content: "🏓 Ping?"
}, this.reference));
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\`\`\``
});
} else {
await this.interaction.createMessage({
content: "🏓 Ping?"
});
const pingMessage = await this.interaction.getOriginal();
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\`\`\``
});
}
}
static description = "Pings Discord's servers";
static aliases = ["pong"];
}
import Command from "../../classes/command.js";
class PingCommand extends Command {
async run() {
if (this.type === "classic") {
const pingMessage = await this.client.rest.channels.createMessage(this.message.channelID, Object.assign({
content: "🏓 Ping?"
}, this.reference));
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\`\`\``
});
} else {
await this.interaction.createMessage({
content: "🏓 Ping?"
});
const pingMessage = await this.interaction.getOriginal();
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\`\`\``
});
}
}
static description = "Pings Discord's servers";
static aliases = ["pong"];
}
export default PingCommand;

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,20 +1,20 @@
import Command from "../../classes/command.js";
class SnowflakeCommand extends Command {
async run() {
this.success = false;
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!";
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!";
this.success = true;
return `<t:${id}:F>`;
}
static description = "Converts a Discord snowflake id into a timestamp";
static aliases = ["timestamp", "snowstamp", "snow"];
static arguments = ["[id]"];
static slashAllowed = false;
}
export default SnowflakeCommand;
import Command from "../../classes/command.js";
class SnowflakeCommand extends Command {
async run() {
this.success = false;
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!";
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!";
this.success = true;
return `<t:${id}:F>`;
}
static description = "Converts a Discord snowflake id into a timestamp";
static aliases = ["timestamp", "snowstamp", "snow"];
static arguments = ["[id]"];
static slashAllowed = false;
}
export default SnowflakeCommand;

View file

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

View file

@ -1,90 +1,90 @@
import { readFileSync } from "fs";
const { version } = JSON.parse(readFileSync(new URL("../../package.json", import.meta.url)));
import os from "os";
import Command from "../../classes/command.js";
import { VERSION } from "oceanic.js";
import pm2 from "pm2";
import { getServers } from "../../utils/misc.js";
class StatsCommand extends Command {
async run() {
const uptime = process.uptime() * 1000;
const connUptime = this.client.uptime;
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]);
const servers = await getServers(this.client);
const processMem = `${(process.memoryUsage().heapUsed / 1024 / 1024).toFixed(2)} MB`;
return {
embeds: [{
"author": {
"name": "esmBot Statistics",
"iconURL": this.client.user.avatarURL()
},
"description": `This instance is managed by **${owner.username}#${owner.discriminator}**.`,
"color": 16711680,
"fields": [{
"name": "Version",
"value": `v${version}${process.env.NODE_ENV === "development" ? `-dev (${process.env.GIT_REV})` : ""}`
},
{
"name": "Process Memory Usage",
"value": processMem,
"inline": true
},
{
"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,
"inline": true
},
{
"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`
},
{
"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`
},
{
"name": "Host",
"value": `${os.type()} ${os.release()} (${os.arch()})`,
"inline": true
},
{
"name": "Library",
"value": `Oceanic ${VERSION}`,
"inline": true
},
{
"name": "Node.js Version",
"value": process.version,
"inline": true
},
{
"name": "Shard",
"value": this.guild ? this.client.guildShardMap[this.guild.id] : "N/A",
"inline": true
},
{
"name": "Servers",
"value": servers ? servers : `${this.client.guilds.size} (for this process only)`,
"inline": true
}
]
}]
};
}
list() {
return new Promise((resolve, reject) => {
pm2.list((err, list) => {
if (err) return reject(err);
resolve(list.filter((v) => v.name.includes("esmBot-proc")));
});
});
}
static description = "Gets some statistics about me";
static aliases = ["status", "stat"];
}
import { readFileSync } from "fs";
const { version } = JSON.parse(readFileSync(new URL("../../package.json", import.meta.url)));
import os from "os";
import Command from "../../classes/command.js";
import { VERSION } from "oceanic.js";
import pm2 from "pm2";
import { getServers } from "../../utils/misc.js";
class StatsCommand extends Command {
async run() {
const uptime = process.uptime() * 1000;
const connUptime = this.client.uptime;
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]);
const servers = await getServers(this.client);
const processMem = `${(process.memoryUsage().heapUsed / 1024 / 1024).toFixed(2)} MB`;
return {
embeds: [{
"author": {
"name": "esmBot Statistics",
"iconURL": this.client.user.avatarURL()
},
"description": `This instance is managed by **${owner.username}#${owner.discriminator}**.`,
"color": 16711680,
"fields": [{
"name": "Version",
"value": `v${version}${process.env.NODE_ENV === "development" ? `-dev (${process.env.GIT_REV})` : ""}`
},
{
"name": "Process Memory Usage",
"value": processMem,
"inline": true
},
{
"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,
"inline": true
},
{
"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`
},
{
"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`
},
{
"name": "Host",
"value": `${os.type()} ${os.release()} (${os.arch()})`,
"inline": true
},
{
"name": "Library",
"value": `Oceanic ${VERSION}`,
"inline": true
},
{
"name": "Node.js Version",
"value": process.version,
"inline": true
},
{
"name": "Shard",
"value": this.guild ? this.client.guildShardMap[this.guild.id] : "N/A",
"inline": true
},
{
"name": "Servers",
"value": servers ? servers : `${this.client.guilds.size} (for this process only)`,
"inline": true
}
]
}]
};
}
list() {
return new Promise((resolve, reject) => {
pm2.list((err, list) => {
if (err) return reject(err);
resolve(list.filter((v) => v.name.includes("esmBot-proc")));
});
});
}
static description = "Gets some statistics about me";
static aliases = ["status", "stat"];
}
export default StatsCommand;

View file

@ -1,36 +1,36 @@
import Command from "../../classes/command.js";
import imagedetect from "../../utils/imagedetect.js";
class StickerCommand extends Command {
async run() {
const result = await imagedetect(this.client, this.message, this.interaction, this.options, false, false, true);
this.success = false;
if (!result) return "You need to provide a sticker!";
if (result.format_type === 1) { // PNG
this.success = true;
return `https://cdn.discordapp.com/stickers/${result.id}.png`;
} else if (result.format_type === 2) { // APNG
this.success = true;
return {
embeds: [{
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)`,
image: {
url: `https://cdn.discordapp.com/stickers/${result.id}.png`
}
}]
};
} else if (result.format_type === 3) { // Lottie
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`;
} else {
return "I don't recognize that sticker format!";
}
}
static description = "Gets a raw sticker image";
static aliases = ["stick"];
static arguments = ["[sticker]"];
}
import Command from "../../classes/command.js";
import imagedetect from "../../utils/imagedetect.js";
class StickerCommand extends Command {
async run() {
const result = await imagedetect(this.client, this.message, this.interaction, this.options, false, false, true);
this.success = false;
if (!result) return "You need to provide a sticker!";
if (result.format_type === 1) { // PNG
this.success = true;
return `https://cdn.discordapp.com/stickers/${result.id}.png`;
} else if (result.format_type === 2) { // APNG
this.success = true;
return {
embeds: [{
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)`,
image: {
url: `https://cdn.discordapp.com/stickers/${result.id}.png`
}
}]
};
} else if (result.format_type === 3) { // Lottie
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`;
} else {
return "I don't recognize that sticker format!";
}
}
static description = "Gets a raw sticker image";
static aliases = ["stick"];
static arguments = ["[sticker]"];
}
export default StickerCommand;

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,46 +1,46 @@
import ImageCommand from "../../classes/imageCommand.js";
import { cleanMessage } from "../../utils/misc.js";
class CaptionCommand extends ImageCommand {
params(url) {
const newArgs = this.options.text ?? this.args.filter(item => !item.includes(url)).join(" ");
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}`;
return {
caption: newCaption,
font: typeof this.options.font === "string" && this.constructor.allowedFonts.includes(this.options.font.toLowerCase()) ? this.options.font.toLowerCase() : "futura"
};
}
static init() {
super.init();
this.flags.push({
name: "noegg",
description: "Disable... something. Not saying what it is though.",
type: 5
}, {
name: "font",
type: 3,
choices: (() => {
const array = [];
for (const font of this.allowedFonts) {
array.push({ name: font, value: font });
}
return array;
})(),
description: "Specify the font you want to use (default: futura)"
});
return this;
}
static description = "Adds a caption to an image";
static aliases = ["gifc", "gcaption", "ifcaption", "ifunnycaption"];
static arguments = ["[text]"];
static requiresText = true;
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 command = "caption";
}
export default CaptionCommand;
import ImageCommand from "../../classes/imageCommand.js";
import { cleanMessage } from "../../utils/misc.js";
class CaptionCommand extends ImageCommand {
params(url) {
const newArgs = this.options.text ?? this.args.filter(item => !item.includes(url)).join(" ");
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}`;
return {
caption: newCaption,
font: typeof this.options.font === "string" && this.constructor.allowedFonts.includes(this.options.font.toLowerCase()) ? this.options.font.toLowerCase() : "futura"
};
}
static init() {
super.init();
this.flags.push({
name: "noegg",
description: "Disable... something. Not saying what it is though.",
type: 5
}, {
name: "font",
type: 3,
choices: (() => {
const array = [];
for (const font of this.allowedFonts) {
array.push({ name: font, value: font });
}
return array;
})(),
description: "Specify the font you want to use (default: futura)"
});
return this;
}
static description = "Adds a caption to an image";
static aliases = ["gifc", "gcaption", "ifcaption", "ifunnycaption"];
static arguments = ["[text]"];
static requiresText = true;
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 command = "caption";
}
export default CaptionCommand;

View file

@ -1,46 +1,46 @@
import ImageCommand from "../../classes/imageCommand.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"];
class CaptionTwoCommand extends ImageCommand {
params(url) {
const newArgs = this.options.text ?? this.args.filter(item => !item.includes(url)).join(" ");
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(" "),
top: !!this.options.top,
font: typeof this.options.font === "string" && this.constructor.allowedFonts.includes(this.options.font.toLowerCase()) ? this.options.font.toLowerCase() : "helvetica"
};
}
static init() {
super.init();
this.flags.push({
name: "top",
description: "Put the caption on the top of an image instead of the bottom",
type: 5
}, {
name: "font",
type: 3,
choices: (() => {
const array = [];
for (const font of this.allowedFonts) {
array.push({ name: font, value: font });
}
return array;
})(),
description: "Specify the font you want to use (default: helvetica)"
});
return this;
}
static description = "Adds a me.me caption/tag list to an image";
static aliases = ["tags2", "meirl", "memecaption", "medotmecaption"];
static arguments = ["{text}"];
static textOptional = true;
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 command = "captionTwo";
}
export default CaptionTwoCommand;
import ImageCommand from "../../classes/imageCommand.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"];
class CaptionTwoCommand extends ImageCommand {
params(url) {
const newArgs = this.options.text ?? this.args.filter(item => !item.includes(url)).join(" ");
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(" "),
top: !!this.options.top,
font: typeof this.options.font === "string" && this.constructor.allowedFonts.includes(this.options.font.toLowerCase()) ? this.options.font.toLowerCase() : "helvetica"
};
}
static init() {
super.init();
this.flags.push({
name: "top",
description: "Put the caption on the top of an image instead of the bottom",
type: 5
}, {
name: "font",
type: 3,
choices: (() => {
const array = [];
for (const font of this.allowedFonts) {
array.push({ name: font, value: font });
}
return array;
})(),
description: "Specify the font you want to use (default: helvetica)"
});
return this;
}
static description = "Adds a me.me caption/tag list to an image";
static aliases = ["tags2", "meirl", "memecaption", "medotmecaption"];
static arguments = ["{text}"];
static textOptional = true;
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 command = "captionTwo";
}
export default CaptionTwoCommand;

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

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