diff --git a/.dockerignore b/.dockerignore index 1fbb8a3..94e404f 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,4 +1,4 @@ -**/node_modules -**/build -**/logs +**/node_modules +**/build +**/logs **/data \ No newline at end of file diff --git a/.editorconfig b/.editorconfig index 0f3bb61..13505ca 100644 --- a/.editorconfig +++ b/.editorconfig @@ -1,3 +1,3 @@ -[*.js] -indent_style = space -indent_size = 2 +[*.js] +indent_style = space +indent_size = 2 diff --git a/.env.example b/.env.example index 8ece7e8..bc38ade 100644 --- a/.env.example +++ b/.env.example @@ -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= \ No newline at end of file diff --git a/.eslintrc.json b/.eslintrc.json index 5ba7ee8..27588c9 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -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" + } +} diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml index 08c46cb..3df709f 100644 --- a/.github/FUNDING.yml +++ b/.github/FUNDING.yml @@ -1,2 +1,2 @@ -patreon: TheEssem +patreon: TheEssem ko_fi: TheEssem \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index c30b02a..a27c1d8 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -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 diff --git a/.github/ISSUE_TEMPLATE/error-report.yml b/.github/ISSUE_TEMPLATE/error-report.yml index 20c1a7d..58024f9 100644 --- a/.github/ISSUE_TEMPLATE/error-report.yml +++ b/.github/ISSUE_TEMPLATE/error-report.yml @@ -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 \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml index 063c59a..0e7bb64 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.yml +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -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 \ No newline at end of file diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 196314b..be5f356 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -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 + diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index f85d4b5..d74e361 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -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 diff --git a/.github/workflows/mkdocs.yml b/.github/workflows/mkdocs.yml index 10f7bc5..e3bcc49 100644 --- a/.github/workflows/mkdocs.yml +++ b/.github/workflows/mkdocs.yml @@ -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 diff --git a/.gitignore b/.gitignore index 0b0ad31..d79dcf6 100644 --- a/.gitignore +++ b/.gitignore @@ -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 \ No newline at end of file diff --git a/.gitmodules b/.gitmodules index d73eae3..b01e750 100644 --- a/.gitmodules +++ b/.gitmodules @@ -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 diff --git a/CMakeLists.txt b/CMakeLists.txt index eb8fc66..77ed27b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -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() diff --git a/Dockerfile b/Dockerfile index 2e53068..31c1aa9 100644 --- a/Dockerfile +++ b/Dockerfile @@ -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"] diff --git a/LICENSE b/LICENSE index 2b01000..c08f23c 100644 --- a/LICENSE +++ b/LICENSE @@ -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. diff --git a/PRIVACY.md b/PRIVACY.md index f9fa103..128f492 100644 --- a/PRIVACY.md +++ b/PRIVACY.md @@ -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. diff --git a/README.md b/README.md index 430d683..93c0f6d 100644 --- a/README.md +++ b/README.md @@ -1,29 +1,29 @@ -# 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. +# 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. diff --git a/api/IMPLEMENTATION.md b/api/IMPLEMENTATION.md index 82344f4..b72b9d4 100644 --- a/api/IMPLEMENTATION.md +++ b/api/IMPLEMENTATION.md @@ -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=` -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=` +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 +} +``` diff --git a/api/index.js b/api/index.js index 7103031..b24e2e6 100644 --- a/api/index.js +++ b/api/index.js @@ -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: } -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: } +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); + }); + }); +}; diff --git a/app.js b/app.js index 9cce4ac..d712148 100644 --- a/app.js +++ b/app.js @@ -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 { - 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 logger.log("info", "Client started!")); +} + init(); \ No newline at end of file diff --git a/application.yml b/application.yml index 96c4464..23173ce 100644 --- a/application.yml +++ b/application.yml @@ -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 + diff --git a/classes/command.js b/classes/command.js index 54b1f4e..3471e4c 100644 --- a/classes/command.js +++ b/classes/command.js @@ -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; \ No newline at end of file diff --git a/classes/imageCommand.js b/classes/imageCommand.js index 554a17b..163af8e 100644 --- a/classes/imageCommand.js +++ b/classes/imageCommand.js @@ -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 || ""} 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; diff --git a/classes/musicCommand.js b/classes/musicCommand.js index 86e581e..c3dc41e 100644 --- a/classes/musicCommand.js +++ b/classes/musicCommand.js @@ -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; diff --git a/classes/soundboardCommand.js b/classes/soundboardCommand.js index 259e885..897da2c 100644 --- a/classes/soundboardCommand.js +++ b/classes/soundboardCommand.js @@ -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; diff --git a/commands/fun/8ball.js b/commands/fun/8ball.js index 65e53e1..80f1bb1 100644 --- a/commands/fun/8ball.js +++ b/commands/fun/8ball.js @@ -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; \ No newline at end of file diff --git a/commands/fun/ancient.js b/commands/fun/ancient.js index c94d565..c32a814 100644 --- a/commands/fun/ancient.js +++ b/commands/fun/ancient.js @@ -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; \ No newline at end of file diff --git a/commands/fun/bird.js b/commands/fun/bird.js index 5df6996..491aa18 100644 --- a/commands/fun/bird.js +++ b/commands/fun/bird.js @@ -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; \ No newline at end of file diff --git a/commands/fun/cat.js b/commands/fun/cat.js index f4e488e..8977341 100644 --- a/commands/fun/cat.js +++ b/commands/fun/cat.js @@ -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; \ No newline at end of file diff --git a/commands/fun/dice.js b/commands/fun/dice.js index c085827..18bd87b 100644 --- a/commands/fun/dice.js +++ b/commands/fun/dice.js @@ -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; \ No newline at end of file diff --git a/commands/fun/dog.js b/commands/fun/dog.js index 49fd58a..83ee96c 100644 --- a/commands/fun/dog.js +++ b/commands/fun/dog.js @@ -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; \ No newline at end of file diff --git a/commands/fun/homebrew.js b/commands/fun/homebrew.js index dc60f71..b6a425e 100644 --- a/commands/fun/homebrew.js +++ b/commands/fun/homebrew.js @@ -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; \ No newline at end of file diff --git a/commands/fun/sonic.js b/commands/fun/sonic.js index 7d67db5..499d1d8 100644 --- a/commands/fun/sonic.js +++ b/commands/fun/sonic.js @@ -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; \ No newline at end of file diff --git a/commands/fun/wikihow.js b/commands/fun/wikihow.js index 47b9e7f..6581a2f 100644 --- a/commands/fun/wikihow.js +++ b/commands/fun/wikihow.js @@ -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; diff --git a/commands/general/avatar.js b/commands/general/avatar.js index ee30e9c..0985460 100644 --- a/commands/general/avatar.js +++ b/commands/general/avatar.js @@ -1,53 +1,53 @@ -import Command from "../../classes/command.js"; -const mentionRegex = /^?$/; - -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 = /^?$/; + +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; diff --git a/commands/general/banner.js b/commands/general/banner.js index f9717a2..83d71f1 100644 --- a/commands/general/banner.js +++ b/commands/general/banner.js @@ -1,55 +1,55 @@ -import Command from "../../classes/command.js"; -import { Routes } from "oceanic.js"; -const mentionRegex = /^?$/; - -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 = /^?$/; + +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; diff --git a/commands/general/base64.js b/commands/general/base64.js index 450e775..f4c9ac6 100644 --- a/commands/general/base64.js +++ b/commands/general/base64.js @@ -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; \ No newline at end of file diff --git a/commands/general/broadcast.js b/commands/general/broadcast.js index 403ebff..fdf2d2b 100644 --- a/commands/general/broadcast.js +++ b/commands/general/broadcast.js @@ -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; \ No newline at end of file diff --git a/commands/general/channel.js b/commands/general/channel.js index 761f323..b1750c3 100644 --- a/commands/general/channel.js +++ b/commands/general/channel.js @@ -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(/^?$/) && 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(/^?$/) && 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(/^?$/) && 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(/^?$/) && 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; diff --git a/commands/general/command.js b/commands/general/command.js index b60fff1..06caaca 100644 --- a/commands/general/command.js +++ b/commands/general/command.js @@ -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; diff --git a/commands/general/count.js b/commands/general/count.js index 0c7f9e6..712ad92 100644 --- a/commands/general/count.js +++ b/commands/general/count.js @@ -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; \ No newline at end of file diff --git a/commands/general/donate.js b/commands/general/donate.js index 88a3a6f..d6b2964 100644 --- a/commands/general/donate.js +++ b/commands/general/donate.js @@ -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; \ No newline at end of file diff --git a/commands/general/emote.js b/commands/general/emote.js index 1901e73..34afce8 100644 --- a/commands/general/emote.js +++ b/commands/general/emote.js @@ -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(/^$/)) { - 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(/^$/)) { + 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; diff --git a/commands/general/eval.js b/commands/general/eval.js index 62b0e1f..4ca045d 100644 --- a/commands/general/eval.js +++ b/commands/general/eval.js @@ -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; \ No newline at end of file diff --git a/commands/general/exec.js b/commands/general/exec.js index 46df533..3d4a036 100644 --- a/commands/general/exec.js +++ b/commands/general/exec.js @@ -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; \ No newline at end of file diff --git a/commands/general/help.js b/commands/general/help.js index 3030eb0..36325a9 100644 --- a/commands/general/help.js +++ b/commands/general/help.js @@ -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; diff --git a/commands/general/image.js b/commands/general/image.js index 9c994ab..1a8e07f 100644 --- a/commands/general/image.js +++ b/commands/general/image.js @@ -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; \ No newline at end of file diff --git a/commands/general/imagereload.js b/commands/general/imagereload.js index 1d625f9..6e612a3 100644 --- a/commands/general/imagereload.js +++ b/commands/general/imagereload.js @@ -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; diff --git a/commands/general/imagestats.js b/commands/general/imagestats.js index e8952f7..247c08c 100644 --- a/commands/general/imagestats.js +++ b/commands/general/imagestats.js @@ -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; diff --git a/commands/general/info.js b/commands/general/info.js index 71df20f..12ee147 100644 --- a/commands/general/info.js +++ b/commands/general/info.js @@ -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; \ No newline at end of file diff --git a/commands/general/lengthen.js b/commands/general/lengthen.js index 66e6a6c..b25f154 100644 --- a/commands/general/lengthen.js +++ b/commands/general/lengthen.js @@ -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; \ No newline at end of file diff --git a/commands/general/ping.js b/commands/general/ping.js index 325c182..0e20a9a 100644 --- a/commands/general/ping.js +++ b/commands/general/ping.js @@ -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; \ No newline at end of file diff --git a/commands/general/prefix.js b/commands/general/prefix.js index 981ef5d..60727f9 100644 --- a/commands/general/prefix.js +++ b/commands/general/prefix.js @@ -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; \ No newline at end of file diff --git a/commands/general/qrcreate.js b/commands/general/qrcreate.js index 79123d8..49f6829 100644 --- a/commands/general/qrcreate.js +++ b/commands/general/qrcreate.js @@ -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; \ No newline at end of file diff --git a/commands/general/qrread.js b/commands/general/qrread.js index cc44cbe..84bcc1d 100644 --- a/commands/general/qrread.js +++ b/commands/general/qrread.js @@ -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; diff --git a/commands/general/raw.js b/commands/general/raw.js index 9c5c4be..28bdb6e 100644 --- a/commands/general/raw.js +++ b/commands/general/raw.js @@ -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; diff --git a/commands/general/reload.js b/commands/general/reload.js index 7c74a6b..9d75543 100644 --- a/commands/general/reload.js +++ b/commands/general/reload.js @@ -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; diff --git a/commands/general/restart.js b/commands/general/restart.js index a39cf2a..e4bbfc3 100644 --- a/commands/general/restart.js +++ b/commands/general/restart.js @@ -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; \ No newline at end of file diff --git a/commands/general/snowflake.js b/commands/general/snowflake.js index 3801f87..0bd7c74 100644 --- a/commands/general/snowflake.js +++ b/commands/general/snowflake.js @@ -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(/^?$/) && 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 ``; - } - - 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(/^?$/) && 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 ``; + } + + static description = "Converts a Discord snowflake id into a timestamp"; + static aliases = ["timestamp", "snowstamp", "snow"]; + static arguments = ["[id]"]; + static slashAllowed = false; +} + +export default SnowflakeCommand; diff --git a/commands/general/soundreload.js b/commands/general/soundreload.js index 3a8cde5..d030c11 100644 --- a/commands/general/soundreload.js +++ b/commands/general/soundreload.js @@ -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; \ No newline at end of file diff --git a/commands/general/stats.js b/commands/general/stats.js index 6e06e79..331d781 100644 --- a/commands/general/stats.js +++ b/commands/general/stats.js @@ -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; \ No newline at end of file diff --git a/commands/general/sticker.js b/commands/general/sticker.js index a54cd45..4aece70 100644 --- a/commands/general/sticker.js +++ b/commands/general/sticker.js @@ -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; \ No newline at end of file diff --git a/commands/general/youtube.js b/commands/general/youtube.js index a135590..17b43b8 100644 --- a/commands/general/youtube.js +++ b/commands/general/youtube.js @@ -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; \ No newline at end of file diff --git a/commands/image-editing/9gag.js b/commands/image-editing/9gag.js index 356818e..e804c52 100644 --- a/commands/image-editing/9gag.js +++ b/commands/image-editing/9gag.js @@ -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; diff --git a/commands/image-editing/avs4you.js b/commands/image-editing/avs4you.js index bd8174f..771930b 100644 --- a/commands/image-editing/avs4you.js +++ b/commands/image-editing/avs4you.js @@ -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; diff --git a/commands/image-editing/bandicam.js b/commands/image-editing/bandicam.js index 19bb649..2208adc 100644 --- a/commands/image-editing/bandicam.js +++ b/commands/image-editing/bandicam.js @@ -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; diff --git a/commands/image-editing/blur.js b/commands/image-editing/blur.js index dd083bf..c1feafe 100644 --- a/commands/image-editing/blur.js +++ b/commands/image-editing/blur.js @@ -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; diff --git a/commands/image-editing/bounce.js b/commands/image-editing/bounce.js index 5075f9d..ea0ce5d 100644 --- a/commands/image-editing/bounce.js +++ b/commands/image-editing/bounce.js @@ -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; diff --git a/commands/image-editing/caption.js b/commands/image-editing/caption.js index f862d29..29e1136 100644 --- a/commands/image-editing/caption.js +++ b/commands/image-editing/caption.js @@ -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; diff --git a/commands/image-editing/caption2.js b/commands/image-editing/caption2.js index dce5ee5..f2d2b14 100644 --- a/commands/image-editing/caption2.js +++ b/commands/image-editing/caption2.js @@ -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; diff --git a/commands/image-editing/circle.js b/commands/image-editing/circle.js index 607345e..14badfb 100644 --- a/commands/image-editing/circle.js +++ b/commands/image-editing/circle.js @@ -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; diff --git a/commands/image-editing/crop.js b/commands/image-editing/crop.js index 23564c6..67368ea 100644 --- a/commands/image-editing/crop.js +++ b/commands/image-editing/crop.js @@ -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; diff --git a/commands/image-editing/deepfry.js b/commands/image-editing/deepfry.js index 40be834..41c572c 100644 --- a/commands/image-editing/deepfry.js +++ b/commands/image-editing/deepfry.js @@ -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; diff --git a/commands/image-editing/deviantart.js b/commands/image-editing/deviantart.js index 44a3860..85544ed 100644 --- a/commands/image-editing/deviantart.js +++ b/commands/image-editing/deviantart.js @@ -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; diff --git a/commands/image-editing/explode.js b/commands/image-editing/explode.js index 63168f4..2f1706e 100644 --- a/commands/image-editing/explode.js +++ b/commands/image-editing/explode.js @@ -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; diff --git a/commands/image-editing/flag.js b/commands/image-editing/flag.js index a5a7500..0410db5 100644 --- a/commands/image-editing/flag.js +++ b/commands/image-editing/flag.js @@ -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; diff --git a/commands/image-editing/flip.js b/commands/image-editing/flip.js index 57feabc..297cdbb 100644 --- a/commands/image-editing/flip.js +++ b/commands/image-editing/flip.js @@ -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; diff --git a/commands/image-editing/flop.js b/commands/image-editing/flop.js index 987c35c..d8b91ca 100644 --- a/commands/image-editing/flop.js +++ b/commands/image-editing/flop.js @@ -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; diff --git a/commands/image-editing/freeze.js b/commands/image-editing/freeze.js index 124a195..dff2478 100644 --- a/commands/image-editing/freeze.js +++ b/commands/image-editing/freeze.js @@ -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; diff --git a/commands/image-editing/funky.js b/commands/image-editing/funky.js index 2da7544..abd9b7b 100644 --- a/commands/image-editing/funky.js +++ b/commands/image-editing/funky.js @@ -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; diff --git a/commands/image-editing/gamexplain.js b/commands/image-editing/gamexplain.js index 1b611ce..fd9e938 100644 --- a/commands/image-editing/gamexplain.js +++ b/commands/image-editing/gamexplain.js @@ -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; diff --git a/commands/image-editing/gif.js b/commands/image-editing/gif.js index a912701..1ae4121 100644 --- a/commands/image-editing/gif.js +++ b/commands/image-editing/gif.js @@ -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; diff --git a/commands/image-editing/globe.js b/commands/image-editing/globe.js index 15fac84..506685b 100644 --- a/commands/image-editing/globe.js +++ b/commands/image-editing/globe.js @@ -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; diff --git a/commands/image-editing/grayscale.js b/commands/image-editing/grayscale.js index c6c2aeb..6c89163 100644 --- a/commands/image-editing/grayscale.js +++ b/commands/image-editing/grayscale.js @@ -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; diff --git a/commands/image-editing/haah.js b/commands/image-editing/haah.js index 282f758..a9881c2 100644 --- a/commands/image-editing/haah.js +++ b/commands/image-editing/haah.js @@ -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; diff --git a/commands/image-editing/hooh.js b/commands/image-editing/hooh.js index 50aafe4..2db4e9c 100644 --- a/commands/image-editing/hooh.js +++ b/commands/image-editing/hooh.js @@ -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; diff --git a/commands/image-editing/hypercam.js b/commands/image-editing/hypercam.js index f053dde..97ab434 100644 --- a/commands/image-editing/hypercam.js +++ b/commands/image-editing/hypercam.js @@ -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; diff --git a/commands/image-editing/ifunny.js b/commands/image-editing/ifunny.js index dee5a15..c7a8934 100644 --- a/commands/image-editing/ifunny.js +++ b/commands/image-editing/ifunny.js @@ -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; diff --git a/commands/image-editing/implode.js b/commands/image-editing/implode.js index 1f05b2e..35680c6 100644 --- a/commands/image-editing/implode.js +++ b/commands/image-editing/implode.js @@ -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; diff --git a/commands/image-editing/invert.js b/commands/image-editing/invert.js index 48dd705..51e7859 100644 --- a/commands/image-editing/invert.js +++ b/commands/image-editing/invert.js @@ -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; diff --git a/commands/image-editing/jpeg.js b/commands/image-editing/jpeg.js index 8c814b7..35c87b4 100644 --- a/commands/image-editing/jpeg.js +++ b/commands/image-editing/jpeg.js @@ -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; diff --git a/commands/image-editing/kinemaster.js b/commands/image-editing/kinemaster.js index 3543441..fa91f47 100644 --- a/commands/image-editing/kinemaster.js +++ b/commands/image-editing/kinemaster.js @@ -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; diff --git a/commands/image-editing/magik.js b/commands/image-editing/magik.js index 80011f4..122be0b 100644 --- a/commands/image-editing/magik.js +++ b/commands/image-editing/magik.js @@ -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; diff --git a/commands/image-editing/meme.js b/commands/image-editing/meme.js index 641d47b..a6af0fb 100644 --- a/commands/image-editing/meme.js +++ b/commands/image-editing/meme.js @@ -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(/(? 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(/(? 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(/(? 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(/(? 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; diff --git a/commands/image-editing/memecenter.js b/commands/image-editing/memecenter.js index bbaf59c..c461242 100644 --- a/commands/image-editing/memecenter.js +++ b/commands/image-editing/memecenter.js @@ -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; diff --git a/commands/image-editing/motivate.js b/commands/image-editing/motivate.js index 007e6aa..37b5a16 100644 --- a/commands/image-editing/motivate.js +++ b/commands/image-editing/motivate.js @@ -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(/(? 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(/(? 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(/(? 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(/(? 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; diff --git a/commands/image-editing/pixelate.js b/commands/image-editing/pixelate.js index 813b4cb..8074d5e 100644 --- a/commands/image-editing/pixelate.js +++ b/commands/image-editing/pixelate.js @@ -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; diff --git a/commands/image-editing/reddit.js b/commands/image-editing/reddit.js index 3409a18..5924248 100644 --- a/commands/image-editing/reddit.js +++ b/commands/image-editing/reddit.js @@ -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; diff --git a/commands/image-editing/reverse.js b/commands/image-editing/reverse.js index 83632b5..3f34ade 100644 --- a/commands/image-editing/reverse.js +++ b/commands/image-editing/reverse.js @@ -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; diff --git a/commands/image-editing/scott.js b/commands/image-editing/scott.js index 8794f72..28b472b 100644 --- a/commands/image-editing/scott.js +++ b/commands/image-editing/scott.js @@ -1,11 +1,11 @@ -import ImageCommand from "../../classes/imageCommand.js"; - -class ScottCommand extends ImageCommand { - static description = "Makes Scott the Woz show off an image"; - static aliases = ["woz", "tv", "porn"]; - - static noImage = "You need to provide an image/GIF for Scott to show off!"; - static command = "scott"; -} - -export default ScottCommand; +import ImageCommand from "../../classes/imageCommand.js"; + +class ScottCommand extends ImageCommand { + static description = "Makes Scott the Woz show off an image"; + static aliases = ["woz", "tv", "porn"]; + + static noImage = "You need to provide an image/GIF for Scott to show off!"; + static command = "scott"; +} + +export default ScottCommand; diff --git a/commands/image-editing/sepia.js b/commands/image-editing/sepia.js index 779a9e8..bdd4fb2 100644 --- a/commands/image-editing/sepia.js +++ b/commands/image-editing/sepia.js @@ -1,14 +1,14 @@ -import ImageCommand from "../../classes/imageCommand.js"; - -class SepiaCommand extends ImageCommand { - params = { - color: "sepia" - }; - - static description = "Adds a sepia filter"; - - static noImage = "You need to provide an image/GIF to add a sepia filter!"; - static command = "colors"; -} - -export default SepiaCommand; +import ImageCommand from "../../classes/imageCommand.js"; + +class SepiaCommand extends ImageCommand { + params = { + color: "sepia" + }; + + static description = "Adds a sepia filter"; + + static noImage = "You need to provide an image/GIF to add a sepia filter!"; + static command = "colors"; +} + +export default SepiaCommand; diff --git a/commands/image-editing/sharpen.js b/commands/image-editing/sharpen.js index bfe7928..0886d7e 100644 --- a/commands/image-editing/sharpen.js +++ b/commands/image-editing/sharpen.js @@ -1,15 +1,15 @@ -import ImageCommand from "../../classes/imageCommand.js"; - -class SharpenCommand extends ImageCommand { - params = { - sharp: true - }; - - static description = "Sharpens an image"; - static aliases = ["sharp"]; - - static noImage = "You need to provide an image/GIF to sharpen!"; - static command = "blur"; -} - -export default SharpenCommand; +import ImageCommand from "../../classes/imageCommand.js"; + +class SharpenCommand extends ImageCommand { + params = { + sharp: true + }; + + static description = "Sharpens an image"; + static aliases = ["sharp"]; + + static noImage = "You need to provide an image/GIF to sharpen!"; + static command = "blur"; +} + +export default SharpenCommand; diff --git a/commands/image-editing/shutterstock.js b/commands/image-editing/shutterstock.js index 6c3e325..001e692 100644 --- a/commands/image-editing/shutterstock.js +++ b/commands/image-editing/shutterstock.js @@ -1,17 +1,17 @@ -import ImageCommand from "../../classes/imageCommand.js"; - -class ShutterstockCommand extends ImageCommand { - params = { - water: "assets/images/shutterstock.png", - gravity: 5, - resize: true - }; - - static description = "Adds the Shutterstock watermark to an image"; - static aliases = ["stock", "stockphoto"]; - - static noImage = "You need to provide an image/GIF to add a Shutterstock watermark!"; - static command = "watermark"; -} - -export default ShutterstockCommand; +import ImageCommand from "../../classes/imageCommand.js"; + +class ShutterstockCommand extends ImageCommand { + params = { + water: "assets/images/shutterstock.png", + gravity: 5, + resize: true + }; + + static description = "Adds the Shutterstock watermark to an image"; + static aliases = ["stock", "stockphoto"]; + + static noImage = "You need to provide an image/GIF to add a Shutterstock watermark!"; + static command = "watermark"; +} + +export default ShutterstockCommand; diff --git a/commands/image-editing/slow.js b/commands/image-editing/slow.js index 320699b..59b2ecf 100644 --- a/commands/image-editing/slow.js +++ b/commands/image-editing/slow.js @@ -1,32 +1,32 @@ -import ImageCommand from "../../classes/imageCommand.js"; - -class SlowCommand extends ImageCommand { - params() { - const speed = parseInt(this.options.multiplier ?? this.args[0]); - return { - slow: true, - speed: isNaN(speed) ? 2 : speed - }; - } - - static init() { - super.init(); - this.flags.push({ - name: "multiplier", - type: 4, - description: "Set the speed multiplier (default: 2)", - min_value: 1 - }); - return this; - } - - static description = "Makes an image sequence slower"; - static aliases = ["slowdown", "slower", "gifspeed2"]; - static arguments = ["{multiplier}"]; - - static requiresGIF = true; - static noImage = "You need to provide an image/GIF to slow down!"; - static command = "speed"; -} - -export default SlowCommand; +import ImageCommand from "../../classes/imageCommand.js"; + +class SlowCommand extends ImageCommand { + params() { + const speed = parseInt(this.options.multiplier ?? this.args[0]); + return { + slow: true, + speed: isNaN(speed) ? 2 : speed + }; + } + + static init() { + super.init(); + this.flags.push({ + name: "multiplier", + type: 4, + description: "Set the speed multiplier (default: 2)", + min_value: 1 + }); + return this; + } + + static description = "Makes an image sequence slower"; + static aliases = ["slowdown", "slower", "gifspeed2"]; + static arguments = ["{multiplier}"]; + + static requiresGIF = true; + static noImage = "You need to provide an image/GIF to slow down!"; + static command = "speed"; +} + +export default SlowCommand; diff --git a/commands/image-editing/snapchat.js b/commands/image-editing/snapchat.js index 170c20c..e800060 100644 --- a/commands/image-editing/snapchat.js +++ b/commands/image-editing/snapchat.js @@ -1,36 +1,36 @@ -import ImageCommand from "../../classes/imageCommand.js"; -import { cleanMessage } from "../../utils/misc.js"; - -class SnapchatCommand extends ImageCommand { - params(url) { - const newArgs = this.options.text ?? this.args.filter(item => !item.includes(url)).join(" "); - const position = parseFloat(this.options.position); - return { - caption: cleanMessage(this.message ?? this.interaction, newArgs), - pos: isNaN(position) ? 0.5 : position - }; - } - - static init() { - super.init(); - this.flags.push({ - name: "position", - type: 10, - description: "Set the position of the caption as a decimal (0.0 is top, 1.0 is bottom, default is 0.5)", - min_value: 0, - max_value: 1 - }); - return this; - } - - static description = "Adds a Snapchat style caption to an image"; - static aliases = ["snap", "caption3"]; - 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 = "snapchat"; -} - -export default SnapchatCommand; +import ImageCommand from "../../classes/imageCommand.js"; +import { cleanMessage } from "../../utils/misc.js"; + +class SnapchatCommand extends ImageCommand { + params(url) { + const newArgs = this.options.text ?? this.args.filter(item => !item.includes(url)).join(" "); + const position = parseFloat(this.options.position); + return { + caption: cleanMessage(this.message ?? this.interaction, newArgs), + pos: isNaN(position) ? 0.5 : position + }; + } + + static init() { + super.init(); + this.flags.push({ + name: "position", + type: 10, + description: "Set the position of the caption as a decimal (0.0 is top, 1.0 is bottom, default is 0.5)", + min_value: 0, + max_value: 1 + }); + return this; + } + + static description = "Adds a Snapchat style caption to an image"; + static aliases = ["snap", "caption3"]; + 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 = "snapchat"; +} + +export default SnapchatCommand; diff --git a/commands/image-editing/soos.js b/commands/image-editing/soos.js index 9f70dda..9aee465 100644 --- a/commands/image-editing/soos.js +++ b/commands/image-editing/soos.js @@ -1,16 +1,16 @@ -import ImageCommand from "../../classes/imageCommand.js"; - -class SooSCommand extends ImageCommand { - params = { - soos: true - }; - - static description = "\"Loops\" an image sequence by reversing it when it's finished"; - static aliases = ["boomerang"]; - - static requiresGIF = true; - static noImage = "You need to provide an image/GIF to loop!"; - static command = "reverse"; -} - -export default SooSCommand; +import ImageCommand from "../../classes/imageCommand.js"; + +class SooSCommand extends ImageCommand { + params = { + soos: true + }; + + static description = "\"Loops\" an image sequence by reversing it when it's finished"; + static aliases = ["boomerang"]; + + static requiresGIF = true; + static noImage = "You need to provide an image/GIF to loop!"; + static command = "reverse"; +} + +export default SooSCommand; diff --git a/commands/image-editing/speechbubble.js b/commands/image-editing/speechbubble.js index 4e3d40c..1ee494b 100644 --- a/commands/image-editing/speechbubble.js +++ b/commands/image-editing/speechbubble.js @@ -1,36 +1,36 @@ -import ImageCommand from "../../classes/imageCommand.js"; - -class SpeechBubbleCommand extends ImageCommand { - params() { - return { - water: this.options.alpha ? "assets/images/speech.png" : "assets/images/speechbubble.png", - gravity: "north", - resize: true, - yscale: 0.2, - alpha: this.options.alpha ? true : false, - flip: this.options.flip ? true : false - }; - } - - static init() { - super.init(); - this.flags.push({ - name: "alpha", - description: "Make the top of the speech bubble transparent", - type: 5 - }, { - name: "flip", - description: "Flips the speech bubble", - type: 5 - }); - return this; - } - - static description = "Adds a speech bubble to an image"; - static aliases = ["speech"]; - - static noImage = "You need to provide an image/GIF to add a speech bubble!"; - static command = "watermark"; -} - -export default SpeechBubbleCommand; +import ImageCommand from "../../classes/imageCommand.js"; + +class SpeechBubbleCommand extends ImageCommand { + params() { + return { + water: this.options.alpha ? "assets/images/speech.png" : "assets/images/speechbubble.png", + gravity: "north", + resize: true, + yscale: 0.2, + alpha: this.options.alpha ? true : false, + flip: this.options.flip ? true : false + }; + } + + static init() { + super.init(); + this.flags.push({ + name: "alpha", + description: "Make the top of the speech bubble transparent", + type: 5 + }, { + name: "flip", + description: "Flips the speech bubble", + type: 5 + }); + return this; + } + + static description = "Adds a speech bubble to an image"; + static aliases = ["speech"]; + + static noImage = "You need to provide an image/GIF to add a speech bubble!"; + static command = "watermark"; +} + +export default SpeechBubbleCommand; diff --git a/commands/image-editing/speed.js b/commands/image-editing/speed.js index dc91e94..ea332f6 100644 --- a/commands/image-editing/speed.js +++ b/commands/image-editing/speed.js @@ -1,31 +1,31 @@ -import ImageCommand from "../../classes/imageCommand.js"; - -class SpeedCommand extends ImageCommand { - params() { - const speed = parseInt(this.options.multiplier ?? this.args[0]); - return { - speed: isNaN(speed) || speed < 1 ? 2 : speed - }; - } - - static init() { - super.init(); - this.flags.push({ - name: "multiplier", - type: 4, - description: "Set the speed multiplier (default: 2)", - min_value: 1 - }); - return this; - } - - static description = "Makes an image sequence faster"; - static aliases = ["speedup", "fast", "gifspeed", "faster"]; - static arguments = ["{multiplier}"]; - - static requiresGIF = true; - static noImage = "You need to provide an image/GIF to speed up!"; - static command = "speed"; -} - -export default SpeedCommand; +import ImageCommand from "../../classes/imageCommand.js"; + +class SpeedCommand extends ImageCommand { + params() { + const speed = parseInt(this.options.multiplier ?? this.args[0]); + return { + speed: isNaN(speed) || speed < 1 ? 2 : speed + }; + } + + static init() { + super.init(); + this.flags.push({ + name: "multiplier", + type: 4, + description: "Set the speed multiplier (default: 2)", + min_value: 1 + }); + return this; + } + + static description = "Makes an image sequence faster"; + static aliases = ["speedup", "fast", "gifspeed", "faster"]; + static arguments = ["{multiplier}"]; + + static requiresGIF = true; + static noImage = "You need to provide an image/GIF to speed up!"; + static command = "speed"; +} + +export default SpeedCommand; diff --git a/commands/image-editing/spin.js b/commands/image-editing/spin.js index ef6387d..6dca188 100644 --- a/commands/image-editing/spin.js +++ b/commands/image-editing/spin.js @@ -1,11 +1,11 @@ -import ImageCommand from "../../classes/imageCommand.js"; - -class SpinCommand extends ImageCommand { - static description = "Spins an image"; - static aliases = ["rotate"]; - - static noImage = "You need to provide an image/GIF to spin!"; - static command = "spin"; -} - -export default SpinCommand; +import ImageCommand from "../../classes/imageCommand.js"; + +class SpinCommand extends ImageCommand { + static description = "Spins an image"; + static aliases = ["rotate"]; + + static noImage = "You need to provide an image/GIF to spin!"; + static command = "spin"; +} + +export default SpinCommand; diff --git a/commands/image-editing/squish.js b/commands/image-editing/squish.js index f3a575c..173a040 100644 --- a/commands/image-editing/squish.js +++ b/commands/image-editing/squish.js @@ -1,11 +1,11 @@ -import ImageCommand from "../../classes/imageCommand.js"; - -class SquishCommand extends ImageCommand { - static description = "Squishes/stretches an image"; - static aliases = ["squishy", "squash"]; - - static noImage = "You need to provide an image/GIF to squish!"; - static command = "squish"; -} - -export default SquishCommand; +import ImageCommand from "../../classes/imageCommand.js"; + +class SquishCommand extends ImageCommand { + static description = "Squishes/stretches an image"; + static aliases = ["squishy", "squash"]; + + static noImage = "You need to provide an image/GIF to squish!"; + static command = "squish"; +} + +export default SquishCommand; diff --git a/commands/image-editing/stretch.js b/commands/image-editing/stretch.js index 0f89814..131dcb1 100644 --- a/commands/image-editing/stretch.js +++ b/commands/image-editing/stretch.js @@ -1,15 +1,15 @@ -import ImageCommand from "../../classes/imageCommand.js"; - -class StretchCommand extends ImageCommand { - params = { - stretch: true - }; - - static description = "Stretches an image to a 1:1 aspect ratio"; - static aliases = ["aspect", "ratio", "aspect11", "11"]; - - static noImage = "You need to provide an image/GIF to stretch!"; - static command = "resize"; -} - -export default StretchCommand; +import ImageCommand from "../../classes/imageCommand.js"; + +class StretchCommand extends ImageCommand { + params = { + stretch: true + }; + + static description = "Stretches an image to a 1:1 aspect ratio"; + static aliases = ["aspect", "ratio", "aspect11", "11"]; + + static noImage = "You need to provide an image/GIF to stretch!"; + static command = "resize"; +} + +export default StretchCommand; diff --git a/commands/image-editing/swirl.js b/commands/image-editing/swirl.js index 476b506..a95cd15 100644 --- a/commands/image-editing/swirl.js +++ b/commands/image-editing/swirl.js @@ -1,11 +1,11 @@ -import ImageCommand from "../../classes/imageCommand.js"; - -class SwirlCommand extends ImageCommand { - static description = "Swirls an image"; - static aliases = ["whirlpool", "distort"]; - - static noImage = "You need to provide an image/GIF to swirl!"; - static command = "swirl"; -} - -export default SwirlCommand; +import ImageCommand from "../../classes/imageCommand.js"; + +class SwirlCommand extends ImageCommand { + static description = "Swirls an image"; + static aliases = ["whirlpool", "distort"]; + + static noImage = "You need to provide an image/GIF to swirl!"; + static command = "swirl"; +} + +export default SwirlCommand; diff --git a/commands/image-editing/tile.js b/commands/image-editing/tile.js index 6566253..a302f4f 100644 --- a/commands/image-editing/tile.js +++ b/commands/image-editing/tile.js @@ -1,11 +1,11 @@ -import ImageCommand from "../../classes/imageCommand.js"; - -class TileCommand extends ImageCommand { - static description = "Creates a tile pattern from an image"; - static aliases = ["wall2"]; - - static noImage = "You need to provide an image/GIF to tile!"; - static command = "tile"; -} - -export default TileCommand; +import ImageCommand from "../../classes/imageCommand.js"; + +class TileCommand extends ImageCommand { + static description = "Creates a tile pattern from an image"; + static aliases = ["wall2"]; + + static noImage = "You need to provide an image/GIF to tile!"; + static command = "tile"; +} + +export default TileCommand; diff --git a/commands/image-editing/uncanny.js b/commands/image-editing/uncanny.js index 49bf262..a6ee468 100644 --- a/commands/image-editing/uncanny.js +++ b/commands/image-editing/uncanny.js @@ -1,65 +1,65 @@ -import ImageCommand from "../../classes/imageCommand.js"; -import { random, cleanMessage } from "../../utils/misc.js"; -import { readdirSync } from "fs"; -import { resolve, dirname } from "path"; -import { fileURLToPath } from "url"; -const prompts = ["you found:", "your dad is:", "you ate:", "your mom is:", "your sister is:", "you saw:", "you get lost in:", "you find:", "you grab:", "you pull out of your pocket:", "you fight:", "it's in your room:"]; -const names = readdirSync(resolve(dirname(fileURLToPath(import.meta.url)), "../../assets/images/uncanny/")).filter((val) => { - if (!val.startsWith(".") && val.endsWith(".png")) return true; -}).map((val) => { - return val.split(".")[0]; -}); - -class UncannyCommand extends ImageCommand { - params(url, name = "unknown") { - const newArgs = this.options.text ?? this.args.join(" "); - // eslint-disable-next-line prefer-const - let [text1, text2] = newArgs.replaceAll(url, "").split(/(? elem.trim()); - if (!text2?.trim()) text2 = name; - return { - caption: text1?.trim() ? cleanMessage(this.message ?? this.interaction, text1) : random(prompts), - caption2: cleanMessage(this.message ?? this.interaction, text2), - path: `assets/images/uncanny/${typeof this.options.phase === "string" && names.includes(this.options.phase.toLowerCase()) ? this.options.phase.toLowerCase() : random(names.filter((val) => val !== "goated"))}.png`, - 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: "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)" - }, { - name: "phase", - type: 3, - choices: (() => { - const array = []; - for (const name of names) { - array.push({ name, value: name }); - } - return array; - })(), - description: "Specify the uncanny image you want to use" - }); - return this; - } - - static textOptional = true; - - static description = "Makes a Mr. Incredible Becomes Uncanny image (separate left/right text with a comma)"; - static aliases = ["canny", "incredible", "pain"]; - static arguments = ["{left text}", "{right text}"]; - - static noImage = "You need to provide an image/GIF to create an uncanny image!"; - static command = "uncanny"; -} - -export default UncannyCommand; +import ImageCommand from "../../classes/imageCommand.js"; +import { random, cleanMessage } from "../../utils/misc.js"; +import { readdirSync } from "fs"; +import { resolve, dirname } from "path"; +import { fileURLToPath } from "url"; +const prompts = ["you found:", "your dad is:", "you ate:", "your mom is:", "your sister is:", "you saw:", "you get lost in:", "you find:", "you grab:", "you pull out of your pocket:", "you fight:", "it's in your room:"]; +const names = readdirSync(resolve(dirname(fileURLToPath(import.meta.url)), "../../assets/images/uncanny/")).filter((val) => { + if (!val.startsWith(".") && val.endsWith(".png")) return true; +}).map((val) => { + return val.split(".")[0]; +}); + +class UncannyCommand extends ImageCommand { + params(url, name = "unknown") { + const newArgs = this.options.text ?? this.args.join(" "); + // eslint-disable-next-line prefer-const + let [text1, text2] = newArgs.replaceAll(url, "").split(/(? elem.trim()); + if (!text2?.trim()) text2 = name; + return { + caption: text1?.trim() ? cleanMessage(this.message ?? this.interaction, text1) : random(prompts), + caption2: cleanMessage(this.message ?? this.interaction, text2), + path: `assets/images/uncanny/${typeof this.options.phase === "string" && names.includes(this.options.phase.toLowerCase()) ? this.options.phase.toLowerCase() : random(names.filter((val) => val !== "goated"))}.png`, + 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: "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)" + }, { + name: "phase", + type: 3, + choices: (() => { + const array = []; + for (const name of names) { + array.push({ name, value: name }); + } + return array; + })(), + description: "Specify the uncanny image you want to use" + }); + return this; + } + + static textOptional = true; + + static description = "Makes a Mr. Incredible Becomes Uncanny image (separate left/right text with a comma)"; + static aliases = ["canny", "incredible", "pain"]; + static arguments = ["{left text}", "{right text}"]; + + static noImage = "You need to provide an image/GIF to create an uncanny image!"; + static command = "uncanny"; +} + +export default UncannyCommand; diff --git a/commands/image-editing/uncaption.js b/commands/image-editing/uncaption.js index 34f2b15..c8992a1 100644 --- a/commands/image-editing/uncaption.js +++ b/commands/image-editing/uncaption.js @@ -1,29 +1,29 @@ -import ImageCommand from "../../classes/imageCommand.js"; - -class UncaptionCommand extends ImageCommand { - params() { - const tolerance = parseFloat(this.options.tolerance); - return { - tolerance: isNaN(tolerance) ? 0.95 : tolerance - }; - } - - static init() { - super.init(); - this.flags.push({ - name: "tolerance", - type: 10, - description: "Set the shade tolerance for the caption detection (0.0 is highest, 1.0 is lowest, default is 0.95)", - min_value: 0, - max_value: 1 - }); - return this; - } - - static description = "Removes the caption from an image"; - - static noImage = "You need to provide an image/GIF to uncaption!"; - static command = "uncaption"; -} - -export default UncaptionCommand; +import ImageCommand from "../../classes/imageCommand.js"; + +class UncaptionCommand extends ImageCommand { + params() { + const tolerance = parseFloat(this.options.tolerance); + return { + tolerance: isNaN(tolerance) ? 0.95 : tolerance + }; + } + + static init() { + super.init(); + this.flags.push({ + name: "tolerance", + type: 10, + description: "Set the shade tolerance for the caption detection (0.0 is highest, 1.0 is lowest, default is 0.95)", + min_value: 0, + max_value: 1 + }); + return this; + } + + static description = "Removes the caption from an image"; + + static noImage = "You need to provide an image/GIF to uncaption!"; + static command = "uncaption"; +} + +export default UncaptionCommand; diff --git a/commands/image-editing/unfreeze.js b/commands/image-editing/unfreeze.js index 13ed4c2..b13559c 100644 --- a/commands/image-editing/unfreeze.js +++ b/commands/image-editing/unfreeze.js @@ -1,15 +1,15 @@ -import ImageCommand from "../../classes/imageCommand.js"; - -class UnfreezeCommand extends ImageCommand { - params = { - loop: true - }; - - static description = "Unfreezes an image sequence"; - - static requiresGIF = true; - static noImage = "You need to provide an image/GIF to unfreeze!"; - static command = "freeze"; -} - -export default UnfreezeCommand; +import ImageCommand from "../../classes/imageCommand.js"; + +class UnfreezeCommand extends ImageCommand { + params = { + loop: true + }; + + static description = "Unfreezes an image sequence"; + + static requiresGIF = true; + static noImage = "You need to provide an image/GIF to unfreeze!"; + static command = "freeze"; +} + +export default UnfreezeCommand; diff --git a/commands/image-editing/waaw.js b/commands/image-editing/waaw.js index b3e08c3..b9d3e3b 100644 --- a/commands/image-editing/waaw.js +++ b/commands/image-editing/waaw.js @@ -1,11 +1,11 @@ -import ImageCommand from "../../classes/imageCommand.js"; - -class WaaWCommand extends ImageCommand { - static description = "Mirrors the right side of an image onto the left"; - static aliases = ["magik3", "mirror"]; - - static noImage = "You need to provide an image/GIF to mirror!"; - static command = "mirror"; -} - -export default WaaWCommand; +import ImageCommand from "../../classes/imageCommand.js"; + +class WaaWCommand extends ImageCommand { + static description = "Mirrors the right side of an image onto the left"; + static aliases = ["magik3", "mirror"]; + + static noImage = "You need to provide an image/GIF to mirror!"; + static command = "mirror"; +} + +export default WaaWCommand; diff --git a/commands/image-editing/wall.js b/commands/image-editing/wall.js index 1854e71..026b4ad 100644 --- a/commands/image-editing/wall.js +++ b/commands/image-editing/wall.js @@ -1,10 +1,10 @@ -import ImageCommand from "../../classes/imageCommand.js"; - -class WallCommand extends ImageCommand { - static description = "Creates a wall from an image"; - - static noImage = "You need to provide an image/GIF to make a wall!"; - static command = "wall"; -} - -export default WallCommand; +import ImageCommand from "../../classes/imageCommand.js"; + +class WallCommand extends ImageCommand { + static description = "Creates a wall from an image"; + + static noImage = "You need to provide an image/GIF to make a wall!"; + static command = "wall"; +} + +export default WallCommand; diff --git a/commands/image-editing/whisper.js b/commands/image-editing/whisper.js index 64b8c25..d0a4c5b 100644 --- a/commands/image-editing/whisper.js +++ b/commands/image-editing/whisper.js @@ -1,22 +1,22 @@ -import ImageCommand from "../../classes/imageCommand.js"; -import { cleanMessage } from "../../utils/misc.js"; - -class WhisperCommand extends ImageCommand { - params(url) { - const newArgs = this.options.text ?? this.args.filter(item => !item.includes(url)).join(" "); - return { - caption: cleanMessage(this.message ?? this.interaction, newArgs) - }; - } - - static description = "Adds a Whisper style caption to an image"; - static aliases = ["caption4"]; - 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 = "whisper"; -} - -export default WhisperCommand; +import ImageCommand from "../../classes/imageCommand.js"; +import { cleanMessage } from "../../utils/misc.js"; + +class WhisperCommand extends ImageCommand { + params(url) { + const newArgs = this.options.text ?? this.args.filter(item => !item.includes(url)).join(" "); + return { + caption: cleanMessage(this.message ?? this.interaction, newArgs) + }; + } + + static description = "Adds a Whisper style caption to an image"; + static aliases = ["caption4"]; + 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 = "whisper"; +} + +export default WhisperCommand; diff --git a/commands/image-editing/wide.js b/commands/image-editing/wide.js index 8baffc2..d73e7cb 100644 --- a/commands/image-editing/wide.js +++ b/commands/image-editing/wide.js @@ -1,15 +1,15 @@ -import ImageCommand from "../../classes/imageCommand.js"; - -class WideCommand extends ImageCommand { - params = { - wide: true - }; - - static description = "Stretches an image to 19x its width"; - static aliases = ["w19", "wide19"]; - - static noImage = "You need to provide an image/GIF to stretch!"; - static command = "resize"; -} - -export default WideCommand; +import ImageCommand from "../../classes/imageCommand.js"; + +class WideCommand extends ImageCommand { + params = { + wide: true + }; + + static description = "Stretches an image to 19x its width"; + static aliases = ["w19", "wide19"]; + + static noImage = "You need to provide an image/GIF to stretch!"; + static command = "resize"; +} + +export default WideCommand; diff --git a/commands/image-editing/woow.js b/commands/image-editing/woow.js index fda4b90..a941a28 100644 --- a/commands/image-editing/woow.js +++ b/commands/image-editing/woow.js @@ -1,16 +1,16 @@ -import ImageCommand from "../../classes/imageCommand.js"; - -class WooWCommand extends ImageCommand { - params = { - vertical: true, - first: true - }; - - static description = "Mirrors the top of an image onto the bottom"; - static aliases = ["magik5", "mirror3"]; - - static noImage = "You need to provide an image/GIF to mirror!"; - static command = "mirror"; -} - -export default WooWCommand; +import ImageCommand from "../../classes/imageCommand.js"; + +class WooWCommand extends ImageCommand { + params = { + vertical: true, + first: true + }; + + static description = "Mirrors the top of an image onto the bottom"; + static aliases = ["magik5", "mirror3"]; + + static noImage = "You need to provide an image/GIF to mirror!"; + static command = "mirror"; +} + +export default WooWCommand; diff --git a/commands/image-editing/zamn.js b/commands/image-editing/zamn.js index 28c6e19..847ae29 100644 --- a/commands/image-editing/zamn.js +++ b/commands/image-editing/zamn.js @@ -1,10 +1,10 @@ -import ImageCommand from "../../classes/imageCommand.js"; - -class ZamnCommand extends ImageCommand { - static description = "Adds a \"ZAMN\" reaction to an image"; - - static noImage = "You need to provide an image/GIF to \"ZAMN\" at!"; - static command = "zamn"; -} - -export default ZamnCommand; +import ImageCommand from "../../classes/imageCommand.js"; + +class ZamnCommand extends ImageCommand { + static description = "Adds a \"ZAMN\" reaction to an image"; + + static noImage = "You need to provide an image/GIF to \"ZAMN\" at!"; + static command = "zamn"; +} + +export default ZamnCommand; diff --git a/commands/message/select-image.js b/commands/message/select-image.js index bfd047d..54400ed 100644 --- a/commands/message/select-image.js +++ b/commands/message/select-image.js @@ -1,23 +1,23 @@ -import Command from "../../classes/command.js"; -import imageDetect from "../../utils/imagedetect.js"; -import { selectedImages } from "../../utils/collections.js"; - -class SelectImageCommand extends Command { - async run() { - await this.acknowledge(); - const message = this.interaction.data.target; - const image = await imageDetect(this.client, message, this.interaction, this.options, true, false, false, true); - this.success = false; - if (image === undefined) { - return "I couldn't find an image in that message!"; - } else if (image.type === "large") { - return "That image is too large (>= 25MB)! Try using a smaller image."; - } else if (image.type === "tenorlimit") { - return "I've been rate-limited by Tenor. Please try uploading your GIF elsewhere."; - } - selectedImages.set(this.author.id, image); - return "The image has been selected for your next command."; - } -} - +import Command from "../../classes/command.js"; +import imageDetect from "../../utils/imagedetect.js"; +import { selectedImages } from "../../utils/collections.js"; + +class SelectImageCommand extends Command { + async run() { + await this.acknowledge(); + const message = this.interaction.data.target; + const image = await imageDetect(this.client, message, this.interaction, this.options, true, false, false, true); + this.success = false; + if (image === undefined) { + return "I couldn't find an image in that message!"; + } else if (image.type === "large") { + return "That image is too large (>= 25MB)! Try using a smaller image."; + } else if (image.type === "tenorlimit") { + return "I've been rate-limited by Tenor. Please try uploading your GIF elsewhere."; + } + selectedImages.set(this.author.id, image); + return "The image has been selected for your next command."; + } +} + export default SelectImageCommand; \ No newline at end of file diff --git a/commands/music/host.js b/commands/music/host.js index 584cf6c..20c89de 100644 --- a/commands/music/host.js +++ b/commands/music/host.js @@ -1,60 +1,60 @@ -import { players } from "../../utils/soundplayer.js"; -import MusicCommand from "../../classes/musicCommand.js"; - -class HostCommand extends MusicCommand { - async run() { - this.success = false; - if (!this.guild) return "This command only works in servers!"; - if (!this.member.voiceState) return "You need to be in a voice channel first!"; - if (!this.guild.voiceStates.has(this.client.user.id)) return "I'm not in a voice channel!"; - if (!this.connection) return "I haven't completely connected yet!"; - if (this.connection.host !== this.author.id && !process.env.OWNER.split(",").includes(this.connection.host)) return "Only the current voice session host can choose another host!"; - const input = this.options.user ?? this.args.join(" "); - if (input?.trim()) { - let user; - if (this.type === "classic") { - const getUser = this.message.mentions.users.length >= 1 ? this.message.mentions.users[0] : this.client.users.get(input); - if (getUser) { - user = getUser; - } else if (input.match(/^?$/) && input >= 21154535154122752n) { - try { - user = await this.client.rest.users.get(input); - } catch { - // no-op - } - } else { - const userRegex = new RegExp(input.split(" ").join("|"), "i"); - const member = this.client.users.find(element => { - return userRegex.test(element.username); - }); - user = member; - } - } else { - user = input; - } - if (!user) return "I can't find that user!"; - if (user.bot) return "This is illegal, you know."; - const member = this.guild.members.get(user.id); - if (!member) return "That user isn't in this server!"; - const object = this.connection; - object.host = member.id; - players.set(this.guildID, object); - this.success = true; - return `🔊 ${member.mention} is the new voice channel host.`; - } else { - const member = this.guild.members.get(players.get(this.guild.id).host); - this.success = true; - return `🔊 The current voice channel host is **${member?.username}#${member?.discriminator}**.`; - } - } - - static flags = [{ - name: "user", - type: 6, - description: "The user you want the new host to be" - }]; - static description = "Gets or changes the host of the current voice session"; - static aliases = ["sethost"]; -} - -export default HostCommand; +import { players } from "../../utils/soundplayer.js"; +import MusicCommand from "../../classes/musicCommand.js"; + +class HostCommand extends MusicCommand { + async run() { + this.success = false; + if (!this.guild) return "This command only works in servers!"; + if (!this.member.voiceState) return "You need to be in a voice channel first!"; + if (!this.guild.voiceStates.has(this.client.user.id)) return "I'm not in a voice channel!"; + if (!this.connection) return "I haven't completely connected yet!"; + if (this.connection.host !== this.author.id && !process.env.OWNER.split(",").includes(this.connection.host)) return "Only the current voice session host can choose another host!"; + const input = this.options.user ?? this.args.join(" "); + if (input?.trim()) { + let user; + if (this.type === "classic") { + const getUser = this.message.mentions.users.length >= 1 ? this.message.mentions.users[0] : this.client.users.get(input); + if (getUser) { + user = getUser; + } else if (input.match(/^?$/) && input >= 21154535154122752n) { + try { + user = await this.client.rest.users.get(input); + } catch { + // no-op + } + } else { + const userRegex = new RegExp(input.split(" ").join("|"), "i"); + const member = this.client.users.find(element => { + return userRegex.test(element.username); + }); + user = member; + } + } else { + user = input; + } + if (!user) return "I can't find that user!"; + if (user.bot) return "This is illegal, you know."; + const member = this.guild.members.get(user.id); + if (!member) return "That user isn't in this server!"; + const object = this.connection; + object.host = member.id; + players.set(this.guildID, object); + this.success = true; + return `🔊 ${member.mention} is the new voice channel host.`; + } else { + const member = this.guild.members.get(players.get(this.guild.id).host); + this.success = true; + return `🔊 The current voice channel host is **${member?.username}#${member?.discriminator}**.`; + } + } + + static flags = [{ + name: "user", + type: 6, + description: "The user you want the new host to be" + }]; + static description = "Gets or changes the host of the current voice session"; + static aliases = ["sethost"]; +} + +export default HostCommand; diff --git a/commands/music/loop.js b/commands/music/loop.js index 8d5331e..b37f494 100644 --- a/commands/music/loop.js +++ b/commands/music/loop.js @@ -1,23 +1,23 @@ -import { players } from "../../utils/soundplayer.js"; -import MusicCommand from "../../classes/musicCommand.js"; - -class LoopCommand extends MusicCommand { - async run() { - this.success = false; - if (!this.guild) return "This command only works in servers!"; - if (!this.member.voiceState) return "You need to be in a voice channel first!"; - if (!this.guild.voiceStates.has(this.client.user.id)) return "I'm not in a voice channel!"; - if (!this.connection) return "I haven't completely connected yet!"; - if (this.connection.host !== this.author.id && !this.member.permissions.has("MANAGE_CHANNELS")) return "Only the current voice session host can loop the music!"; - const object = this.connection; - object.loop = !object.loop; - players.set(this.guild.id, object); - this.success = true; - return object.loop ? "🔊 The player is now looping." : "🔊 The player is no longer looping."; - } - - static description = "Loops the music"; - static aliases = ["toggleloop", "repeat"]; -} - +import { players } from "../../utils/soundplayer.js"; +import MusicCommand from "../../classes/musicCommand.js"; + +class LoopCommand extends MusicCommand { + async run() { + this.success = false; + if (!this.guild) return "This command only works in servers!"; + if (!this.member.voiceState) return "You need to be in a voice channel first!"; + if (!this.guild.voiceStates.has(this.client.user.id)) return "I'm not in a voice channel!"; + if (!this.connection) return "I haven't completely connected yet!"; + if (this.connection.host !== this.author.id && !this.member.permissions.has("MANAGE_CHANNELS")) return "Only the current voice session host can loop the music!"; + const object = this.connection; + object.loop = !object.loop; + players.set(this.guild.id, object); + this.success = true; + return object.loop ? "🔊 The player is now looping." : "🔊 The player is no longer looping."; + } + + static description = "Loops the music"; + static aliases = ["toggleloop", "repeat"]; +} + export default LoopCommand; \ No newline at end of file diff --git a/commands/music/music.js b/commands/music/music.js index 684a880..17b1cc2 100644 --- a/commands/music/music.js +++ b/commands/music/music.js @@ -1,48 +1,48 @@ -import Command from "../../classes/command.js"; -import { commands, aliases, info, categories } from "../../utils/collections.js"; - -// all-in-one music command -class MusicAIOCommand extends Command { - async run() { - let cmd = this.type === "classic" ? this.args[0] : this.optionsArray[0].name; - if (cmd === "music" || this.constructor.aliases.includes(cmd)) return "https://esmbot.net/robotdance.gif"; - await this.acknowledge(); - if (this.type === "classic") { - this.origOptions.args.shift(); - } else { - this.origOptions.interaction.data.options.raw = this.origOptions.interaction.data.options.raw[0].options; - } - if (aliases.has(cmd)) cmd = aliases.get(cmd); - if (commands.has(cmd) && info.get(cmd).category === "music") { - const command = commands.get(cmd); - const inst = new command(this.client, this.origOptions); - const result = await inst.run(); - this.success = inst.success; - return result; - } else { - this.success = false; - return "That isn't a valid music command!"; - } - } - - static postInit() { - this.flags = []; - for (const cmd of categories.get("music")) { - if (cmd === "music") continue; - const cmdInfo = info.get(cmd); - this.flags.push({ - name: cmd, - type: 1, - description: cmdInfo.description, - options: cmdInfo.flags - }); - } - return this; - } - - static description = "Handles music playback"; - static aliases = ["m"]; - static directAllowed = false; -} - -export default MusicAIOCommand; +import Command from "../../classes/command.js"; +import { commands, aliases, info, categories } from "../../utils/collections.js"; + +// all-in-one music command +class MusicAIOCommand extends Command { + async run() { + let cmd = this.type === "classic" ? this.args[0] : this.optionsArray[0].name; + if (cmd === "music" || this.constructor.aliases.includes(cmd)) return "https://esmbot.net/robotdance.gif"; + await this.acknowledge(); + if (this.type === "classic") { + this.origOptions.args.shift(); + } else { + this.origOptions.interaction.data.options.raw = this.origOptions.interaction.data.options.raw[0].options; + } + if (aliases.has(cmd)) cmd = aliases.get(cmd); + if (commands.has(cmd) && info.get(cmd).category === "music") { + const command = commands.get(cmd); + const inst = new command(this.client, this.origOptions); + const result = await inst.run(); + this.success = inst.success; + return result; + } else { + this.success = false; + return "That isn't a valid music command!"; + } + } + + static postInit() { + this.flags = []; + for (const cmd of categories.get("music")) { + if (cmd === "music") continue; + const cmdInfo = info.get(cmd); + this.flags.push({ + name: cmd, + type: 1, + description: cmdInfo.description, + options: cmdInfo.flags + }); + } + return this; + } + + static description = "Handles music playback"; + static aliases = ["m"]; + static directAllowed = false; +} + +export default MusicAIOCommand; diff --git a/commands/music/nowplaying.js b/commands/music/nowplaying.js index e93a768..f453048 100644 --- a/commands/music/nowplaying.js +++ b/commands/music/nowplaying.js @@ -1,51 +1,51 @@ -import format from "format-duration"; -import MusicCommand from "../../classes/musicCommand.js"; - -class NowPlayingCommand extends MusicCommand { - async run() { - this.success = false; - if (!this.guild) return "This command only works in servers!"; - if (!this.member.voiceState) return "You need to be in a voice channel first!"; - if (!this.guild.voiceStates.has(this.client.user.id)) return "I'm not in a voice channel!"; - if (!this.connection) return "I haven't completely connected yet!"; - const player = this.connection.player; - if (!player) return "I'm not playing anything!"; - const track = await player.node.rest.decode(player.track); - const parts = Math.floor((player.position / track.length) * 10); - this.success = true; - return { - embeds: [{ - color: 16711680, - author: { - name: "Now Playing", - iconURL: this.client.user.avatarURL() - }, - fields: [{ - name: "ℹī¸ Title", - value: track.title ? track.title : "Unknown" - }, - { - name: "🎤 Artist", - value: track.author ? track.author : "Unknown" - }, - { - name: "đŸ’Ŧ Channel", - value: (this.guild.channels.get(this.member.voiceState.channelID) ?? await this.client.rest.channels.get(this.member.voiceState.channelID)).name - }, - { - name: "🌐 Node", - value: player.node ? player.node.name : "Unknown" - }, - { - name: `${"â–Ŧ".repeat(parts)}🔘${"â–Ŧ".repeat(10 - parts)}`, - value: `${format(player.position)}/${track.isStream ? "∞" : format(track.length)}` - }] - }] - }; - } - - static description = "Shows the currently playing song"; - static aliases = ["playing", "np", "current"]; -} - -export default NowPlayingCommand; +import format from "format-duration"; +import MusicCommand from "../../classes/musicCommand.js"; + +class NowPlayingCommand extends MusicCommand { + async run() { + this.success = false; + if (!this.guild) return "This command only works in servers!"; + if (!this.member.voiceState) return "You need to be in a voice channel first!"; + if (!this.guild.voiceStates.has(this.client.user.id)) return "I'm not in a voice channel!"; + if (!this.connection) return "I haven't completely connected yet!"; + const player = this.connection.player; + if (!player) return "I'm not playing anything!"; + const track = await player.node.rest.decode(player.track); + const parts = Math.floor((player.position / track.length) * 10); + this.success = true; + return { + embeds: [{ + color: 16711680, + author: { + name: "Now Playing", + iconURL: this.client.user.avatarURL() + }, + fields: [{ + name: "ℹī¸ Title", + value: track.title ? track.title : "Unknown" + }, + { + name: "🎤 Artist", + value: track.author ? track.author : "Unknown" + }, + { + name: "đŸ’Ŧ Channel", + value: (this.guild.channels.get(this.member.voiceState.channelID) ?? await this.client.rest.channels.get(this.member.voiceState.channelID)).name + }, + { + name: "🌐 Node", + value: player.node ? player.node.name : "Unknown" + }, + { + name: `${"â–Ŧ".repeat(parts)}🔘${"â–Ŧ".repeat(10 - parts)}`, + value: `${format(player.position)}/${track.isStream ? "∞" : format(track.length)}` + }] + }] + }; + } + + static description = "Shows the currently playing song"; + static aliases = ["playing", "np", "current"]; +} + +export default NowPlayingCommand; diff --git a/commands/music/play.js b/commands/music/play.js index e48eeb7..ac950a9 100644 --- a/commands/music/play.js +++ b/commands/music/play.js @@ -1,41 +1,41 @@ -import { play } from "../../utils/soundplayer.js"; -import MusicCommand from "../../classes/musicCommand.js"; -const prefixes = ["scsearch:", "spsearch:", "sprec:", "amsearch:", "dzsearch:", "dzisrc:"]; -if (process.env.YT_DISABLED !== "true") prefixes.push("ytsearch:", "ytmsearch:"); - -class PlayCommand extends MusicCommand { - async run() { - const input = this.options.query ?? this.args.join(" "); - if (!input && ((!this.message || this.message?.attachments.size <= 0))) { - this.success = false; - return "You need to provide what you want to play!"; - } - let query = input ? input.trim() : ""; - const attachment = this.type === "classic" ? this.message.attachments.first() : null; - if (query.startsWith("||") && query.endsWith("||")) { - query = query.substring(2, query.length - 2); - } - if (query.startsWith("<") && query.endsWith(">")) { - query = query.substring(1, query.length - 1); - } - try { - const url = new URL(query); - return play(this.client, url, { channel: this.channel, member: this.member, type: this.type, interaction: this.interaction }, true); - } catch { - const search = prefixes.some(v => query.startsWith(v)) ? query : !query && attachment ? attachment.url : (process.env.YT_DISABLED !== "true" ? `ytsearch:${query}` : `dzsearch:${query}`); - return play(this.client, search, { channel: this.channel, member: this.member, type: this.type, interaction: this.interaction }, true); - } - } - - static flags = [{ - name: "query", - type: 3, - description: "An audio search query or URL", - required: true - }]; - static description = "Plays a song or adds it to the queue"; - static aliases = ["p"]; - static arguments = ["[url]"]; -} - -export default PlayCommand; +import { play } from "../../utils/soundplayer.js"; +import MusicCommand from "../../classes/musicCommand.js"; +const prefixes = ["scsearch:", "spsearch:", "sprec:", "amsearch:", "dzsearch:", "dzisrc:"]; +if (process.env.YT_DISABLED !== "true") prefixes.push("ytsearch:", "ytmsearch:"); + +class PlayCommand extends MusicCommand { + async run() { + const input = this.options.query ?? this.args.join(" "); + if (!input && ((!this.message || this.message?.attachments.size <= 0))) { + this.success = false; + return "You need to provide what you want to play!"; + } + let query = input ? input.trim() : ""; + const attachment = this.type === "classic" ? this.message.attachments.first() : null; + if (query.startsWith("||") && query.endsWith("||")) { + query = query.substring(2, query.length - 2); + } + if (query.startsWith("<") && query.endsWith(">")) { + query = query.substring(1, query.length - 1); + } + try { + const url = new URL(query); + return play(this.client, url, { channel: this.channel, member: this.member, type: this.type, interaction: this.interaction }, true); + } catch { + const search = prefixes.some(v => query.startsWith(v)) ? query : !query && attachment ? attachment.url : (process.env.YT_DISABLED !== "true" ? `ytsearch:${query}` : `dzsearch:${query}`); + return play(this.client, search, { channel: this.channel, member: this.member, type: this.type, interaction: this.interaction }, true); + } + } + + static flags = [{ + name: "query", + type: 3, + description: "An audio search query or URL", + required: true + }]; + static description = "Plays a song or adds it to the queue"; + static aliases = ["p"]; + static arguments = ["[url]"]; +} + +export default PlayCommand; diff --git a/commands/music/queue.js b/commands/music/queue.js index 321aefc..e42de7f 100644 --- a/commands/music/queue.js +++ b/commands/music/queue.js @@ -1,65 +1,65 @@ -import { request } from "undici"; -import format from "format-duration"; -import { nodes } from "../../utils/soundplayer.js"; -import paginator from "../../utils/pagination/pagination.js"; -import MusicCommand from "../../classes/musicCommand.js"; - -class QueueCommand extends MusicCommand { - async run() { - this.success = false; - if (!this.guild) return "This command only works in servers!"; - if (!this.member.voiceState) return "You need to be in a voice channel first!"; - if (!this.guild.voiceStates.has(this.client.user.id)) return "I'm not in a voice channel!"; - if (!this.channel.permissionsOf(this.client.user.id.toString()).has("EMBED_LINKS")) return "I don't have the `Embed Links` permission!"; - const player = this.connection; - if (!player) return "I haven't completely connected yet!"; - const node = nodes.filter((val) => val.name === player.player.node.name)[0]; - const tracks = await request(`http://${node.url}/decodetracks`, { method: "POST", body: JSON.stringify(this.queue), headers: { authorization: node.auth, "content-type": "application/json" } }).then(res => res.body.json()); - const trackList = []; - const firstTrack = tracks.shift(); - for (const [i, track] of tracks.entries()) { - trackList.push(`${i + 1}. ${track.info.author !== "" ? track.info.author : "(blank)"} - **${track.info.title !== "" ? track.info.title : "(blank)"}** (${track.info.isStream ? "∞" : format(track.info.length)})`); - } - const pageSize = 5; - const embeds = []; - const groups = trackList.map((item, index) => { - return index % pageSize === 0 ? trackList.slice(index, index + pageSize) : null; - }).filter(Boolean); - if (groups.length === 0) groups.push("del"); - for (const [i, value] of groups.entries()) { - embeds.push({ - embeds: [{ - author: { - name: "Queue", - iconURL: this.client.user.avatarURL() - }, - color: 16711680, - footer: { - text: `Page ${i + 1} of ${groups.length}` - }, - fields: [{ - name: "đŸŽļ Now Playing", - value: `${firstTrack.info.author !== "" ? firstTrack.info.author : "(blank)"} - **${firstTrack.info.title !== "" ? firstTrack.info.title : "(blank)"}** (${firstTrack.info.isStream ? "∞" : format(firstTrack.info.length)})` - }, { - name: "🔁 Looping?", - value: player.loop ? "Yes" : "No" - }, { - name: "🌐 Node", - value: player.player.node ? player.player.node.name : "Unknown" - }, { - name: "🗒ī¸ Queue", - value: value !== "del" ? value.join("\n") : "There's nothing in the queue!" - }] - }] - }); - } - if (embeds.length === 0) return "There's nothing in the queue!"; - this.success = true; - return paginator(this.client, { type: this.type, message: this.message, interaction: this.interaction, channel: this.channel, author: this.author }, embeds); - } - - static description = "Shows the current queue"; - static aliases = ["q"]; -} - +import { request } from "undici"; +import format from "format-duration"; +import { nodes } from "../../utils/soundplayer.js"; +import paginator from "../../utils/pagination/pagination.js"; +import MusicCommand from "../../classes/musicCommand.js"; + +class QueueCommand extends MusicCommand { + async run() { + this.success = false; + if (!this.guild) return "This command only works in servers!"; + if (!this.member.voiceState) return "You need to be in a voice channel first!"; + if (!this.guild.voiceStates.has(this.client.user.id)) return "I'm not in a voice channel!"; + if (!this.channel.permissionsOf(this.client.user.id.toString()).has("EMBED_LINKS")) return "I don't have the `Embed Links` permission!"; + const player = this.connection; + if (!player) return "I haven't completely connected yet!"; + const node = nodes.filter((val) => val.name === player.player.node.name)[0]; + const tracks = await request(`http://${node.url}/decodetracks`, { method: "POST", body: JSON.stringify(this.queue), headers: { authorization: node.auth, "content-type": "application/json" } }).then(res => res.body.json()); + const trackList = []; + const firstTrack = tracks.shift(); + for (const [i, track] of tracks.entries()) { + trackList.push(`${i + 1}. ${track.info.author !== "" ? track.info.author : "(blank)"} - **${track.info.title !== "" ? track.info.title : "(blank)"}** (${track.info.isStream ? "∞" : format(track.info.length)})`); + } + const pageSize = 5; + const embeds = []; + const groups = trackList.map((item, index) => { + return index % pageSize === 0 ? trackList.slice(index, index + pageSize) : null; + }).filter(Boolean); + if (groups.length === 0) groups.push("del"); + for (const [i, value] of groups.entries()) { + embeds.push({ + embeds: [{ + author: { + name: "Queue", + iconURL: this.client.user.avatarURL() + }, + color: 16711680, + footer: { + text: `Page ${i + 1} of ${groups.length}` + }, + fields: [{ + name: "đŸŽļ Now Playing", + value: `${firstTrack.info.author !== "" ? firstTrack.info.author : "(blank)"} - **${firstTrack.info.title !== "" ? firstTrack.info.title : "(blank)"}** (${firstTrack.info.isStream ? "∞" : format(firstTrack.info.length)})` + }, { + name: "🔁 Looping?", + value: player.loop ? "Yes" : "No" + }, { + name: "🌐 Node", + value: player.player.node ? player.player.node.name : "Unknown" + }, { + name: "🗒ī¸ Queue", + value: value !== "del" ? value.join("\n") : "There's nothing in the queue!" + }] + }] + }); + } + if (embeds.length === 0) return "There's nothing in the queue!"; + this.success = true; + return paginator(this.client, { type: this.type, message: this.message, interaction: this.interaction, channel: this.channel, author: this.author }, embeds); + } + + static description = "Shows the current queue"; + static aliases = ["q"]; +} + export default QueueCommand; \ No newline at end of file diff --git a/commands/music/remove.js b/commands/music/remove.js index 1140651..c381b21 100644 --- a/commands/music/remove.js +++ b/commands/music/remove.js @@ -1,33 +1,33 @@ -import { queues } from "../../utils/soundplayer.js"; -import MusicCommand from "../../classes/musicCommand.js"; - -class RemoveCommand extends MusicCommand { - async run() { - this.success = false; - if (!this.guild) return "This command only works in servers!"; - if (!this.member.voiceState) return "You need to be in a voice channel first!"; - if (!this.guild.voiceStates.has(this.client.user.id)) return "I'm not in a voice channel!"; - if (!this.connection) return "I haven't completely connected yet!"; - if (this.connection.host !== this.author.id && !process.env.OWNER.split(",").includes(this.connection.host)) return "Only the current voice session host can remove songs from the queue!"; - const pos = parseInt(this.options.position ?? this.args[0]); - if (isNaN(pos) || pos > this.queue.length || pos < 1) return "That's not a valid position!"; - const removed = this.queue.splice(pos, 1); - if (removed.length === 0) return "That's not a valid position!"; - const track = await this.connection.player.node.rest.decode(removed[0]); - queues.set(this.guildID, this.queue); - this.success = true; - return `🔊 The song \`${track.title ? track.title : "(blank)"}\` has been removed from the queue.`; - } - - static flags = [{ - name: "position", - type: 4, - description: "The queue position you want to remove", - min_value: 1, - required: true - }]; - static description = "Removes a song from the queue"; - static aliases = ["rm"]; -} - -export default RemoveCommand; +import { queues } from "../../utils/soundplayer.js"; +import MusicCommand from "../../classes/musicCommand.js"; + +class RemoveCommand extends MusicCommand { + async run() { + this.success = false; + if (!this.guild) return "This command only works in servers!"; + if (!this.member.voiceState) return "You need to be in a voice channel first!"; + if (!this.guild.voiceStates.has(this.client.user.id)) return "I'm not in a voice channel!"; + if (!this.connection) return "I haven't completely connected yet!"; + if (this.connection.host !== this.author.id && !process.env.OWNER.split(",").includes(this.connection.host)) return "Only the current voice session host can remove songs from the queue!"; + const pos = parseInt(this.options.position ?? this.args[0]); + if (isNaN(pos) || pos > this.queue.length || pos < 1) return "That's not a valid position!"; + const removed = this.queue.splice(pos, 1); + if (removed.length === 0) return "That's not a valid position!"; + const track = await this.connection.player.node.rest.decode(removed[0]); + queues.set(this.guildID, this.queue); + this.success = true; + return `🔊 The song \`${track.title ? track.title : "(blank)"}\` has been removed from the queue.`; + } + + static flags = [{ + name: "position", + type: 4, + description: "The queue position you want to remove", + min_value: 1, + required: true + }]; + static description = "Removes a song from the queue"; + static aliases = ["rm"]; +} + +export default RemoveCommand; diff --git a/commands/music/seek.js b/commands/music/seek.js index 89b64e4..6972e98 100644 --- a/commands/music/seek.js +++ b/commands/music/seek.js @@ -1,38 +1,38 @@ -import MusicCommand from "../../classes/musicCommand.js"; - -class SeekCommand extends MusicCommand { - async run() { - this.success = false; - if (!this.guild) return "This command only works in servers!"; - if (!this.member.voiceState) return "You need to be in a voice channel first!"; - if (!this.guild.voiceStates.has(this.client.user.id)) return "I'm not in a voice channel!"; - if (!this.connection) return "I haven't completely connected yet!"; - if (this.connection.host !== this.author.id) return "Only the current voice session host can seek the music!"; - const player = this.connection.player; - const track = await player.node.rest.decode(player.track); - if (!track.isSeekable) return "This track isn't seekable!"; - const pos = this.options.position ?? this.args[0]; - let seconds; - if (typeof pos === "string" && pos.includes(":")) { - seconds = +(pos.split(":").reduce((acc, time) => (60 * acc) + +time)); - } else { - seconds = parseFloat(pos); - } - if (isNaN(seconds) || (seconds * 1000) > track.length || (seconds * 1000) < 0) return "That's not a valid position!"; - player.seekTo(seconds * 1000); - this.success = true; - return `🔊 Seeked track to ${seconds} second(s).`; - } - - static flags = [{ - name: "position", - type: 3, - description: "Seek to this position", - required: true - }]; - static description = "Seeks to a different position in the music"; - static aliases = ["pos"]; - static arguments = ["[seconds]"]; -} - -export default SeekCommand; +import MusicCommand from "../../classes/musicCommand.js"; + +class SeekCommand extends MusicCommand { + async run() { + this.success = false; + if (!this.guild) return "This command only works in servers!"; + if (!this.member.voiceState) return "You need to be in a voice channel first!"; + if (!this.guild.voiceStates.has(this.client.user.id)) return "I'm not in a voice channel!"; + if (!this.connection) return "I haven't completely connected yet!"; + if (this.connection.host !== this.author.id) return "Only the current voice session host can seek the music!"; + const player = this.connection.player; + const track = await player.node.rest.decode(player.track); + if (!track.isSeekable) return "This track isn't seekable!"; + const pos = this.options.position ?? this.args[0]; + let seconds; + if (typeof pos === "string" && pos.includes(":")) { + seconds = +(pos.split(":").reduce((acc, time) => (60 * acc) + +time)); + } else { + seconds = parseFloat(pos); + } + if (isNaN(seconds) || (seconds * 1000) > track.length || (seconds * 1000) < 0) return "That's not a valid position!"; + player.seekTo(seconds * 1000); + this.success = true; + return `🔊 Seeked track to ${seconds} second(s).`; + } + + static flags = [{ + name: "position", + type: 3, + description: "Seek to this position", + required: true + }]; + static description = "Seeks to a different position in the music"; + static aliases = ["pos"]; + static arguments = ["[seconds]"]; +} + +export default SeekCommand; diff --git a/commands/music/shuffle.js b/commands/music/shuffle.js index 661c54b..bc02a0a 100644 --- a/commands/music/shuffle.js +++ b/commands/music/shuffle.js @@ -1,23 +1,23 @@ -import { players } from "../../utils/soundplayer.js"; -import MusicCommand from "../../classes/musicCommand.js"; - -class ShuffleCommand extends MusicCommand { - async run() { - this.success = false; - if (!this.guild) return "This command only works in servers!"; - if (!this.member.voiceState) return "You need to be in a voice channel first!"; - if (!this.guild.voiceStates.has(this.client.user.id)) return "I'm not in a voice channel!"; - if (!this.connection) return "I haven't completely connected yet!"; - if (this.connection.host !== this.author.id) return "Only the current voice session host can shuffle the music!"; - const object = this.connection; - object.shuffle = !object.shuffle; - players.set(this.guildID, object); - this.success = true; - return object.shuffle ? "🔊 The player is now shuffling." : "🔊 The player is no longer shuffling."; - } - - static description = "Shuffles the music"; - static aliases = ["toggleshuffle"]; -} - +import { players } from "../../utils/soundplayer.js"; +import MusicCommand from "../../classes/musicCommand.js"; + +class ShuffleCommand extends MusicCommand { + async run() { + this.success = false; + if (!this.guild) return "This command only works in servers!"; + if (!this.member.voiceState) return "You need to be in a voice channel first!"; + if (!this.guild.voiceStates.has(this.client.user.id)) return "I'm not in a voice channel!"; + if (!this.connection) return "I haven't completely connected yet!"; + if (this.connection.host !== this.author.id) return "Only the current voice session host can shuffle the music!"; + const object = this.connection; + object.shuffle = !object.shuffle; + players.set(this.guildID, object); + this.success = true; + return object.shuffle ? "🔊 The player is now shuffling." : "🔊 The player is no longer shuffling."; + } + + static description = "Shuffles the music"; + static aliases = ["toggleshuffle"]; +} + export default ShuffleCommand; \ No newline at end of file diff --git a/commands/music/skip.js b/commands/music/skip.js index a5e8be0..00ee076 100644 --- a/commands/music/skip.js +++ b/commands/music/skip.js @@ -1,41 +1,41 @@ -import { skipVotes } from "../../utils/soundplayer.js"; -import MusicCommand from "../../classes/musicCommand.js"; - -class SkipCommand extends MusicCommand { - async run() { - this.success = false; - if (!this.guild) return "This command only works in servers!"; - if (!this.member.voiceState) return "You need to be in a voice channel first!"; - if (!this.guild.voiceStates.has(this.client.user.id)) return "I'm not in a voice channel!"; - const player = this.connection; - if (!player) return "I haven't completely connected yet!"; - if (player.host !== this.author.id && !this.member.permissions.has("MANAGE_CHANNELS")) { - const votes = skipVotes.get(this.guild.id) ?? { count: 0, ids: [], max: Math.min(3, player.voiceChannel.voiceMembers.filter((i) => i.id !== this.client.user.id && !i.bot).length) }; - if (votes.ids.includes(this.author.id)) return "You've already voted to skip!"; - const newObject = { - count: votes.count + 1, - ids: [...votes.ids, this.author.id].filter(item => !!item), - max: votes.max - }; - if (votes.count + 1 === votes.max) { - await player.player.stopTrack(this.guild.id); - skipVotes.set(this.guild.id, { count: 0, ids: [], max: Math.min(3, player.voiceChannel.voiceMembers.filter((i) => i.id !== this.client.user.id && !i.bot).length) }); - this.success = true; - if (this.type === "application") return "🔊 The current song has been skipped."; - } else { - skipVotes.set(this.guild.id, newObject); - this.success = true; - return `🔊 Voted to skip song (${votes.count + 1}/${votes.max} people have voted).`; - } - } else { - await player.player.stopTrack(); - this.success = true; - if (this.type === "application") return "🔊 The current song has been skipped."; - } - } - - static description = "Skips the current song"; - static aliases = ["forceskip", "s"]; -} - -export default SkipCommand; +import { skipVotes } from "../../utils/soundplayer.js"; +import MusicCommand from "../../classes/musicCommand.js"; + +class SkipCommand extends MusicCommand { + async run() { + this.success = false; + if (!this.guild) return "This command only works in servers!"; + if (!this.member.voiceState) return "You need to be in a voice channel first!"; + if (!this.guild.voiceStates.has(this.client.user.id)) return "I'm not in a voice channel!"; + const player = this.connection; + if (!player) return "I haven't completely connected yet!"; + if (player.host !== this.author.id && !this.member.permissions.has("MANAGE_CHANNELS")) { + const votes = skipVotes.get(this.guild.id) ?? { count: 0, ids: [], max: Math.min(3, player.voiceChannel.voiceMembers.filter((i) => i.id !== this.client.user.id && !i.bot).length) }; + if (votes.ids.includes(this.author.id)) return "You've already voted to skip!"; + const newObject = { + count: votes.count + 1, + ids: [...votes.ids, this.author.id].filter(item => !!item), + max: votes.max + }; + if (votes.count + 1 === votes.max) { + await player.player.stopTrack(this.guild.id); + skipVotes.set(this.guild.id, { count: 0, ids: [], max: Math.min(3, player.voiceChannel.voiceMembers.filter((i) => i.id !== this.client.user.id && !i.bot).length) }); + this.success = true; + if (this.type === "application") return "🔊 The current song has been skipped."; + } else { + skipVotes.set(this.guild.id, newObject); + this.success = true; + return `🔊 Voted to skip song (${votes.count + 1}/${votes.max} people have voted).`; + } + } else { + await player.player.stopTrack(); + this.success = true; + if (this.type === "application") return "🔊 The current song has been skipped."; + } + } + + static description = "Skips the current song"; + static aliases = ["forceskip", "s"]; +} + +export default SkipCommand; diff --git a/commands/music/stop.js b/commands/music/stop.js index 98e5f81..125dcd4 100644 --- a/commands/music/stop.js +++ b/commands/music/stop.js @@ -1,28 +1,28 @@ -import { manager, players, queues } from "../../utils/soundplayer.js"; -import MusicCommand from "../../classes/musicCommand.js"; - -class StopCommand extends MusicCommand { - async run() { - this.success = false; - if (!this.guild) return "This command only works in servers!"; - if (!this.member.voiceState) return "You need to be in a voice channel first!"; - if (!this.guild.voiceStates.has(this.client.user.id)) return "I'm not in a voice channel!"; - if (!this.connection) { - await manager.getNode().leaveChannel(this.guild.id); - this.success = true; - return "🔊 The current voice channel session has ended."; - } - if (this.connection.host !== this.author.id && !this.member.permissions.has("MANAGE_CHANNELS")) return "Only the current voice session host can stop the music!"; - const connection = this.connection.player; - connection.node.leaveChannel(this.guild.id); - players.delete(this.guild.id); - queues.delete(this.guild.id); - this.success = true; - return `🔊 The voice channel session in \`${this.connection.voiceChannel.name}\` has ended.`; - } - - static description = "Stops the music"; - static aliases = ["disconnect"]; -} - -export default StopCommand; +import { manager, players, queues } from "../../utils/soundplayer.js"; +import MusicCommand from "../../classes/musicCommand.js"; + +class StopCommand extends MusicCommand { + async run() { + this.success = false; + if (!this.guild) return "This command only works in servers!"; + if (!this.member.voiceState) return "You need to be in a voice channel first!"; + if (!this.guild.voiceStates.has(this.client.user.id)) return "I'm not in a voice channel!"; + if (!this.connection) { + await manager.getNode().leaveChannel(this.guild.id); + this.success = true; + return "🔊 The current voice channel session has ended."; + } + if (this.connection.host !== this.author.id && !this.member.permissions.has("MANAGE_CHANNELS")) return "Only the current voice session host can stop the music!"; + const connection = this.connection.player; + connection.node.leaveChannel(this.guild.id); + players.delete(this.guild.id); + queues.delete(this.guild.id); + this.success = true; + return `🔊 The voice channel session in \`${this.connection.voiceChannel.name}\` has ended.`; + } + + static description = "Stops the music"; + static aliases = ["disconnect"]; +} + +export default StopCommand; diff --git a/commands/music/toggle.js b/commands/music/toggle.js index 2949f8e..fd633f3 100644 --- a/commands/music/toggle.js +++ b/commands/music/toggle.js @@ -1,21 +1,21 @@ -import MusicCommand from "../../classes/musicCommand.js"; - -class ToggleCommand extends MusicCommand { - async run() { - this.success = false; - if (!this.guild) return "This command only works in servers!"; - if (!this.member.voiceState) return "You need to be in a voice channel first!"; - if (!this.guild.voiceStates.has(this.client.user.id)) return "I'm not in a voice channel!"; - if (!this.connection) return "I haven't completely connected yet!"; - if (this.connection.host !== this.author.id && !this.member.permissions.has("MANAGE_CHANNELS")) return "Only the current voice session host can pause/resume the music!"; - const player = this.connection.player; - player.setPaused(!player.paused ? true : false); - this.success = true; - return `🔊 The player has been ${player.paused ? "paused" : "resumed"}.`; - } - - static description = "Pauses/resumes the current song"; - static aliases = ["pause", "resume"]; -} - -export default ToggleCommand; +import MusicCommand from "../../classes/musicCommand.js"; + +class ToggleCommand extends MusicCommand { + async run() { + this.success = false; + if (!this.guild) return "This command only works in servers!"; + if (!this.member.voiceState) return "You need to be in a voice channel first!"; + if (!this.guild.voiceStates.has(this.client.user.id)) return "I'm not in a voice channel!"; + if (!this.connection) return "I haven't completely connected yet!"; + if (this.connection.host !== this.author.id && !this.member.permissions.has("MANAGE_CHANNELS")) return "Only the current voice session host can pause/resume the music!"; + const player = this.connection.player; + player.setPaused(!player.paused ? true : false); + this.success = true; + return `🔊 The player has been ${player.paused ? "paused" : "resumed"}.`; + } + + static description = "Pauses/resumes the current song"; + static aliases = ["pause", "resume"]; +} + +export default ToggleCommand; diff --git a/commands/soundboard/boi.js b/commands/soundboard/boi.js index 4ba0f90..7318b76 100644 --- a/commands/soundboard/boi.js +++ b/commands/soundboard/boi.js @@ -1,9 +1,9 @@ -import SoundboardCommand from "../../classes/soundboardCommand.js"; - -class BoiCommand extends SoundboardCommand { - static file = "./assets/audio/boi.ogg"; - static description = "Plays the \"boi\" sound effect"; - static aliases = ["boy", "neutron", "hugh"]; -} - +import SoundboardCommand from "../../classes/soundboardCommand.js"; + +class BoiCommand extends SoundboardCommand { + static file = "./assets/audio/boi.ogg"; + static description = "Plays the \"boi\" sound effect"; + static aliases = ["boy", "neutron", "hugh"]; +} + export default BoiCommand; \ No newline at end of file diff --git a/commands/soundboard/boom.js b/commands/soundboard/boom.js index 76273dc..6c08421 100644 --- a/commands/soundboard/boom.js +++ b/commands/soundboard/boom.js @@ -1,9 +1,9 @@ -import SoundboardCommand from "../../classes/soundboardCommand.js"; - -class BoomCommand extends SoundboardCommand { - static file = "./assets/audio/boom.ogg"; - static description = "Plays the Vine boom sound effect"; - static aliases = ["thud", "vine"]; -} - +import SoundboardCommand from "../../classes/soundboardCommand.js"; + +class BoomCommand extends SoundboardCommand { + static file = "./assets/audio/boom.ogg"; + static description = "Plays the Vine boom sound effect"; + static aliases = ["thud", "vine"]; +} + export default BoomCommand; \ No newline at end of file diff --git a/commands/soundboard/bruh.js b/commands/soundboard/bruh.js index 06e019b..65de7e7 100644 --- a/commands/soundboard/bruh.js +++ b/commands/soundboard/bruh.js @@ -1,9 +1,9 @@ -import SoundboardCommand from "../../classes/soundboardCommand.js"; - -class BruhCommand extends SoundboardCommand { - static file = "./assets/audio/bruh.ogg"; - static description = "Plays the \"bruh\" sound effect"; - static aliases = ["bro"]; -} - +import SoundboardCommand from "../../classes/soundboardCommand.js"; + +class BruhCommand extends SoundboardCommand { + static file = "./assets/audio/bruh.ogg"; + static description = "Plays the \"bruh\" sound effect"; + static aliases = ["bro"]; +} + export default BruhCommand; \ No newline at end of file diff --git a/commands/soundboard/damndaniel.js b/commands/soundboard/damndaniel.js index 049b0e7..8fcd151 100644 --- a/commands/soundboard/damndaniel.js +++ b/commands/soundboard/damndaniel.js @@ -1,9 +1,9 @@ -import SoundboardCommand from "../../classes/soundboardCommand.js"; - -class DamnDanielCommand extends SoundboardCommand { - static file = "./assets/audio/damndaniel.ogg"; - static description = "Plays the \"damn daniel\" sound effect"; - static aliases = ["daniel", "damn"]; -} - -export default DamnDanielCommand; +import SoundboardCommand from "../../classes/soundboardCommand.js"; + +class DamnDanielCommand extends SoundboardCommand { + static file = "./assets/audio/damndaniel.ogg"; + static description = "Plays the \"damn daniel\" sound effect"; + static aliases = ["daniel", "damn"]; +} + +export default DamnDanielCommand; diff --git a/commands/soundboard/explosion.js b/commands/soundboard/explosion.js index e9dd261..f2790f1 100644 --- a/commands/soundboard/explosion.js +++ b/commands/soundboard/explosion.js @@ -1,8 +1,8 @@ -import SoundboardCommand from "../../classes/soundboardCommand.js"; - -class ExplosionCommand extends SoundboardCommand { - static file = "./assets/audio/explosion.ogg"; - static description = "Plays an explosion sound effect"; -} - +import SoundboardCommand from "../../classes/soundboardCommand.js"; + +class ExplosionCommand extends SoundboardCommand { + static file = "./assets/audio/explosion.ogg"; + static description = "Plays an explosion sound effect"; +} + export default ExplosionCommand; \ No newline at end of file diff --git a/commands/soundboard/fakeping.js b/commands/soundboard/fakeping.js index 669e612..73d9907 100644 --- a/commands/soundboard/fakeping.js +++ b/commands/soundboard/fakeping.js @@ -1,9 +1,9 @@ -import SoundboardCommand from "../../classes/soundboardCommand.js"; - -class FakePingCommand extends SoundboardCommand { - static file = "./assets/audio/ping.ogg"; - static description = "Plays a Discord ping sound effect"; - static aliases = ["notification", "notif"]; -} - +import SoundboardCommand from "../../classes/soundboardCommand.js"; + +class FakePingCommand extends SoundboardCommand { + static file = "./assets/audio/ping.ogg"; + static description = "Plays a Discord ping sound effect"; + static aliases = ["notification", "notif"]; +} + export default FakePingCommand; \ No newline at end of file diff --git a/commands/soundboard/fart.js b/commands/soundboard/fart.js index 8f75a91..551ab7a 100644 --- a/commands/soundboard/fart.js +++ b/commands/soundboard/fart.js @@ -1,9 +1,9 @@ -import SoundboardCommand from "../../classes/soundboardCommand.js"; - -class FartCommand extends SoundboardCommand { - static file = "./assets/audio/fart.ogg"; - static description = "Plays a fart sound effect"; - static aliases = ["toot"]; -} - +import SoundboardCommand from "../../classes/soundboardCommand.js"; + +class FartCommand extends SoundboardCommand { + static file = "./assets/audio/fart.ogg"; + static description = "Plays a fart sound effect"; + static aliases = ["toot"]; +} + export default FartCommand; \ No newline at end of file diff --git a/commands/soundboard/fartreverb.js b/commands/soundboard/fartreverb.js index 2c470f0..75fe1d2 100644 --- a/commands/soundboard/fartreverb.js +++ b/commands/soundboard/fartreverb.js @@ -1,9 +1,9 @@ -import SoundboardCommand from "../../classes/soundboardCommand.js"; - -class FartReverbCommand extends SoundboardCommand { - static file = "./assets/audio/fart2.ogg"; - static description = "Plays a fart sound effect with extra reverb"; - static aliases = ["fart2"]; -} - +import SoundboardCommand from "../../classes/soundboardCommand.js"; + +class FartReverbCommand extends SoundboardCommand { + static file = "./assets/audio/fart2.ogg"; + static description = "Plays a fart sound effect with extra reverb"; + static aliases = ["fart2"]; +} + export default FartReverbCommand; \ No newline at end of file diff --git a/commands/soundboard/fbi.js b/commands/soundboard/fbi.js index eb1a7f4..430c1ae 100644 --- a/commands/soundboard/fbi.js +++ b/commands/soundboard/fbi.js @@ -1,9 +1,9 @@ -import SoundboardCommand from "../../classes/soundboardCommand.js"; - -class FBICommand extends SoundboardCommand { - static file = "./assets/audio/fbi.ogg"; - static description = "Plays the \"FBI OPEN UP\" sound effect"; - static aliases = ["openup"]; -} - +import SoundboardCommand from "../../classes/soundboardCommand.js"; + +class FBICommand extends SoundboardCommand { + static file = "./assets/audio/fbi.ogg"; + static description = "Plays the \"FBI OPEN UP\" sound effect"; + static aliases = ["openup"]; +} + export default FBICommand; \ No newline at end of file diff --git a/commands/soundboard/mail.js b/commands/soundboard/mail.js index f0ae1e4..2b2a253 100644 --- a/commands/soundboard/mail.js +++ b/commands/soundboard/mail.js @@ -1,9 +1,9 @@ -import SoundboardCommand from "../../classes/soundboardCommand.js"; - -class MailCommand extends SoundboardCommand { - static file = "./assets/audio/mail.ogg"; - static description = "Plays the \"You've got mail\" sound effect"; - static aliases = ["yougotmail", "youvegotmail", "aol"]; -} - +import SoundboardCommand from "../../classes/soundboardCommand.js"; + +class MailCommand extends SoundboardCommand { + static file = "./assets/audio/mail.ogg"; + static description = "Plays the \"You've got mail\" sound effect"; + static aliases = ["yougotmail", "youvegotmail", "aol"]; +} + export default MailCommand; \ No newline at end of file diff --git a/commands/soundboard/oof.js b/commands/soundboard/oof.js index f4e5b89..4d07a88 100644 --- a/commands/soundboard/oof.js +++ b/commands/soundboard/oof.js @@ -1,9 +1,9 @@ -import SoundboardCommand from "../../classes/soundboardCommand.js"; - -class OofCommand extends SoundboardCommand { - static file = "./assets/audio/oof.ogg"; - static description = "Plays the Roblox \"oof\" sound"; - static aliases = ["roblox", "commitdie"]; -} - +import SoundboardCommand from "../../classes/soundboardCommand.js"; + +class OofCommand extends SoundboardCommand { + static file = "./assets/audio/oof.ogg"; + static description = "Plays the Roblox \"oof\" sound"; + static aliases = ["roblox", "commitdie"]; +} + export default OofCommand; \ No newline at end of file diff --git a/commands/soundboard/soundboard.js b/commands/soundboard/soundboard.js index b7c6b6e..9358905 100644 --- a/commands/soundboard/soundboard.js +++ b/commands/soundboard/soundboard.js @@ -1,35 +1,35 @@ -import { play } from "../../utils/soundplayer.js"; -import Command from "../../classes/command.js"; -import { sounds, info } from "../../utils/collections.js"; - -// all-in-one soundboard command -class SoundboardAIOCommand extends Command { - async run() { - const soundName = this.type === "classic" ? this.args[0] : this.optionsArray[0].name; - if (!sounds.has(soundName)) { - this.success = false; - return "You need to provide a sound to play!"; - } - const name = sounds.get(soundName); - await this.acknowledge(); - return await play(this.client, name, { channel: this.channel, member: this.member, type: this.type, interaction: this.interaction }); - } - - static postInit() { - this.flags = []; - for (const sound of sounds.keys()) { - this.flags.push({ - name: sound, - type: 1, - description: info.get(sound).description - }); - } - return this; - } - - static description = "Plays a sound effect"; - static aliases = ["sound", "sb"]; - static directAllowed = false; -} - +import { play } from "../../utils/soundplayer.js"; +import Command from "../../classes/command.js"; +import { sounds, info } from "../../utils/collections.js"; + +// all-in-one soundboard command +class SoundboardAIOCommand extends Command { + async run() { + const soundName = this.type === "classic" ? this.args[0] : this.optionsArray[0].name; + if (!sounds.has(soundName)) { + this.success = false; + return "You need to provide a sound to play!"; + } + const name = sounds.get(soundName); + await this.acknowledge(); + return await play(this.client, name, { channel: this.channel, member: this.member, type: this.type, interaction: this.interaction }); + } + + static postInit() { + this.flags = []; + for (const sound of sounds.keys()) { + this.flags.push({ + name: sound, + type: 1, + description: info.get(sound).description + }); + } + return this; + } + + static description = "Plays a sound effect"; + static aliases = ["sound", "sb"]; + static directAllowed = false; +} + export default SoundboardAIOCommand; \ No newline at end of file diff --git a/commands/soundboard/winxp.js b/commands/soundboard/winxp.js index 56f4a81..4ed9b3d 100644 --- a/commands/soundboard/winxp.js +++ b/commands/soundboard/winxp.js @@ -1,9 +1,9 @@ -import SoundboardCommand from "../../classes/soundboardCommand.js"; - -class WinXPCommand extends SoundboardCommand { - static file = "./assets/audio/winxp.ogg"; - static description = "Plays the Windows XP startup sound"; - static aliases = ["windows", "xp"]; -} - +import SoundboardCommand from "../../classes/soundboardCommand.js"; + +class WinXPCommand extends SoundboardCommand { + static file = "./assets/audio/winxp.ogg"; + static description = "Plays the Windows XP startup sound"; + static aliases = ["windows", "xp"]; +} + export default WinXPCommand; \ No newline at end of file diff --git a/commands/tags/tags.js b/commands/tags/tags.js index 6d62ca4..3d3f87e 100644 --- a/commands/tags/tags.js +++ b/commands/tags/tags.js @@ -1,169 +1,169 @@ -import database from "../../utils/database.js"; -import paginator from "../../utils/pagination/pagination.js"; -import { random } from "../../utils/misc.js"; -import Command from "../../classes/command.js"; -const blacklist = ["create", "add", "edit", "remove", "delete", "list", "random", "own", "owner"]; - -class TagsCommand extends Command { - // todo: attempt to not make this file the worst thing that human eyes have ever seen - async run() { - this.success = false; - if (!this.guild) return "This command only works in servers!"; - const cmd = this.type === "classic" ? (this.args[0] ?? "").toLowerCase() : this.optionsArray[0].name; - if (!cmd || !cmd.trim()) return "You need to provide the name of the tag you want to view!"; - const tagName = this.type === "classic" ? this.args.slice(1)[0] : (this.optionsArray[0].options[0] ?? {}).value; - - if (cmd === "create" || cmd === "add") { - if (!tagName || !tagName.trim()) return "You need to provide the name of the tag you want to add!"; - if (blacklist.includes(tagName)) return "You can't make a tag with that name!"; - const getResult = await database.getTag(this.guild.id, tagName); - if (getResult) return "This tag already exists!"; - const result = await database.setTag(tagName, { content: this.type === "classic" ? this.args.slice(2).join(" ") : this.optionsArray[0].options[1].value, author: this.member.id }, this.guild); - this.success = true; - if (result) return result; - return `The tag \`${tagName}\` has been added!`; - } else if (cmd === "delete" || cmd === "remove") { - if (!tagName || !tagName.trim()) return "You need to provide the name of the tag you want to delete!"; - const getResult = await database.getTag(this.guild.id, tagName); - if (!getResult) return "This tag doesn't exist!"; - const owners = process.env.OWNER.split(","); - if (getResult.author !== this.author.id && !this.member.permissions.has("MANAGE_MESSAGES") && !owners.includes(this.author.id)) return "You don't own this tag!"; - await database.removeTag(tagName, this.guild); - this.success = true; - return `The tag \`${tagName}\` has been deleted!`; - } else if (cmd === "edit") { - if (!tagName || !tagName.trim()) return "You need to provide the name of the tag you want to edit!"; - const getResult = await database.getTag(this.guild.id, tagName); - if (!getResult) return "This tag doesn't exist!"; - const owners = process.env.OWNER.split(","); - if (getResult.author !== this.author.id && !this.member.permissions.has("MANAGE_MESSAGES") && !owners.includes(this.author.id)) return "You don't own this tag!"; - await database.editTag(tagName, { content: this.type === "classic" ? this.args.slice(2).join(" ") : this.optionsArray[0].options[1].value, author: this.member.id }, this.guild); - this.success = true; - return `The tag \`${tagName}\` has been edited!`; - } else if (cmd === "own" || cmd === "owner") { - if (!tagName || !tagName.trim()) return "You need to provide the name of the tag you want to check the owner of!"; - const getResult = await database.getTag(this.guild.id, tagName); - if (!getResult) return "This tag doesn't exist!"; - const user = this.client.users.get(getResult.author); - this.success = true; - if (!user) { - try { - const restUser = await this.client.rest.users.get(getResult.author); - return `This tag is owned by **${restUser.username}#${restUser.discriminator}** (\`${getResult.author}\`).`; - } catch { - return `I couldn't find exactly who owns this tag, but I was able to get their ID: \`${getResult.author}\``; - } - } else { - return `This tag is owned by **${user.username}#${user.discriminator}** (\`${getResult.author}\`).`; - } - } else if (cmd === "list") { - if (!this.channel.permissionsOf(this.client.user.id.toString()).has("EMBED_LINKS")) return "I don't have the `Embed Links` permission!"; - const tagList = await database.getTags(this.guild.id); - const embeds = []; - const groups = Object.keys(tagList).map((item, index) => { - return index % 15 === 0 ? Object.keys(tagList).slice(index, index + 15) : null; - }).filter((item) => { - return item; - }); - for (const [i, value] of groups.entries()) { - embeds.push({ - embeds: [{ - title: "Tag List", - color: 16711680, - footer: { - text: `Page ${i + 1} of ${groups.length}` - }, - description: value.join("\n"), - author: { - name: this.author.username, - iconURL: this.author.avatarURL() - } - }] - }); - } - if (embeds.length === 0) return "I couldn't find any tags!"; - this.success = true; - return paginator(this.client, { type: this.type, message: this.message, interaction: this.interaction, channel: this.channel, author: this.author }, embeds); - } else { - let getResult; - if (cmd === "random") { - const tagList = await database.getTags(this.guild.id); - getResult = tagList[random(Object.keys(tagList))]; - } else { - getResult = await database.getTag(this.guild.id, this.type === "classic" ? cmd : tagName); - } - if (!getResult) return "This tag doesn't exist!"; - this.success = true; - if (getResult.content.length > 2000) { - return { - embeds: [{ - color: 16711680, - description: getResult.content - }], - }; - } - return getResult.content; - } - } - - static description = "Manage tags"; - static aliases = ["t", "tag", "ta"]; - static arguments = { - default: ["[name]"], - add: ["[name]", "[content]"], - delete: ["[name]"], - edit: ["[name]", "[content]"], - owner: ["[name]"] - }; - - static subArgs = [{ - name: "name", - type: 3, - description: "The name of the tag", - required: true - }, { - name: "content", - type: 3, - description: "The content of the tag", - required: true - }]; - - static flags = [{ - name: "add", - type: 1, - description: "Adds a new tag", - options: this.subArgs - }, { - name: "delete", - type: 1, - description: "Deletes a tag", - options: [this.subArgs[0]] - }, { - name: "edit", - type: 1, - description: "Edits an existing tag", - options: this.subArgs - }, { - name: "get", - type: 1, - description: "Gets a tag", - options: [this.subArgs[0]] - }, { - name: "list", - type: 1, - description: "Lists every tag in this server" - }, { - name: "owner", - type: 1, - description: "Gets the owner of a tag", - options: [this.subArgs[0]] - }, { - name: "random", - type: 1, - description: "Gets a random tag" - }]; - static directAllowed = false; - static dbRequired = true; -} - -export default TagsCommand; +import database from "../../utils/database.js"; +import paginator from "../../utils/pagination/pagination.js"; +import { random } from "../../utils/misc.js"; +import Command from "../../classes/command.js"; +const blacklist = ["create", "add", "edit", "remove", "delete", "list", "random", "own", "owner"]; + +class TagsCommand extends Command { + // todo: attempt to not make this file the worst thing that human eyes have ever seen + async run() { + this.success = false; + if (!this.guild) return "This command only works in servers!"; + const cmd = this.type === "classic" ? (this.args[0] ?? "").toLowerCase() : this.optionsArray[0].name; + if (!cmd || !cmd.trim()) return "You need to provide the name of the tag you want to view!"; + const tagName = this.type === "classic" ? this.args.slice(1)[0] : (this.optionsArray[0].options[0] ?? {}).value; + + if (cmd === "create" || cmd === "add") { + if (!tagName || !tagName.trim()) return "You need to provide the name of the tag you want to add!"; + if (blacklist.includes(tagName)) return "You can't make a tag with that name!"; + const getResult = await database.getTag(this.guild.id, tagName); + if (getResult) return "This tag already exists!"; + const result = await database.setTag(tagName, { content: this.type === "classic" ? this.args.slice(2).join(" ") : this.optionsArray[0].options[1].value, author: this.member.id }, this.guild); + this.success = true; + if (result) return result; + return `The tag \`${tagName}\` has been added!`; + } else if (cmd === "delete" || cmd === "remove") { + if (!tagName || !tagName.trim()) return "You need to provide the name of the tag you want to delete!"; + const getResult = await database.getTag(this.guild.id, tagName); + if (!getResult) return "This tag doesn't exist!"; + const owners = process.env.OWNER.split(","); + if (getResult.author !== this.author.id && !this.member.permissions.has("MANAGE_MESSAGES") && !owners.includes(this.author.id)) return "You don't own this tag!"; + await database.removeTag(tagName, this.guild); + this.success = true; + return `The tag \`${tagName}\` has been deleted!`; + } else if (cmd === "edit") { + if (!tagName || !tagName.trim()) return "You need to provide the name of the tag you want to edit!"; + const getResult = await database.getTag(this.guild.id, tagName); + if (!getResult) return "This tag doesn't exist!"; + const owners = process.env.OWNER.split(","); + if (getResult.author !== this.author.id && !this.member.permissions.has("MANAGE_MESSAGES") && !owners.includes(this.author.id)) return "You don't own this tag!"; + await database.editTag(tagName, { content: this.type === "classic" ? this.args.slice(2).join(" ") : this.optionsArray[0].options[1].value, author: this.member.id }, this.guild); + this.success = true; + return `The tag \`${tagName}\` has been edited!`; + } else if (cmd === "own" || cmd === "owner") { + if (!tagName || !tagName.trim()) return "You need to provide the name of the tag you want to check the owner of!"; + const getResult = await database.getTag(this.guild.id, tagName); + if (!getResult) return "This tag doesn't exist!"; + const user = this.client.users.get(getResult.author); + this.success = true; + if (!user) { + try { + const restUser = await this.client.rest.users.get(getResult.author); + return `This tag is owned by **${restUser.username}#${restUser.discriminator}** (\`${getResult.author}\`).`; + } catch { + return `I couldn't find exactly who owns this tag, but I was able to get their ID: \`${getResult.author}\``; + } + } else { + return `This tag is owned by **${user.username}#${user.discriminator}** (\`${getResult.author}\`).`; + } + } else if (cmd === "list") { + if (!this.channel.permissionsOf(this.client.user.id.toString()).has("EMBED_LINKS")) return "I don't have the `Embed Links` permission!"; + const tagList = await database.getTags(this.guild.id); + const embeds = []; + const groups = Object.keys(tagList).map((item, index) => { + return index % 15 === 0 ? Object.keys(tagList).slice(index, index + 15) : null; + }).filter((item) => { + return item; + }); + for (const [i, value] of groups.entries()) { + embeds.push({ + embeds: [{ + title: "Tag List", + color: 16711680, + footer: { + text: `Page ${i + 1} of ${groups.length}` + }, + description: value.join("\n"), + author: { + name: this.author.username, + iconURL: this.author.avatarURL() + } + }] + }); + } + if (embeds.length === 0) return "I couldn't find any tags!"; + this.success = true; + return paginator(this.client, { type: this.type, message: this.message, interaction: this.interaction, channel: this.channel, author: this.author }, embeds); + } else { + let getResult; + if (cmd === "random") { + const tagList = await database.getTags(this.guild.id); + getResult = tagList[random(Object.keys(tagList))]; + } else { + getResult = await database.getTag(this.guild.id, this.type === "classic" ? cmd : tagName); + } + if (!getResult) return "This tag doesn't exist!"; + this.success = true; + if (getResult.content.length > 2000) { + return { + embeds: [{ + color: 16711680, + description: getResult.content + }], + }; + } + return getResult.content; + } + } + + static description = "Manage tags"; + static aliases = ["t", "tag", "ta"]; + static arguments = { + default: ["[name]"], + add: ["[name]", "[content]"], + delete: ["[name]"], + edit: ["[name]", "[content]"], + owner: ["[name]"] + }; + + static subArgs = [{ + name: "name", + type: 3, + description: "The name of the tag", + required: true + }, { + name: "content", + type: 3, + description: "The content of the tag", + required: true + }]; + + static flags = [{ + name: "add", + type: 1, + description: "Adds a new tag", + options: this.subArgs + }, { + name: "delete", + type: 1, + description: "Deletes a tag", + options: [this.subArgs[0]] + }, { + name: "edit", + type: 1, + description: "Edits an existing tag", + options: this.subArgs + }, { + name: "get", + type: 1, + description: "Gets a tag", + options: [this.subArgs[0]] + }, { + name: "list", + type: 1, + description: "Lists every tag in this server" + }, { + name: "owner", + type: 1, + description: "Gets the owner of a tag", + options: [this.subArgs[0]] + }, { + name: "random", + type: 1, + description: "Gets a random tag" + }]; + static directAllowed = false; + static dbRequired = true; +} + +export default TagsCommand; diff --git a/config/commands.json b/config/commands.json index 5294ada..c7bfe24 100644 --- a/config/commands.json +++ b/config/commands.json @@ -1,7 +1,7 @@ -{ - "types": { - "classic": true, - "application": true - }, - "blacklist": [] +{ + "types": { + "classic": true, + "application": false + }, + "blacklist": [] } \ No newline at end of file diff --git a/config/messages.json b/config/messages.json index 4c6f16f..4331d7b 100644 --- a/config/messages.json +++ b/config/messages.json @@ -1,202 +1,202 @@ -{ - "emotes": [ - "", - "", - "", - "", - "", - "" - ], - "messages": [ - "with your sanity", - "h", - "Club Penguin", - "Skype", - "with yo mama", - "with a bootleg plug-and-play", - "FL Studio: SoundCloud Rapper Edition", - "Fake Download Button Simulator", - "quick i need playing lines for my discord bot", - "a game", - "anime", - "absolutely nothing", - "Mozilla Firefox", - "Google Chrome", - "music bot now I guess", - "Fortnut", - "epic mashups bro", - "Netscape Navigator", - "Ubuntu", - "Linux", - "Hello Kitty Island Adventure", - "with the Infinity Gauntlet", - "BLJ Simulator", - "Jake Paul videos on repeat", - "gniyalP", - "HaaH WaaW", - "Shrek Extra Large", - "dQw4w9WgXcQ", - "Hong Kong 97", - "Reddit", - "Super Smash Bros. Ultimate", - "Ryujinx", - "Battletoads for Wii", - "MS Paint", - "Roblox", - "Minecraft", - "Desert Bus", - "Mega Man Legends 3", - "Sonic's Schoolhouse", - "Action 52", - "the funny memes epic", - "Nintendoâ„ĸ", - "Wario World", - "the Cat Piano", - "Pac-Man Championship Edition DX+", - "Pac-Man Championship Edition 2", - "Bottom Text", - "The Elder Scrolls 6", - "Skyrim", - "Game Boy Advance Video", - "Nintendo Switch Online", - "lol 7", - "Meme Run", - "Yoshi for the NES", - "Family Guy", - "Deltarune", - "subscribe to obama", - "yeah", - "semicolon", - "Super Mario Maker 2", - "jeff", - "TikTok", - "woo yeah", - "Mario", - "Microsoft Internet Explorer", - "joe mama", - "Sonic 06", - "Wii Speak Channel", - "Metal Gear Solid 4", - "iPod Music", - "Liquid Sun", - "with your Discord server", - "Scatman's World", - "with a stone, Luigi.", - "Doin' Your Mom by FAttY Spins", - "with a broken god", - "games with the mortals", - "#BringBackNationalSex", - "chiptune", - "foobar2000", - "XMPlay", - "OpenMPT", - "follow @esmBot@wetdry.world", - "with GIFs", - "check out the website at esmbot.net", - "the", - "you just lost the game", - "Yandere Simulator", - "PogChamp", - "yourself", - "sentience", - "with Brody Foxx", - "AOL Instant Messenger", - "beep boop", - "Animal Crossing: New Horizons", - "Hello, Gordon!", - "the j", - "Want esmBot to play some new games? Send suggestions!", - "MilkyTracker", - "with chimps", - "with the TF2 source code", - "alvin chipmunk nightcore", - "Troll", - "ay yo the pizza here", - "100 gecs", - "with limited resources", - "messages from 2018", - "&caption get real", - "Open Fortress", - "btw I use arch", - "Friday Night Funkin", - "fgsfds", - "Doilus Stage", - "please report any issues you find thank you", - "xfce real", - "OpenRCT2", - "Sus? As in the popular video game Among Us?", - "$19 Fortnite Card", - "Wild Woody", - "RDI Halcyon", - "KFC", - "Cave Story", - "YouTube ads", - "there are federal agents outside my house", - "WhatsApp", - "Half Life 3", - "Tower Defense Simulator", - "trans rights", - "balls", - "Vivaldi", - "Space Channel 5: Part 2", - "Vib Ribbon", - "[[Hyperlink Blocked]]", - "Steam Deck", - "Something big is coming.", - "This image has expired.", - "The GIF File Format", - "Scrimblo Bimblo", - "Babagee", - "Quilt", - "Minecraft Alpha 1.0.16.05", - "dame da ne", - "Fall Guys", - "with a box of wind", - "The Incredibles", - "AVIF", - "Sega Nomad", - "Item Asylum", - "TIC-80", - "Ghetto Smosh", - "Splatoon 3", - "changed", - "Chutes and Ladders", - "i'm gonna break my monitor i swear", - "The Simpsons: Hit and Run", - "Plants vs. Zombies", - "Neverball", - "joe biden 300 dollar fish", - "The Bron Jame", - "VMWare Workstation 16 Pro", - "outside", - "Sonic 3 and Knuckles", - "The Binding of Isaac", - "Lego Island", - "on Windows 1.00", - "Sonic Mania Plus", - "Rhythm Tengoku", - "with the ThinkPadâ„ĸ TrackPointâ„ĸ", - "Mort the Chicken", - "Yakuza 0", - "GregTech", - "a sick guitar riff", - "Visual Studio Code", - "Luigi's Mansion", - "Postal 2", - "Geometry Dash", - "FTL: Faster Than Light", - "Bad Apple", - "Ultrakill", - "with marbles", - "Pikmin 3", - "Trials Rising", - "Pizza Tower", - "ANTONBLAST", - "[object Object]", - "Xonotic", - "Lario", - "Hi-Fi Rush", - "Calckey", - "The clock is ticking." - ] -} +{ + "emotes": [ + "", + "", + "", + "", + "", + "" + ], + "messages": [ + "with your sanity", + "h", + "Club Penguin", + "Skype", + "with yo mama", + "with a bootleg plug-and-play", + "FL Studio: SoundCloud Rapper Edition", + "Fake Download Button Simulator", + "quick i need playing lines for my discord bot", + "a game", + "anime", + "absolutely nothing", + "Mozilla Firefox", + "Google Chrome", + "music bot now I guess", + "Fortnut", + "epic mashups bro", + "Netscape Navigator", + "Ubuntu", + "Linux", + "Hello Kitty Island Adventure", + "with the Infinity Gauntlet", + "BLJ Simulator", + "Jake Paul videos on repeat", + "gniyalP", + "HaaH WaaW", + "Shrek Extra Large", + "dQw4w9WgXcQ", + "Hong Kong 97", + "Reddit", + "Super Smash Bros. Ultimate", + "Ryujinx", + "Battletoads for Wii", + "MS Paint", + "Roblox", + "Minecraft", + "Desert Bus", + "Mega Man Legends 3", + "Sonic's Schoolhouse", + "Action 52", + "the funny memes epic", + "Nintendoâ„ĸ", + "Wario World", + "the Cat Piano", + "Pac-Man Championship Edition DX+", + "Pac-Man Championship Edition 2", + "Bottom Text", + "The Elder Scrolls 6", + "Skyrim", + "Game Boy Advance Video", + "Nintendo Switch Online", + "lol 7", + "Meme Run", + "Yoshi for the NES", + "Family Guy", + "Deltarune", + "subscribe to obama", + "yeah", + "semicolon", + "Super Mario Maker 2", + "jeff", + "TikTok", + "woo yeah", + "Mario", + "Microsoft Internet Explorer", + "joe mama", + "Sonic 06", + "Wii Speak Channel", + "Metal Gear Solid 4", + "iPod Music", + "Liquid Sun", + "with your Discord server", + "Scatman's World", + "with a stone, Luigi.", + "Doin' Your Mom by FAttY Spins", + "with a broken god", + "games with the mortals", + "#BringBackNationalSex", + "chiptune", + "foobar2000", + "XMPlay", + "OpenMPT", + "follow @esmBot@wetdry.world", + "with GIFs", + "check out the website at esmbot.net", + "the", + "you just lost the game", + "Yandere Simulator", + "PogChamp", + "yourself", + "sentience", + "with Brody Foxx", + "AOL Instant Messenger", + "beep boop", + "Animal Crossing: New Horizons", + "Hello, Gordon!", + "the j", + "Want esmBot to play some new games? Send suggestions!", + "MilkyTracker", + "with chimps", + "with the TF2 source code", + "alvin chipmunk nightcore", + "Troll", + "ay yo the pizza here", + "100 gecs", + "with limited resources", + "messages from 2018", + "&caption get real", + "Open Fortress", + "btw I use arch", + "Friday Night Funkin", + "fgsfds", + "Doilus Stage", + "please report any issues you find thank you", + "xfce real", + "OpenRCT2", + "Sus? As in the popular video game Among Us?", + "$19 Fortnite Card", + "Wild Woody", + "RDI Halcyon", + "KFC", + "Cave Story", + "YouTube ads", + "there are federal agents outside my house", + "WhatsApp", + "Half Life 3", + "Tower Defense Simulator", + "trans rights", + "balls", + "Vivaldi", + "Space Channel 5: Part 2", + "Vib Ribbon", + "[[Hyperlink Blocked]]", + "Steam Deck", + "Something big is coming.", + "This image has expired.", + "The GIF File Format", + "Scrimblo Bimblo", + "Babagee", + "Quilt", + "Minecraft Alpha 1.0.16.05", + "dame da ne", + "Fall Guys", + "with a box of wind", + "The Incredibles", + "AVIF", + "Sega Nomad", + "Item Asylum", + "TIC-80", + "Ghetto Smosh", + "Splatoon 3", + "changed", + "Chutes and Ladders", + "i'm gonna break my monitor i swear", + "The Simpsons: Hit and Run", + "Plants vs. Zombies", + "Neverball", + "joe biden 300 dollar fish", + "The Bron Jame", + "VMWare Workstation 16 Pro", + "outside", + "Sonic 3 and Knuckles", + "The Binding of Isaac", + "Lego Island", + "on Windows 1.00", + "Sonic Mania Plus", + "Rhythm Tengoku", + "with the ThinkPadâ„ĸ TrackPointâ„ĸ", + "Mort the Chicken", + "Yakuza 0", + "GregTech", + "a sick guitar riff", + "Visual Studio Code", + "Luigi's Mansion", + "Postal 2", + "Geometry Dash", + "FTL: Faster Than Light", + "Bad Apple", + "Ultrakill", + "with marbles", + "Pikmin 3", + "Trials Rising", + "Pizza Tower", + "ANTONBLAST", + "[object Object]", + "Xonotic", + "Lario", + "Hi-Fi Rush", + "Calckey", + "The clock is ticking." + ] +} diff --git a/config/servers.json b/config/servers.json index cada2f4..2e272e0 100644 --- a/config/servers.json +++ b/config/servers.json @@ -1,11 +1,11 @@ -{ - "lava": [ - { "name": "localhost", "url": "localhost:2333", "auth": "youshallnotpass", "local": true } - ], - "image": [ - { "server": "localhost", "auth": "verycoolpass100", "tls": false } - ], - "searx": [ - "https://searx.projectlounge.pw" - ] +{ + "lava": [ + { "name": "localhost", "url": "localhost:2333", "auth": "youshallnotpass", "local": true } + ], + "image": [ + { "server": "localhost", "auth": "verycoolpass100", "tls": false } + ], + "searx": [ + "https://searx.projectlounge.pw" + ] } \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml index 93fa2c8..4e47eb9 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,84 +1,84 @@ -version: '3.0' - -services: - bot: - build: - context: . - image: esmbot - restart: unless-stopped - volumes: - - ./logs:/home/esmBot/.internal/logs - - ./bot-help:/home/esmBot/help - - ./bot-temp:/home/esmBot/temp - - ./config:/home/esmBot/.internal/config - env_file: - - .env - environment: - OUTPUT: /home/esmBot/help/help.md - TEMPDIR: /home/esmBot/temp - user: root - links: - - lavalink - depends_on: - lavalink: - condition: service_healthy - postgres: - condition: service_started - - networks: - esmbot: - ipv4_address: 172.20.0.2 - - lavalink: - container_name: lavalink - image: fredboat/lavalink:dev - restart: unless-stopped - volumes: - - ./application.yml:/opt/Lavalink/application.yml - - ./assets:/opt/Lavalink/assets - healthcheck: - test: "curl -H \"Authorization: $$(cat /opt/Lavalink/application.yml | grep password: | sed 's/^ *password: //g' | tr -d '\"')\" -f http://localhost:2333/version" - interval: 10s - timeout: 10s - retries: 5 - start_period: 10s - networks: - esmbot: - ipv4_address: 172.20.0.3 - - postgres: - container_name: postgres - image: postgres:13-alpine - restart: unless-stopped - volumes: - - pg-data:/var/lib/postgresql/data - environment: - POSTGRES_PASSWORD: verycoolpass100 - POSTGRES_USER: esmbot - POSTGRES_DB: esmbot - networks: - esmbot: - ipv4_address: 172.20.0.4 - - adminer: - image: adminer - restart: unless-stopped - depends_on: - - postgres - ports: - - 8888:8080 - networks: - esmbot: - ipv4_address: 172.20.0.5 - -volumes: - bot-help: - bot-temp: - pg-data: - -networks: - esmbot: - driver: bridge - ipam: - config: +version: '3.0' + +services: + bot: + build: + context: . + image: esmbot + restart: unless-stopped + volumes: + - ./logs:/home/esmBot/.internal/logs + - ./bot-help:/home/esmBot/help + - ./bot-temp:/home/esmBot/temp + - ./config:/home/esmBot/.internal/config + env_file: + - .env + environment: + OUTPUT: /home/esmBot/help/help.md + TEMPDIR: /home/esmBot/temp + user: root + links: + - lavalink + depends_on: + lavalink: + condition: service_healthy + postgres: + condition: service_started + + networks: + esmbot: + ipv4_address: 172.20.0.2 + + lavalink: + container_name: lavalink + image: fredboat/lavalink:dev + restart: unless-stopped + volumes: + - ./application.yml:/opt/Lavalink/application.yml + - ./assets:/opt/Lavalink/assets + healthcheck: + test: "curl -H \"Authorization: $$(cat /opt/Lavalink/application.yml | grep password: | sed 's/^ *password: //g' | tr -d '\"')\" -f http://localhost:2333/version" + interval: 10s + timeout: 10s + retries: 5 + start_period: 10s + networks: + esmbot: + ipv4_address: 172.20.0.3 + + postgres: + container_name: postgres + image: postgres:13-alpine + restart: unless-stopped + volumes: + - pg-data:/var/lib/postgresql/data + environment: + POSTGRES_PASSWORD: verycoolpass100 + POSTGRES_USER: esmbot + POSTGRES_DB: esmbot + networks: + esmbot: + ipv4_address: 172.20.0.4 + + adminer: + image: adminer + restart: unless-stopped + depends_on: + - postgres + ports: + - 8888:8080 + networks: + esmbot: + ipv4_address: 172.20.0.5 + +volumes: + bot-help: + bot-temp: + pg-data: + +networks: + esmbot: + driver: bridge + ipam: + config: - subnet: 172.20.0.0/24 \ No newline at end of file diff --git a/docs/config.md b/docs/config.md index 061a9f4..107dbde 100644 --- a/docs/config.md +++ b/docs/config.md @@ -1,78 +1,78 @@ -# Config -esmBot uses a mix of environment variables and JSON for configuration. - -## Environment Variables (.env) -To make managing environment variables easier, an example `.env` file is included with the bot at `.env.example` and can be used to load the variables on startup. - -### Required -- `NODE_ENV`: Used for tuning the bot to different environments. If you don't know what to set it to, leave it as is. -- `TOKEN`: Your bot's token. You can find this [here](https://discord.com/developers/applications) under your application's Bot tab. -- `DB`: The database connection string. By default the `sqlite` and `postgresql` protocols are available, but this can be expanded by putting proper DB driver scripts into `utils/database/`. -- `OWNER`: Your Discord user ID. This is used for granting yourself access to certain management commands. Adding multiple users is supported by separating the IDs with a comma; however, this is not recommended for security purposes. -- `PREFIX`: The bot's default command prefix for classic commands. Note that servers can set their own individual prefixes via the `prefix` command. - -### Optional -These variables that are not necessarily required for the bot to run, but can greatly enhance its functionality: - -- `STAYVC`: Set this to true if you want the bot to stay in voice chat after playing music/a sound effect. You can make it leave by using the stop command. -- `TENOR`: An API token from [Tenor](https://tenor.com/gifapi). This is required for using GIFs from Tenor. -- `OUTPUT`: A directory to output the help documentation in Markdown format to. It's recommended to set this to a directory being served by a web server. -- `TEMPDIR`: A directory that will store generated images larger than 8MB. It's recommended to set this to a directory being served by a web server. -- `TMP_DOMAIN`: The root domain/directory that the images larger than 8MB are stored at. Example: `https://projectlounge.pw/tmp` -- `THRESHOLD`: A filesize threshold that the bot will start deleting old files in `TEMPDIR` at. -- `METRICS`: The HTTP port to serve [Prometheus](https://prometheus.io/)-compatible metrics on. -- `API_TYPE`: Set this to "none" if you want to process all images locally. Alternatively, set it to "ws" to use an image API server specified in the `image` block of `config/servers.json`. -- `ADMIN_SERVER`: A Discord server/guild ID to limit owner-only commands such as eval to. - -## JSON -The JSON-based configuration files are located in `config/`. - -### commands.json -```js -{ - "types": { - "classic": false, // Enable/disable "classic" (prefixed) commands, note that classic commands in direct messages will still work - "application": true // Enable/disable application commands (slash and context menu commands) - }, - "blacklist": [ - // Names of commands that you don't want the bot to load - ] -} -``` - -### messages.json -```js -{ - "emotes": [ - // Discord emote strings to use in the "Processing... this may take a while" messages, e.g. "" or "⚙ī¸" - ], - "messages": [ - // Strings to use in the bot's activity message/playing status - ] -} -``` - -### servers.json -```js -{ - "lava": [ // Objects containing info for connecting to Lavalink audio server(s) - { - "name": "test", // A human-friendly name for the server - "url": "localhost:2333", // IP address/domain name and port for the server - "auth": "youshallnotpass", // Password/authorization code for the server - "local": false // Whether or not the esmBot "assets" folder is located next to the Lavalink jar file - } - ], - "image": [ // Objects containing info for connecting to WS image server(s) - { - "server": "localhost", // IP address or domain name for the server - "auth": "verycoolpass100", // Password/authorization code for the server - "tls": false // Whether or not this is a secure TLS/wss connection - } - ], - "searx": [ - // URLs for Searx/SearXNG instances used for image/YouTube searches, e.g. "https://searx.projectlounge.pw" - // Note: instances must support getting results over JSON - ] -} -``` +# Config +esmBot uses a mix of environment variables and JSON for configuration. + +## Environment Variables (.env) +To make managing environment variables easier, an example `.env` file is included with the bot at `.env.example` and can be used to load the variables on startup. + +### Required +- `NODE_ENV`: Used for tuning the bot to different environments. If you don't know what to set it to, leave it as is. +- `TOKEN`: Your bot's token. You can find this [here](https://discord.com/developers/applications) under your application's Bot tab. +- `DB`: The database connection string. By default the `sqlite` and `postgresql` protocols are available, but this can be expanded by putting proper DB driver scripts into `utils/database/`. +- `OWNER`: Your Discord user ID. This is used for granting yourself access to certain management commands. Adding multiple users is supported by separating the IDs with a comma; however, this is not recommended for security purposes. +- `PREFIX`: The bot's default command prefix for classic commands. Note that servers can set their own individual prefixes via the `prefix` command. + +### Optional +These variables that are not necessarily required for the bot to run, but can greatly enhance its functionality: + +- `STAYVC`: Set this to true if you want the bot to stay in voice chat after playing music/a sound effect. You can make it leave by using the stop command. +- `TENOR`: An API token from [Tenor](https://tenor.com/gifapi). This is required for using GIFs from Tenor. +- `OUTPUT`: A directory to output the help documentation in Markdown format to. It's recommended to set this to a directory being served by a web server. +- `TEMPDIR`: A directory that will store generated images larger than 8MB. It's recommended to set this to a directory being served by a web server. +- `TMP_DOMAIN`: The root domain/directory that the images larger than 8MB are stored at. Example: `https://projectlounge.pw/tmp` +- `THRESHOLD`: A filesize threshold that the bot will start deleting old files in `TEMPDIR` at. +- `METRICS`: The HTTP port to serve [Prometheus](https://prometheus.io/)-compatible metrics on. +- `API_TYPE`: Set this to "none" if you want to process all images locally. Alternatively, set it to "ws" to use an image API server specified in the `image` block of `config/servers.json`. +- `ADMIN_SERVER`: A Discord server/guild ID to limit owner-only commands such as eval to. + +## JSON +The JSON-based configuration files are located in `config/`. + +### commands.json +```js +{ + "types": { + "classic": false, // Enable/disable "classic" (prefixed) commands, note that classic commands in direct messages will still work + "application": true // Enable/disable application commands (slash and context menu commands) + }, + "blacklist": [ + // Names of commands that you don't want the bot to load + ] +} +``` + +### messages.json +```js +{ + "emotes": [ + // Discord emote strings to use in the "Processing... this may take a while" messages, e.g. "" or "⚙ī¸" + ], + "messages": [ + // Strings to use in the bot's activity message/playing status + ] +} +``` + +### servers.json +```js +{ + "lava": [ // Objects containing info for connecting to Lavalink audio server(s) + { + "name": "test", // A human-friendly name for the server + "url": "localhost:2333", // IP address/domain name and port for the server + "auth": "youshallnotpass", // Password/authorization code for the server + "local": false // Whether or not the esmBot "assets" folder is located next to the Lavalink jar file + } + ], + "image": [ // Objects containing info for connecting to WS image server(s) + { + "server": "localhost", // IP address or domain name for the server + "auth": "verycoolpass100", // Password/authorization code for the server + "tls": false // Whether or not this is a secure TLS/wss connection + } + ], + "searx": [ + // URLs for Searx/SearXNG instances used for image/YouTube searches, e.g. "https://searx.projectlounge.pw" + // Note: instances must support getting results over JSON + ] +} +``` diff --git a/docs/custom-commands.md b/docs/custom-commands.md index 2cce3e9..6594c63 100644 --- a/docs/custom-commands.md +++ b/docs/custom-commands.md @@ -1,88 +1,88 @@ -# Custom Commands -esmBot has a powerful and flexible command handler, allowing you to create new commands and categories simply by creating new files. This page will provide a reference for creating new commands. - -## Directory Structure -The bot loads commands from subdirectories inside of the `commands` directory, which looks something like this by default: -``` -commands/ - - fun - > cat.js - > ... - - general - > help.js - > ping.js - > ... - - image-editing - > caption.js - > speed.js - > ... -``` -As you can see, each command is grouped into categories, which are represented by subdirectories. To create a new category, you can simply create a new directory inside of the `commands` directory, and to create a new command, you can create a new JS file under one of those subdirectories. - -!!! tip - The `message` category is special; commands in here act as right-click context menu message commands instead of "classic" or slash commands. - -## Command Structure -It's recommended to use the `Command` class located in `classes/command.js` to create a new command in most cases. This class provides various parameters and fields that will likely be useful when creating a command. Here is a simple example of a working command file: -```js -import Command from "../../classes/command.js"; - -class HelloCommand extends Command { - async run() { - return "Hello world!"; - } - - static description = "A simple command example"; - static aliases = ["helloworld"]; -} - -export default HelloCommand; -``` -As you can see, the first thing we do is import the Command class. We then create a new class for the command that extends that class to provide the needed parameters. We then define the command function, which is named `run`. Some static parameters, including the command description and an alias for the command, `helloworld`, are also defined. Finally, once everything in the command class is defined, we export the new class to be loaded as a module by the command handler. - -The default command name is the same as the filename that you save it as, excluding the `.js` file extension. If you ever want to change the name of the command, just rename the file. - -The parameters available to your command consist of the following: - -- `this.client`: An instance of an Oceanic [`Client`](https://docs.oceanic.ws/dev/classes/Client.Client.html), useful for getting info or performing lower-level communication with the Discord API. -- `this.origOptions`: The raw options object provided to the command by the command handler. -- `this.type`: The type of message that activated the command. Can be "classic" (a regular message) or "application" (slash/context menu commands). -- `this.channel`: An Oceanic [`TextChannel`](https://docs.oceanic.ws/dev/classes/TextChannel.TextChannel.html) object of the channel that the command was run in, useful for getting info about a server and how to respond to a message. -- `this.guild`: An Oceanic [`Guild`](https://docs.oceanic.ws/dev/classes/Guild.Guild.html) object of the guild that the command was run in. This is undefined in DMs. -- `this.author`: An Oceanic [`User`](https://docs.oceanic.ws/dev/classes/User.User.html) object of the user who ran the command, or a [`Member`](https://docs.oceanic.ws/dev/classes/Member.Member.html) object identical to `this.member` if run in a server as a slash command. -- `this.member`: An Oceanic [`Member`](https://docs.oceanic.ws/dev/classes/Member.Member.html) object of the server member who ran the command. When running the command outside of a server, this parameter is undefined when run as a "classic" command or a [`User`](https://docs.oceanic.ws/dev/classes/User.User.html) object identical to `this.author` when run as a slash command. -- `this.options`: When run as a "classic" command, this is an object of special arguments (e.g. `--argument=true`) passed to the command. These arguments are stored in a key/value format, so following the previous example, `this.options.argument` would return true. When run as a slash command, this is an object of every argument passed to the command. - -Some options are only available depending on the context/original message type, which can be checked with `this.type`. The options only available with "classic" messages are listed below: - -- `this.message`: An Oceanic [`Message`](https://docs.oceanic.ws/dev/classes/Message.Message.html) object of the message that the command was run from, useful for interaction. -- `this.args`: An array of text arguments passed to the command. -- `this.content`: A string of the raw content of the command message, excluding the prefix and command name. -- `this.reference`: An object that's useful if you ever decide to reply to a user inside the command. You can use [`Object.assign`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/assign) to combine your message content with this parameter. - -The options only available with application (slash and context menu) commands are listed below: - -- `this.interaction`: An Oceanic [`CommandInteraction`](https://docs.oceanic.ws/dev/classes/CommandInteraction.CommandInteraction.html) object of the incoming slash command data. -- `this.optionsArray`: A raw array of command options. Should rarely be used. -- `this.success`: A boolean value that causes the bot to respond with a normal message when `true`, or an "ephemeral" message (a message that's only visible to the person who ran the command) when `false`. - -Some static fields are also available and can be set depending on your command. These fields are listed below: - -- `description`: Your command's description, which is shown in the help command. -- `aliases`: An array of command aliases. People will be able to run the command using these as well as the normal command name. -- `arguments`: An array of command argument types, which are shown in the help command. -- `flags`: An array of objects specifying command flags, or special arguments, that will be shown when running `help ` or a slash command. Example: -```js -static flags = [{ - name: "argument", - type: Constants.ApplicationCommandOptionTypes.STRING, // translates to 3, see https://discord.com/developers/docs/interactions/application-commands#application-command-object-application-command-option-type - description: "Does a thing", - ... -}]; -``` -- `slashAllowed`: Specifies whether or not the command is available via slash commands. -- `directAllowed`: Specifies whether or not a command is available in direct messages. -- `adminOnly`: Specifies whether or not a command should be limited to the bot owner(s). - -## The `run` Function +# Custom Commands +esmBot has a powerful and flexible command handler, allowing you to create new commands and categories simply by creating new files. This page will provide a reference for creating new commands. + +## Directory Structure +The bot loads commands from subdirectories inside of the `commands` directory, which looks something like this by default: +``` +commands/ + - fun + > cat.js + > ... + - general + > help.js + > ping.js + > ... + - image-editing + > caption.js + > speed.js + > ... +``` +As you can see, each command is grouped into categories, which are represented by subdirectories. To create a new category, you can simply create a new directory inside of the `commands` directory, and to create a new command, you can create a new JS file under one of those subdirectories. + +!!! tip + The `message` category is special; commands in here act as right-click context menu message commands instead of "classic" or slash commands. + +## Command Structure +It's recommended to use the `Command` class located in `classes/command.js` to create a new command in most cases. This class provides various parameters and fields that will likely be useful when creating a command. Here is a simple example of a working command file: +```js +import Command from "../../classes/command.js"; + +class HelloCommand extends Command { + async run() { + return "Hello world!"; + } + + static description = "A simple command example"; + static aliases = ["helloworld"]; +} + +export default HelloCommand; +``` +As you can see, the first thing we do is import the Command class. We then create a new class for the command that extends that class to provide the needed parameters. We then define the command function, which is named `run`. Some static parameters, including the command description and an alias for the command, `helloworld`, are also defined. Finally, once everything in the command class is defined, we export the new class to be loaded as a module by the command handler. + +The default command name is the same as the filename that you save it as, excluding the `.js` file extension. If you ever want to change the name of the command, just rename the file. + +The parameters available to your command consist of the following: + +- `this.client`: An instance of an Oceanic [`Client`](https://docs.oceanic.ws/dev/classes/Client.Client.html), useful for getting info or performing lower-level communication with the Discord API. +- `this.origOptions`: The raw options object provided to the command by the command handler. +- `this.type`: The type of message that activated the command. Can be "classic" (a regular message) or "application" (slash/context menu commands). +- `this.channel`: An Oceanic [`TextChannel`](https://docs.oceanic.ws/dev/classes/TextChannel.TextChannel.html) object of the channel that the command was run in, useful for getting info about a server and how to respond to a message. +- `this.guild`: An Oceanic [`Guild`](https://docs.oceanic.ws/dev/classes/Guild.Guild.html) object of the guild that the command was run in. This is undefined in DMs. +- `this.author`: An Oceanic [`User`](https://docs.oceanic.ws/dev/classes/User.User.html) object of the user who ran the command, or a [`Member`](https://docs.oceanic.ws/dev/classes/Member.Member.html) object identical to `this.member` if run in a server as a slash command. +- `this.member`: An Oceanic [`Member`](https://docs.oceanic.ws/dev/classes/Member.Member.html) object of the server member who ran the command. When running the command outside of a server, this parameter is undefined when run as a "classic" command or a [`User`](https://docs.oceanic.ws/dev/classes/User.User.html) object identical to `this.author` when run as a slash command. +- `this.options`: When run as a "classic" command, this is an object of special arguments (e.g. `--argument=true`) passed to the command. These arguments are stored in a key/value format, so following the previous example, `this.options.argument` would return true. When run as a slash command, this is an object of every argument passed to the command. + +Some options are only available depending on the context/original message type, which can be checked with `this.type`. The options only available with "classic" messages are listed below: + +- `this.message`: An Oceanic [`Message`](https://docs.oceanic.ws/dev/classes/Message.Message.html) object of the message that the command was run from, useful for interaction. +- `this.args`: An array of text arguments passed to the command. +- `this.content`: A string of the raw content of the command message, excluding the prefix and command name. +- `this.reference`: An object that's useful if you ever decide to reply to a user inside the command. You can use [`Object.assign`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/assign) to combine your message content with this parameter. + +The options only available with application (slash and context menu) commands are listed below: + +- `this.interaction`: An Oceanic [`CommandInteraction`](https://docs.oceanic.ws/dev/classes/CommandInteraction.CommandInteraction.html) object of the incoming slash command data. +- `this.optionsArray`: A raw array of command options. Should rarely be used. +- `this.success`: A boolean value that causes the bot to respond with a normal message when `true`, or an "ephemeral" message (a message that's only visible to the person who ran the command) when `false`. + +Some static fields are also available and can be set depending on your command. These fields are listed below: + +- `description`: Your command's description, which is shown in the help command. +- `aliases`: An array of command aliases. People will be able to run the command using these as well as the normal command name. +- `arguments`: An array of command argument types, which are shown in the help command. +- `flags`: An array of objects specifying command flags, or special arguments, that will be shown when running `help ` or a slash command. Example: +```js +static flags = [{ + name: "argument", + type: Constants.ApplicationCommandOptionTypes.STRING, // translates to 3, see https://discord.com/developers/docs/interactions/application-commands#application-command-object-application-command-option-type + description: "Does a thing", + ... +}]; +``` +- `slashAllowed`: Specifies whether or not the command is available via slash commands. +- `directAllowed`: Specifies whether or not a command is available in direct messages. +- `adminOnly`: Specifies whether or not a command should be limited to the bot owner(s). + +## The `run` Function The main JS code of your command is specified in the `run` function. This function should return a [`Promise`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise) of your command output, which is why the `run` function [is an async function by default](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/async_function). The return value inside the `Promise` should be either a string or an object; you should return a string whenever you intend to reply with plain text, or an object if you intend to reply with something else, such as an embed or attachment. \ No newline at end of file diff --git a/docs/docker.md b/docs/docker.md index 0f9a8a1..f9b5f37 100644 --- a/docs/docker.md +++ b/docs/docker.md @@ -1,27 +1,27 @@ -# Docker -You can run the bot as well as its dependencies as a series of Docker containers. However, the manual setup is still recommended if you want more control over the bot. - -[To start, install Docker on your system via the instructions here.](https://docs.docker.com/engine/install/#server) - -Once you've installed Docker, you should clone the esmBot repo: -```sh -cd ~ -git clone --recurse-submodules https://github.com/esmBot/esmBot -cd esmBot -``` -Modify the `.env` file as described in step 7 of the manual setup. Make sure to change the `DB` option to this, however: -``` -DB=postgresql://esmbot:verycoolpass100@postgres:5432/esmbot -``` -You should then modify the `config/servers.json` file to change the IP addresses of the servers to match the Docker containers. Example: -```json -{ - "lava": [ - { "name": "localhost", "url": "lavalink:2333", "auth": "youshallnotpass", "local": true } - ], - "image": [ - { "server": "api", "auth": "verycoolpass100", "tls": false } - ] -} -``` +# Docker +You can run the bot as well as its dependencies as a series of Docker containers. However, the manual setup is still recommended if you want more control over the bot. + +[To start, install Docker on your system via the instructions here.](https://docs.docker.com/engine/install/#server) + +Once you've installed Docker, you should clone the esmBot repo: +```sh +cd ~ +git clone --recurse-submodules https://github.com/esmBot/esmBot +cd esmBot +``` +Modify the `.env` file as described in step 7 of the manual setup. Make sure to change the `DB` option to this, however: +``` +DB=postgresql://esmbot:verycoolpass100@postgres:5432/esmbot +``` +You should then modify the `config/servers.json` file to change the IP addresses of the servers to match the Docker containers. Example: +```json +{ + "lava": [ + { "name": "localhost", "url": "lavalink:2333", "auth": "youshallnotpass", "local": true } + ], + "image": [ + { "server": "api", "auth": "verycoolpass100", "tls": false } + ] +} +``` Finally, start the bot by running `docker-compose up -d`. \ No newline at end of file diff --git a/docs/index.md b/docs/index.md index 822cfcc..9c55a79 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1,3 +1,3 @@ -Welcome to the esmBot documentation site! - +Welcome to the esmBot documentation site! + You're probably looking for the [Setup](setup.md) page. If you're an end user looking to invite the bot, head over to [https://esmbot.net/invite](https://esmbot.net/invite). \ No newline at end of file diff --git a/docs/postgresql.md b/docs/postgresql.md index 7868d7f..8bd4e8e 100644 --- a/docs/postgresql.md +++ b/docs/postgresql.md @@ -1,65 +1,65 @@ -# PostgreSQL -Here are some instructions for setting up PostgreSQL for use with esmBot. - -### 1. Install PostgreSQL. - -=== "Debian/Ubuntu" - ```sh - sudo apt-get install postgresql postgresql-client - ``` -=== "Fedora/RHEL" - ```sh - sudo dnf install postgresql postgresql-server - ``` -=== "Alpine" - ```sh - doas apk add postgresql - ``` -=== "Arch/Manjaro" - ```sh - sudo pacman -S postgresql - ``` - -*** - -### 2. (Optional) Tune PostgreSQL. - -[PGTune](https://pgtune.leopard.in.ua/) is a useful tool for generating configuration files for your PostgreSQL database. It is highly recommended that you generate a config using this tool as it can increase stability and performance. - -*** - -### 3. Create the bot user and database. - -When you install PostgreSQL, it'll create a new user on your system that acts as the "superuser" of the database. You'll need to run Postgres commands as this user; however, you can run a command as that user without switching to it by using `sudo`. - -First, you'll need to create a user that the bot can interact with as well as the database itself: -```sh -sudo su - postgres -c "createuser esmbot" -sudo su - postgres -c "createdb esmbot" -``` -Then, launch the PostgreSQL shell for the next few commands: -```sh -sudo -u postgres psql -``` -If you want to give the user a password, you can do so like this: -```sql -ALTER USER esmbot WITH PASSWORD 'new_password'; -``` -Once you're inside the shell, you'll need to make sure the bot owns the database and has permissions: -```sql -ALTER DATABASE esmbot OWNER TO esmbot; -``` - -You're done! - -*** - -### Troubleshooting -If you get an error like `error: permission denied for table counts` when attempting to run the bot, try running these commands in the PostgreSQL shell: -```sql -\c esmbot -GRANT ALL PRIVILEGES ON guilds TO esmbot; -GRANT ALL PRIVILEGES ON counts TO esmbot; -GRANT ALL PRIVILEGES ON tags TO esmbot; -\q -``` +# PostgreSQL +Here are some instructions for setting up PostgreSQL for use with esmBot. + +### 1. Install PostgreSQL. + +=== "Debian/Ubuntu" + ```sh + sudo apt-get install postgresql postgresql-client + ``` +=== "Fedora/RHEL" + ```sh + sudo dnf install postgresql postgresql-server + ``` +=== "Alpine" + ```sh + doas apk add postgresql + ``` +=== "Arch/Manjaro" + ```sh + sudo pacman -S postgresql + ``` + +*** + +### 2. (Optional) Tune PostgreSQL. + +[PGTune](https://pgtune.leopard.in.ua/) is a useful tool for generating configuration files for your PostgreSQL database. It is highly recommended that you generate a config using this tool as it can increase stability and performance. + +*** + +### 3. Create the bot user and database. + +When you install PostgreSQL, it'll create a new user on your system that acts as the "superuser" of the database. You'll need to run Postgres commands as this user; however, you can run a command as that user without switching to it by using `sudo`. + +First, you'll need to create a user that the bot can interact with as well as the database itself: +```sh +sudo su - postgres -c "createuser esmbot" +sudo su - postgres -c "createdb esmbot" +``` +Then, launch the PostgreSQL shell for the next few commands: +```sh +sudo -u postgres psql +``` +If you want to give the user a password, you can do so like this: +```sql +ALTER USER esmbot WITH PASSWORD 'new_password'; +``` +Once you're inside the shell, you'll need to make sure the bot owns the database and has permissions: +```sql +ALTER DATABASE esmbot OWNER TO esmbot; +``` + +You're done! + +*** + +### Troubleshooting +If you get an error like `error: permission denied for table counts` when attempting to run the bot, try running these commands in the PostgreSQL shell: +```sql +\c esmbot +GRANT ALL PRIVILEGES ON guilds TO esmbot; +GRANT ALL PRIVILEGES ON counts TO esmbot; +GRANT ALL PRIVILEGES ON tags TO esmbot; +\q +``` diff --git a/docs/requirements.txt b/docs/requirements.txt index 5919752..386bc86 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -1,2 +1,2 @@ -mkdocs-glightbox +mkdocs-glightbox mkdocs-git-revision-date-localized-plugin \ No newline at end of file diff --git a/docs/setup.md b/docs/setup.md index 28212ce..926b16e 100644 --- a/docs/setup.md +++ b/docs/setup.md @@ -1,237 +1,237 @@ -# Setup -Here are some instructions to get esmBot up and running from source. - -??? check "Recommended system requirements" - - 64-bit CPU/operating system - - Quad-core CPU or better - - 1GB or more of RAM - - Linux-based operating system or virtual machine ([Ubuntu 22.04 LTS](https://ubuntu.com/download/server) or [Fedora 36](https://getfedora.org/) are recommended) - -!!! warning - If you want to run the bot on Windows, [Windows Subsystem for Linux](https://docs.microsoft.com/en-us/windows/wsl/install-win10) is recommended. This guide is somewhat Linux-centric, so for now you're mostly on your own if you decide not to use WSL. - -If you have any further questions regarding setup, feel free to ask in the #support channel on the [esmBot Support server](https://esmbot.net/support). - -!!! tip - You can run the bot using Docker for a somewhat simpler setup experience. [Click here to go to the Docker setup guide.](https://docs.esmbot.net/docker) - -### 1. Install the required native dependencies. -Choose the distro you're using below for insallation instructions. -=== "Debian/Ubuntu" - These instructions apply to Debian version 12 (bookworm) or Ubuntu version 22.04 (jammy) or later. - ```sh - sudo apt-get install git curl build-essential cmake ffmpeg sqlite3 ttf-mscorefonts-installer libmagick++-dev libvips-dev libcgif-dev libgirepository1.0-dev fonts-noto-color-emoji libimagequant-dev meson - ``` - On older Debian/Ubuntu versions, you may need to install some of these packages (notably libcgif-dev and meson) through alternative methods. -=== "Fedora/RHEL" - These instructions apply to Fedora 36/RHEL 9 or later. - - Some of these packages require that you add the RPM Fusion and/or EPEL repositories. You can find instructions on how to add them [here](https://rpmfusion.org/Configuration). - ```sh - sudo dnf install git curl cmake ffmpeg sqlite gcc-c++ libcgif-devel ImageMagick-c++-devel vips-devel libimagequant-devel gobject-introspection-devel google-noto-emoji-color-fonts meson - ``` - On RHEL-based distros like AlmaLinux and Rocky Linux, you may need to add [Remi's RPM Repository](https://rpms.remirepo.net) for the vips package. -=== "Alpine" - These instructions apply to the current Edge versions. - ```sh - doas apk add git curl msttcorefonts-installer python3 sqlite3 alpine-sdk cmake ffmpeg imagemagick-dev vips-dev font-noto-emoji gobject-introspection-dev cgif-dev libimagequant-dev meson - ``` -=== "Arch/Manjaro" - ```sh - sudo pacman -S git curl cmake pango ffmpeg npm imagemagick libvips sqlite3 libltdl noto-fonts-emoji gobject-introspection libcgif libimagequant meson - ``` - You'll also need to install [`ttf-ms-win10-auto`](https://aur.archlinux.org/packages/ttf-ms-win10-auto/) from the AUR. - -*** - -### 2. Install libvips. - -[libvips](https://github.com/libvips/libvips) is the core of esmBot's image processing commands. Version 8.13.0 or higher is recommended because it contains fixes to GIF handling and support for the freeze command; however, this version isn't packaged for most distros yet. To fix this, you'll need to build libvips from source. - -!!! note - Alpine, Arch, RHEL **(not Fedora!)**, and Ubuntu 22.10 (Kinetic Kudu) users can skip this step, since these distros now have 8.13.0 packaged. - -First, download the source and move into it: -```sh -git clone https://github.com/libvips/libvips -cd libvips -``` -From here, you can set up the build: -```sh -meson setup --prefix=/usr --buildtype=release -Dnsgif=true build -``` -If that command finishes with no errors, you can compile and install it: -```sh -cd build -meson compile -sudo meson install -``` - -*** - -### 3. Install Node.js. - -Node.js is the runtime that esmBot is built on top of. The bot requires version 16 or above to run. - -First things first, we'll need to install pnpm, the package manager used by the bot. Run the following to install it: -```sh -curl -fsSL https://get.pnpm.io/install.sh | sh - -``` - -Once you've done that, continue with the instructions for your operating system below. - -=== "Debian/Ubuntu" - You'll need a more recent version than what's provided in most Debian/Ubuntu-based distros. You can add a repository that contains a supported version by running this command: - ```sh - curl -fsSL https://deb.nodesource.com/setup_16.x | sudo bash - - ``` - After that, you can install Node.js with this command: - ```sh - sudo apt-get install nodejs - ``` -=== "Fedora/RHEL" - ```sh - sudo dnf install nodejs - ``` -=== "Alpine" - ```sh - doas apk add nodejs - ``` -=== "Arch/Manjaro" - ```sh - sudo pacman -S nodejs - ``` - -*** - -### 4. Set up the database. - -esmBot officially supports two database systems: SQLite and PostgreSQL. While SQLite is smaller and requires no initial setup, PostgreSQL has better performance (especially in large environments). - -!!! tip - If you're new to databases and self-hosting, choose SQLite. - -If you would like to use the SQLite database, no configuration is needed and you can move on to the next step. - -If you would like to use the PostgreSQL database, view the setup instructions [here](https://docs.esmbot.net/postgresql) and come back here when you're finished. - -*** - -### 5. Clone the repo and install the required Node modules. -```sh -cd ~ -git clone --recursive https://github.com/esmBot/esmBot -cd esmBot -pnpm i -g node-gyp -pnpm install -pnpm build -``` - -*** - -### 6. (Optional) Set up Lavalink. - -Lavalink is the audio server used by esmBot for soundboard commands and music playback. If you do not plan on using these features, you can safely skip this step. - -!!! warning - There are websites out there providing lists of public Lavalink instances that can be used with the bot. However, these are not recommended due to performance/security concerns and missing features, and it is highly recommended to set one up yourself instead using the steps below. - -Lavalink requires a Java (11 or later) installation. You can use [SDKMAN](https://sdkman.io) to install Eclipse Temurin, a popular Java distribution: -```sh -sdk install java 11.0.15-tem -``` - -Initial setup is like this: -```sh -cd ~ -mkdir Lavalink -cd Lavalink -curl -OL https://github.com/freyacodes/Lavalink/releases/latest/download/Lavalink.jar -cp ~/esmBot/application.yml . -ln -s ~/esmBot/assets assets -``` -To run Lavalink, you can use this command: -```sh -java -Djdk.tls.client.protocols=TLSv1.2 -jar Lavalink.jar -``` - -!!! info - You'll need to run Lavalink alongside the bot in order to use it. There are a few methods to do this, such as the `screen` command, creating a new systemd service, or simply just opening a new terminal session alongside your current one. - -*** - -### 7. Configure the bot. - -Configuration is done via environment variables which can be specified through a `.env` file. Copy `.env.example` to get a starter config file: -```sh -cp .env.example .env -``` - -!!! tip - If you can't see either of these files, don't worry - Linux treats files whose names start with a . as hidden files. - -To edit this file in the terminal, run this command: -```sh -nano .env -``` -This will launch a text editor with the file ready to go. Create a Discord application [here](https://discord.com/developers/applications) and select the Bot tab on the left, then create a bot user. Once you've done this, copy the token it gives you and put it in the `TOKEN` variable. - -When you're finished editing the file, press Ctrl + X, then Y and Enter. - -An overview of each of the variables in the `.env` file can be found [here](https://docs.esmbot.net/config). - -*** - -### 8. Run the bot. - -Once everything else is set up, you can start the bot like so: -```sh -pnpm start -``` -If the bot starts successfully, you're done! You can invite the bot to your server by generating an invite link under OAuth -> URL Generator in the Discord application dashboard. - -!!! note - You will need to select the `bot` and `applications.commands` scopes. - The following permissions are needed in most cases for the bot to work properly: - -
- ![Required permissions](assets/permissions.png){ loading=lazy, width=500 } -
click to enlarge
-
- -If you want the bot to run 24/7, you can use the [PM2](https://pm2.keymetrics.io) process manager. Install it using the following command: -```sh -pnpm add -g pm2 -``` - -Once you've done that, you can start the bot using the following command: -```sh -pm2 start ecosystem.config.cjs -``` - -!!! tip - If you wish to update the bot to the latest version/commit at any time, just run `git pull` and `pnpm install`. - -*** - -## Troubleshooting -??? faq "Error: Cannot find module './build/Release/image.node'" - The native image functions haven't been built. Run `pnpm run build` to build them. - -??? faq "`pnpm install` or `pnpm build` fails with error 'ELIFECYCLE  Command failed.'" - You seem to be missing node-gyp. This can be fixed by running: - ```sh - pnpm i -g node-gyp - rm -rf node_modules - pnpm install - ``` - -??? faq "Error: connect ECONNREFUSED 127.0.0.1:5432" - PostgreSQL isn't running, you should be able to start it with `sudo systemctl start postgresql`. If you don't intend to use PostgreSQL, you should take another look at your `DB` variable in the .env file. - -??? faq "Gifs from Tenor result in a "no decode delegate for this image format" or "improper image header" error" - Tenor GIFs are actually stored as MP4s, which libvips can't decode most of the time. You'll need to get a Tenor API key from [here](https://developers.google.com/tenor/guides/quickstart) and put it in the `TENOR` variable in .env. - -*** - -If you have any further questions regarding self-hosting, feel free to ask in the #support channel on the [esmBot Support server](https://esmbot.net/support). +# Setup +Here are some instructions to get esmBot up and running from source. + +??? check "Recommended system requirements" + - 64-bit CPU/operating system + - Quad-core CPU or better + - 1GB or more of RAM + - Linux-based operating system or virtual machine ([Ubuntu 22.04 LTS](https://ubuntu.com/download/server) or [Fedora 36](https://getfedora.org/) are recommended) + +!!! warning + If you want to run the bot on Windows, [Windows Subsystem for Linux](https://docs.microsoft.com/en-us/windows/wsl/install-win10) is recommended. This guide is somewhat Linux-centric, so for now you're mostly on your own if you decide not to use WSL. + +If you have any further questions regarding setup, feel free to ask in the #support channel on the [esmBot Support server](https://esmbot.net/support). + +!!! tip + You can run the bot using Docker for a somewhat simpler setup experience. [Click here to go to the Docker setup guide.](https://docs.esmbot.net/docker) + +### 1. Install the required native dependencies. +Choose the distro you're using below for insallation instructions. +=== "Debian/Ubuntu" + These instructions apply to Debian version 12 (bookworm) or Ubuntu version 22.04 (jammy) or later. + ```sh + sudo apt-get install git curl build-essential cmake ffmpeg sqlite3 ttf-mscorefonts-installer libmagick++-dev libvips-dev libcgif-dev libgirepository1.0-dev fonts-noto-color-emoji libimagequant-dev meson + ``` + On older Debian/Ubuntu versions, you may need to install some of these packages (notably libcgif-dev and meson) through alternative methods. +=== "Fedora/RHEL" + These instructions apply to Fedora 36/RHEL 9 or later. + + Some of these packages require that you add the RPM Fusion and/or EPEL repositories. You can find instructions on how to add them [here](https://rpmfusion.org/Configuration). + ```sh + sudo dnf install git curl cmake ffmpeg sqlite gcc-c++ libcgif-devel ImageMagick-c++-devel vips-devel libimagequant-devel gobject-introspection-devel google-noto-emoji-color-fonts meson + ``` + On RHEL-based distros like AlmaLinux and Rocky Linux, you may need to add [Remi's RPM Repository](https://rpms.remirepo.net) for the vips package. +=== "Alpine" + These instructions apply to the current Edge versions. + ```sh + doas apk add git curl msttcorefonts-installer python3 sqlite3 alpine-sdk cmake ffmpeg imagemagick-dev vips-dev font-noto-emoji gobject-introspection-dev cgif-dev libimagequant-dev meson + ``` +=== "Arch/Manjaro" + ```sh + sudo pacman -S git curl cmake pango ffmpeg npm imagemagick libvips sqlite3 libltdl noto-fonts-emoji gobject-introspection libcgif libimagequant meson + ``` + You'll also need to install [`ttf-ms-win10-auto`](https://aur.archlinux.org/packages/ttf-ms-win10-auto/) from the AUR. + +*** + +### 2. Install libvips. + +[libvips](https://github.com/libvips/libvips) is the core of esmBot's image processing commands. Version 8.13.0 or higher is recommended because it contains fixes to GIF handling and support for the freeze command; however, this version isn't packaged for most distros yet. To fix this, you'll need to build libvips from source. + +!!! note + Alpine, Arch, RHEL **(not Fedora!)**, and Ubuntu 22.10 (Kinetic Kudu) users can skip this step, since these distros now have 8.13.0 packaged. + +First, download the source and move into it: +```sh +git clone https://github.com/libvips/libvips +cd libvips +``` +From here, you can set up the build: +```sh +meson setup --prefix=/usr --buildtype=release -Dnsgif=true build +``` +If that command finishes with no errors, you can compile and install it: +```sh +cd build +meson compile +sudo meson install +``` + +*** + +### 3. Install Node.js. + +Node.js is the runtime that esmBot is built on top of. The bot requires version 16 or above to run. + +First things first, we'll need to install pnpm, the package manager used by the bot. Run the following to install it: +```sh +curl -fsSL https://get.pnpm.io/install.sh | sh - +``` + +Once you've done that, continue with the instructions for your operating system below. + +=== "Debian/Ubuntu" + You'll need a more recent version than what's provided in most Debian/Ubuntu-based distros. You can add a repository that contains a supported version by running this command: + ```sh + curl -fsSL https://deb.nodesource.com/setup_16.x | sudo bash - + ``` + After that, you can install Node.js with this command: + ```sh + sudo apt-get install nodejs + ``` +=== "Fedora/RHEL" + ```sh + sudo dnf install nodejs + ``` +=== "Alpine" + ```sh + doas apk add nodejs + ``` +=== "Arch/Manjaro" + ```sh + sudo pacman -S nodejs + ``` + +*** + +### 4. Set up the database. + +esmBot officially supports two database systems: SQLite and PostgreSQL. While SQLite is smaller and requires no initial setup, PostgreSQL has better performance (especially in large environments). + +!!! tip + If you're new to databases and self-hosting, choose SQLite. + +If you would like to use the SQLite database, no configuration is needed and you can move on to the next step. + +If you would like to use the PostgreSQL database, view the setup instructions [here](https://docs.esmbot.net/postgresql) and come back here when you're finished. + +*** + +### 5. Clone the repo and install the required Node modules. +```sh +cd ~ +git clone --recursive https://github.com/esmBot/esmBot +cd esmBot +pnpm i -g node-gyp +pnpm install +pnpm build +``` + +*** + +### 6. (Optional) Set up Lavalink. + +Lavalink is the audio server used by esmBot for soundboard commands and music playback. If you do not plan on using these features, you can safely skip this step. + +!!! warning + There are websites out there providing lists of public Lavalink instances that can be used with the bot. However, these are not recommended due to performance/security concerns and missing features, and it is highly recommended to set one up yourself instead using the steps below. + +Lavalink requires a Java (11 or later) installation. You can use [SDKMAN](https://sdkman.io) to install Eclipse Temurin, a popular Java distribution: +```sh +sdk install java 11.0.15-tem +``` + +Initial setup is like this: +```sh +cd ~ +mkdir Lavalink +cd Lavalink +curl -OL https://github.com/freyacodes/Lavalink/releases/latest/download/Lavalink.jar +cp ~/esmBot/application.yml . +ln -s ~/esmBot/assets assets +``` +To run Lavalink, you can use this command: +```sh +java -Djdk.tls.client.protocols=TLSv1.2 -jar Lavalink.jar +``` + +!!! info + You'll need to run Lavalink alongside the bot in order to use it. There are a few methods to do this, such as the `screen` command, creating a new systemd service, or simply just opening a new terminal session alongside your current one. + +*** + +### 7. Configure the bot. + +Configuration is done via environment variables which can be specified through a `.env` file. Copy `.env.example` to get a starter config file: +```sh +cp .env.example .env +``` + +!!! tip + If you can't see either of these files, don't worry - Linux treats files whose names start with a . as hidden files. + +To edit this file in the terminal, run this command: +```sh +nano .env +``` +This will launch a text editor with the file ready to go. Create a Discord application [here](https://discord.com/developers/applications) and select the Bot tab on the left, then create a bot user. Once you've done this, copy the token it gives you and put it in the `TOKEN` variable. + +When you're finished editing the file, press Ctrl + X, then Y and Enter. + +An overview of each of the variables in the `.env` file can be found [here](https://docs.esmbot.net/config). + +*** + +### 8. Run the bot. + +Once everything else is set up, you can start the bot like so: +```sh +pnpm start +``` +If the bot starts successfully, you're done! You can invite the bot to your server by generating an invite link under OAuth -> URL Generator in the Discord application dashboard. + +!!! note + You will need to select the `bot` and `applications.commands` scopes. + The following permissions are needed in most cases for the bot to work properly: + +
+ ![Required permissions](assets/permissions.png){ loading=lazy, width=500 } +
click to enlarge
+
+ +If you want the bot to run 24/7, you can use the [PM2](https://pm2.keymetrics.io) process manager. Install it using the following command: +```sh +pnpm add -g pm2 +``` + +Once you've done that, you can start the bot using the following command: +```sh +pm2 start ecosystem.config.cjs +``` + +!!! tip + If you wish to update the bot to the latest version/commit at any time, just run `git pull` and `pnpm install`. + +*** + +## Troubleshooting +??? faq "Error: Cannot find module './build/Release/image.node'" + The native image functions haven't been built. Run `pnpm run build` to build them. + +??? faq "`pnpm install` or `pnpm build` fails with error 'ELIFECYCLE  Command failed.'" + You seem to be missing node-gyp. This can be fixed by running: + ```sh + pnpm i -g node-gyp + rm -rf node_modules + pnpm install + ``` + +??? faq "Error: connect ECONNREFUSED 127.0.0.1:5432" + PostgreSQL isn't running, you should be able to start it with `sudo systemctl start postgresql`. If you don't intend to use PostgreSQL, you should take another look at your `DB` variable in the .env file. + +??? faq "Gifs from Tenor result in a "no decode delegate for this image format" or "improper image header" error" + Tenor GIFs are actually stored as MP4s, which libvips can't decode most of the time. You'll need to get a Tenor API key from [here](https://developers.google.com/tenor/guides/quickstart) and put it in the `TENOR` variable in .env. + +*** + +If you have any further questions regarding self-hosting, feel free to ask in the #support channel on the [esmBot Support server](https://esmbot.net/support). diff --git a/ecosystem.config.cjs b/ecosystem.config.cjs index bc19548..721f94b 100644 --- a/ecosystem.config.cjs +++ b/ecosystem.config.cjs @@ -1,10 +1,10 @@ -module.exports = { - apps: [{ - name: "esmBot-manager", - script: "utils/pm2/ext.js", - autorestart: true, - exp_backoff_restart_delay: 1000, - watch: false, - exec_mode: "fork" - }] +module.exports = { + apps: [{ + name: "esmBot-manager", + script: "utils/pm2/ext.js", + autorestart: true, + exp_backoff_restart_delay: 1000, + watch: false, + exec_mode: "fork" + }] }; \ No newline at end of file diff --git a/events/debug.js b/events/debug.js index aa77abb..ac9663b 100644 --- a/events/debug.js +++ b/events/debug.js @@ -1,5 +1,5 @@ -import { debug } from "../utils/logger.js"; - -export default async (client, message) => { - debug(message); +import { debug } from "../utils/logger.js"; + +export default async (client, message) => { + debug(message); }; \ No newline at end of file diff --git a/events/error.js b/events/error.js index 71e1e84..91eec74 100644 --- a/events/error.js +++ b/events/error.js @@ -1,5 +1,5 @@ -import { error } from "../utils/logger.js"; - -export default async (client, message) => { - error(message); +import { error } from "../utils/logger.js"; + +export default async (client, message) => { + error(message); }; \ No newline at end of file diff --git a/events/guildCreate.js b/events/guildCreate.js deleted file mode 100644 index f63ca10..0000000 --- a/events/guildCreate.js +++ /dev/null @@ -1,6 +0,0 @@ -import { log } from "../utils/logger.js"; - -// run when the bot is added to a guild -export default async (client, guild) => { - log(`[GUILD JOIN] ${guild.name} (${guild.id}) added the bot.`); -}; diff --git a/events/guildDelete.js b/events/guildDelete.js deleted file mode 100644 index 8bb2352..0000000 --- a/events/guildDelete.js +++ /dev/null @@ -1,6 +0,0 @@ -import { log } from "../utils/logger.js"; - -// run when the bot is removed from a guild -export default async (client, guild) => { - log(`[GUILD LEAVE] ${guild.name} (${guild.id}) removed the bot.`); -}; diff --git a/events/interactionCreate.js b/events/interactionCreate.js deleted file mode 100644 index 845317b..0000000 --- a/events/interactionCreate.js +++ /dev/null @@ -1,89 +0,0 @@ -import database from "../utils/database.js"; -import * as logger from "../utils/logger.js"; -import { commands, messageCommands } from "../utils/collections.js"; -import { clean } from "../utils/misc.js"; -import { upload } from "../utils/tempimages.js"; - -// run when a slash command is executed -export default async (client, interaction) => { - if (interaction?.type !== 2) return; - - // check if command exists and if it's enabled - const command = interaction.data.name; - let cmd = commands.get(command); - if (!cmd) { - cmd = messageCommands.get(command); - if (!cmd) return; - } - if (cmd.dbRequired && !database) { - await interaction["createMessage"]({ content: "This command is unavailable on stateless instances of esmBot.", flags: 64 }); - return; - } - - const invoker = interaction.member ?? interaction.user; - - // actually run the command - logger.log("log", `${invoker.username} (${invoker.id}) ran application command ${command}`); - try { - if (database) { - await database.addCount(command); - } - // eslint-disable-next-line no-unused-vars - const commandClass = new cmd(client, { type: "application", interaction }); - const result = await commandClass.run(); - const replyMethod = interaction.acknowledged ? "editOriginal" : "createMessage"; - if (typeof result === "string") { - await interaction[replyMethod]({ - content: result, - flags: commandClass.success ? 0 : 64 - }); - } else if (typeof result === "object") { - if (result.contents && result.name) { - const fileSize = 8388119; - if (result.contents.length > fileSize) { - if (process.env.TEMPDIR && process.env.TEMPDIR !== "") { - await upload(client, result, interaction, true); - } else { - await interaction[replyMethod]({ - content: "The resulting image was more than 8MB in size, so I can't upload it.", - flags: 64 - }); - } - } else { - await interaction[replyMethod](result.text ? result.text : { files: [result] }); - } - } else { - await interaction[replyMethod](Object.assign({ - flags: result.flags ?? (commandClass.success ? 0 : 64) - }, result)); - } - } else { - logger.warn(`Unknown return type for command ${command}: ${result} (${typeof result})`); - await interaction[replyMethod](Object.assign({ - flags: commandClass.success ? 0 : 64 - }, result)); - } - } catch (error) { - const replyMethod = interaction.acknowledged ? "editOriginal" : "createMessage"; - if (error.toString().includes("Request entity too large")) { - await interaction[replyMethod]({ content: "The resulting file was too large to upload. Try again with a smaller image if possible.", flags: 64 }); - } else if (error.toString().includes("Job ended prematurely")) { - await interaction[replyMethod]({ content: "Something happened to the image servers before I could receive the image. Try running your command again.", flags: 64 }); - } else { - logger.error(`Error occurred with application command ${command} with arguments ${JSON.stringify(interaction.data.optionsArray)}: ${error.stack || error}`); - try { - let err = error; - if (error?.constructor?.name == "Promise") err = await error; - await interaction[replyMethod]({ - content: "Uh oh! I ran into an error while running this command. Please report the content of the attached file at the following link or on the esmBot Support server: ", - files: [{ - contents: `Message: ${clean(err)}\n\nStack Trace: ${clean(err.stack)}`, - name: "error.txt" - }] - }); - } catch (e) { - logger.error(`While attempting to send the previous error message, another error occurred: ${e.stack || e}`); - } - } - } -}; diff --git a/events/messageCreate.js b/events/messageCreate.js deleted file mode 100644 index 614fdc9..0000000 --- a/events/messageCreate.js +++ /dev/null @@ -1,184 +0,0 @@ -import database from "../utils/database.js"; -import { log, error as _error } from "../utils/logger.js"; -import { prefixCache, aliases, disabledCache, disabledCmdCache, commands } from "../utils/collections.js"; -import parseCommand from "../utils/parseCommand.js"; -import { clean } from "../utils/misc.js"; -import { upload } from "../utils/tempimages.js"; -import { ThreadChannel } from "oceanic.js"; - -let mentionRegex; - -// run when someone sends a message -export default async (client, message) => { - // ignore other bots - if (message.author.bot) return; - - // don't run command if bot can't send messages - let permChannel = message.channel; - if (permChannel instanceof ThreadChannel && !permChannel.parent) { - try { - permChannel = await client.rest.channels.get(message.channel.parentID); - } catch { - return; - } - } - if (message.guildID && !permChannel.permissionsOf(client.user.id.toString()).has("SEND_MESSAGES")) return; - - if (!mentionRegex) mentionRegex = new RegExp(`^<@!?${client.user.id}> `); - - let guildDB; - let text; - const mentionResult = message.content.match(mentionRegex); - if (mentionResult) { - text = message.content.substring(mentionResult[0].length).trim(); - } else if (message.guildID && database) { - const cachedPrefix = prefixCache.get(message.guildID); - if (cachedPrefix && message.content.startsWith(cachedPrefix)) { - text = message.content.substring(cachedPrefix.length).trim(); - } else { - guildDB = await database.getGuild(message.guildID); - if (message.content.startsWith(guildDB.prefix)) { - text = message.content.substring(guildDB.prefix.length).trim(); - prefixCache.set(message.guildID, guildDB.prefix); - } else { - return; - } - } - } else if (message.content.startsWith(process.env.PREFIX)) { - text = message.content.substring(process.env.PREFIX.length).trim(); - } else if (!message.guildID) { - text = message.content; - } else { - return; - } - - // separate commands and args - const preArgs = text.split(/\s+/g); - const command = preArgs.shift().toLowerCase(); - const aliased = aliases.get(command); - - // check if command exists and if it's enabled - const cmd = commands.get(aliased ?? command); - if (!cmd) return; - - // block certain commands from running in DMs - if (!cmd.directAllowed && !message.guildID) return; - - if (cmd.dbRequired && !database) { - await client.rest.channels.createMessage(message.channelID, { - content: "This command is unavailable on stateless instances of esmBot." - }); - return; - } - - // don't run if message is in a disabled channel - if (message.guildID && database) { - let disabled = disabledCache.get(message.guildID); - if (!disabled) { - if (!guildDB) guildDB = await database.getGuild(message.guildID); - disabledCache.set(message.guildID, guildDB.disabled); - disabled = guildDB.disabled; - } - if (disabled.includes(message.channelID) && command != "channel") return; - - let disabledCmds = disabledCmdCache.get(message.guildID); - if (!disabledCmds) { - if (!guildDB) guildDB = await database.getGuild(message.guildID); - disabledCmdCache.set(message.guildID, guildDB.disabled_commands ?? guildDB.disabledCommands); - disabledCmds = guildDB.disabled_commands ?? guildDB.disabledCommands; - } - if (disabledCmds.includes(aliased ?? command)) return; - } - - // actually run the command - log("log", `${message.author.username} (${message.author.id}) ran classic command ${command}`); - const reference = { - messageReference: { - channelID: message.channelID, - messageID: message.id, - guildID: message.guildID ?? undefined, - failIfNotExists: false - }, - allowedMentions: { - repliedUser: false - } - }; - try { - // parse args - const parsed = parseCommand(preArgs); - if (database) { - await database.addCount(aliases.get(command) ?? command); - } - const startTime = new Date(); - // eslint-disable-next-line no-unused-vars - const commandClass = new cmd(client, { type: "classic", message, args: parsed._, content: text.replace(command, "").trim(), specialArgs: (({ _, ...o }) => o)(parsed) }); // we also provide the message content as a parameter for cases where we need more accuracy - const result = await commandClass.run(); - const endTime = new Date(); - if ((endTime - startTime) >= 180000) reference.allowedMentions.repliedUser = true; - if (typeof result === "string") { - reference.allowedMentions.repliedUser = true; - await client.rest.channels.createMessage(message.channelID, Object.assign({ - content: result - }, reference)); - } else if (typeof result === "object") { - if (result.contents && result.name) { - let fileSize = 8388119; - if (message.guildID) { - switch (message.guild.premiumTier) { - case 2: - fileSize = 52428308; - break; - case 3: - fileSize = 104856616; - break; - } - } - if (result.contents.length > fileSize) { - if (process.env.TEMPDIR && process.env.TEMPDIR !== "") { - await upload(client, result, message); - } else { - await client.rest.channels.createMessage(message.channelID, { - content: "The resulting image was more than 8MB in size, so I can't upload it." - }); - } - } else { - await client.rest.channels.createMessage(message.channelID, Object.assign({ - content: result.text ? result.text : undefined, - files: [result] - }, reference)); - } - } else { - await client.rest.channels.createMessage(message.channelID, Object.assign(result, reference)); - } - } - } catch (error) { - if (error.toString().includes("Request entity too large")) { - await client.rest.channels.createMessage(message.channelID, Object.assign({ - content: "The resulting file was too large to upload. Try again with a smaller image if possible." - }, reference)); - } else if (error.toString().includes("Job ended prematurely")) { - await client.rest.channels.createMessage(message.channelID, Object.assign({ - content: "Something happened to the image servers before I could receive the image. Try running your command again." - }, reference)); - } else if (error.toString().includes("Timed out")) { - await client.rest.channels.createMessage(message.channelID, Object.assign({ - content: "The request timed out before I could download that image. Try uploading your image somewhere else or reducing its size." - }, reference)); - } else { - _error(`Error occurred with command message ${message.content}: ${error.stack || error}`); - try { - let err = error; - if (error?.constructor?.name == "Promise") err = await error; - await client.rest.channels.createMessage(message.channelID, Object.assign({ - content: "Uh oh! I ran into an error while running this command. Please report the content of the attached file at the following link or on the esmBot Support server: ", - files: [{ - contents: `Message: ${clean(err)}\n\nStack Trace: ${clean(err.stack)}`, - name: "error.txt" - }] - }, reference)); - } catch (e) { - _error(`While attempting to send the previous error message, another error occurred: ${e.stack || e}`); - } - } - } -}; diff --git a/events/ready.js b/events/ready.js index 81d2435..c9d909e 100644 --- a/events/ready.js +++ b/events/ready.js @@ -1,38 +1,38 @@ -import { activityChanger, checkBroadcast } from "../utils/misc.js"; -import { send } from "../utils/handler.js"; -import { generateList, createPage } from "../utils/help.js"; -import { logger } from "../utils/logger.js"; -import { readFileSync } from "fs"; - -const { types } = JSON.parse(readFileSync(new URL("../config/commands.json", import.meta.url))); -let ready = false; - -export default async (client) => { - if (ready) return; - - // send slash command data - if (types.application) { - try { - await send(client); - } catch (e) { - logger.log("error", e); - logger.log("error", "Failed to send command data to Discord, slash/message commands may be unavailable."); - } - } - - // generate docs - if (process.env.OUTPUT && process.env.OUTPUT !== "") { - generateList(); - await createPage(process.env.OUTPUT); - logger.log("info", "The help docs have been generated."); - } - - await checkBroadcast(client); - activityChanger(client); - - ready = true; - - if (process.env.PM2_USAGE) process.send("ready"); - - logger.log("info", "Started esmBot."); +import { activityChanger, checkBroadcast } from "../utils/misc.js"; +import { send } from "../utils/handler.js"; +import { generateList, createPage } from "../utils/help.js"; +import { logger } from "../utils/logger.js"; +import { readFileSync } from "fs"; + +const { types } = JSON.parse(readFileSync(new URL("../config/commands.json", import.meta.url))); +let ready = false; + +export default async (client) => { + if (ready) return; + + // send slash command data + if (types.application) { + try { + await send(client); + } catch (e) { + logger.log("error", e); + logger.log("error", "Failed to send command data to Discord, slash/message commands may be unavailable."); + } + } + + // generate docs + if (process.env.OUTPUT && process.env.OUTPUT !== "") { + generateList(); + await createPage(process.env.OUTPUT); + logger.log("info", "The help docs have been generated."); + } + + await checkBroadcast(client); + activityChanger(client); + + ready = true; + + if (process.env.PM2_USAGE) process.send("ready"); + + logger.log("info", "Started esmBot."); }; \ No newline at end of file diff --git a/events/roommessage.js b/events/roommessage.js new file mode 100644 index 0000000..b90e8c9 --- /dev/null +++ b/events/roommessage.js @@ -0,0 +1,135 @@ +import database from "../utils/database.js"; +import { log, error as _error, logger } from "../utils/logger.js"; +import { prefixCache, aliases, disabledCache, disabledCmdCache, commands } from "../utils/collections.js"; +import parseCommand from "../utils/parseCommand.js"; +import { clean } from "../utils/misc.js"; +import sizeOf from "image-size"; +// import { upload } from "../utils/tempimages.js"; +// import { ThreadChannel } from "oceanic.js"; + +let mentionRegex; + +// run when someone sends a message +export default async function (matrixClient, event, room, toStartOfTimeline) { + // console.log(matrixClient) + if (event.getType() == "m.room.message") { + if (toStartOfTimeline) { + return; // don't act on paginated results + } + if (event.event.sender == process.env.MATRIX_USERNAME) return; + // console.log(event.event); + let text; + text = event.event.content.body; + // if a reply, strip the reply from the formatting + text = text.replace(/.*\n\n/g, "") + if (text.startsWith(process.env.PREFIX)) { + text = text.substring(process.env.PREFIX.length).trim(); + } else { + return; + } + + // separate commands and args + const preArgs = text.split(/\s+/g); + const command = preArgs.shift().toLowerCase(); + const aliased = aliases.get(command); + + const cmd = commands.get(aliased ?? command); + if (!cmd) return; + + // command time :peachtime: + log("log", `${event.sender.name} (${event.event.sender}) ran command ${command}`); + const reference = { + messageReference: { + channelID: event.event.room_id, + messageID: event.event.event_id, + guildID: undefined, + failIfNotExists: false + }, + allowedMentions: { + repliedUser: false + } + }; + try { + // parse args + const parsed = parseCommand(preArgs); + // if (database) { + // await database.addCount(aliases.get(command) ?? command); + // } + const startTime = new Date(); + // eslint-disable-next-line no-unused-vars + const commandClass = new cmd(matrixClient, { type: "classic", message: event.event, args: parsed._, content: text.replace(command, "").trim(), specialArgs: (({ _, ...o }) => o)(parsed) }); // we also provide the message content as a parameter for cases where we need more accuracy + const result = await commandClass.run(); + const endTime = new Date(); + if ((endTime - startTime) >= 180000) reference.allowedMentions.repliedUser = true; + if (typeof result === "string") { + const content = { + body: result, + msgtype: "m.text", + }; + matrixClient.sendEvent(event.event.room_id, "m.room.message", content, "", (err, res) => { + console.log(err); + }); + } else if (typeof result === "object") { + // console.log(result) + if (result.contents && result.name) { + let fileSize = 52428308; + if (result.contents.length > fileSize) { + if (process.env.TEMPDIR && process.env.TEMPDIR !== "") { + await upload(client, result, message); + } else { + const content = { + body: "imag too big :(", + msgtype: "m.text", + }; + matrixClient.sendEvent(event.event.room_id, "m.room.message", content, "", (err, res) => { + console.log(err); + }); + } + } else { + const mxcUri = await matrixClient.uploadContent(result.contents); + console.log(mxcUri.content_uri) + // TODO: make info object get width, height, and mime from natives so i dont need to read the buffer + const imgsize = sizeOf(result.contents) + await matrixClient.sendImageMessage(event.event.room_id, mxcUri.content_uri, {h: imgsize.height, w: imgsize.width, mimetype: `image/${imgsize.type}`, size: result.contents.length, thumbnail_info: {h: imgsize.height, w: imgsize.width, mimetype: `image/${imgsize.type}`, size: result.contents.length}}, result.name) + } + } else { + // await client.rest.channels.createMessage(message.channelID, Object.assign(result, reference)); + } + } else { + console.log(typeof result) + } + } catch (error) { + logger.log("error", error.stack) + // if (error.toString().includes("Request entity too large")) { + // await client.rest.channels.createMessage(event.event.room_id, Object.assign({ + // content: "The resulting file was too large to upload. Try again with a smaller image if possible." + // }, reference)); + // } else if (error.toString().includes("Job ended prematurely")) { + // await client.rest.channels.createMessage(event.event.room_id, Object.assign({ + // content: "Something happened to the image servers before I could receive the image. Try running your command again." + // }, reference)); + // } else if (error.toString().includes("Timed out")) { + // await client.rest.channels.createMessage(event.event.room_id, Object.assign({ + // content: "The request timed out before I could download that image. Try uploading your image somewhere else or reducing its size." + // }, reference)); + // } else { + // _error(`Error occurred with command message ${event.event.content.body}: ${error.stack || error}`); + // try { + // let err = error; + // if (error?.constructor?.name == "Promise") err = await error; + // await client.rest.channels.createMessage(event.event.room_id, Object.assign({ + // content: "Uh oh! I ran into an error while running this command. Please report the content of the attached file at the following link or on the esmBot Support server: ", + // files: [{ + // contents: `Message: ${clean(err)}\n\nStack Trace: ${clean(err.stack)}`, + // name: "error.txt" + // }] + // }, reference)); + // } catch (e) { + // _error(`While attempting to send the previous error message, another error occurred: ${e.stack || e}`); + // } + // } + } + return; + } + return; +}; \ No newline at end of file diff --git a/events/shardReady.js b/events/shardReady.js deleted file mode 100644 index 0df70b4..0000000 --- a/events/shardReady.js +++ /dev/null @@ -1,17 +0,0 @@ -import { players, errHandle } from "../utils/soundplayer.js"; - -export default async (client, id) => { - for (const player of players.values()) { - if (id !== player.voiceChannel.guild.shard.id) return; - try { - await player.player.connection.connect({ - guildId: player.voiceChannel.guildID, - channelId: player.voiceChannel.id, - shardId: player.voiceChannel.guild.shard.id, - deaf: true - }); - } catch (e) { - errHandle(e, client, player.player, player.playingMessage, player.voiceChannel, { type: "classic" }, true); - } - } -}; \ No newline at end of file diff --git a/events/voiceChannelLeave.js b/events/voiceChannelLeave.js deleted file mode 100644 index 00e8fdf..0000000 --- a/events/voiceChannelLeave.js +++ /dev/null @@ -1,115 +0,0 @@ -import { players, queues, skipVotes } from "../utils/soundplayer.js"; -import AwaitRejoin from "../utils/awaitrejoin.js"; -import { random } from "../utils/misc.js"; -import { logger } from "../utils/logger.js"; - -const isWaiting = new Map(); - -export default async (client, member, oldChannel) => { - if (!oldChannel) return; - const connection = players.get(oldChannel.guildID); - if (oldChannel.id === connection?.voiceChannel.id) { - if (oldChannel.voiceMembers.filter((i) => i.id !== client.user.id && !i.bot).length === 0) { - if (isWaiting.has(oldChannel.id)) return; - isWaiting.set(oldChannel.id, true); - connection.player.setPaused(true); - const waitMessage = await client.rest.channels.createMessage(connection.originalChannel.id, { - content: "🔊 Waiting 10 seconds for someone to return..." - }); - const awaitRejoin = new AwaitRejoin(oldChannel, true, member.id); - awaitRejoin.once("end", async (rejoined, newMember, cancel) => { - isWaiting.delete(oldChannel.id); - if (rejoined) { - if (cancel) return; - connection.player.setPaused(false); - if (member.id !== newMember.id) { - players.set(connection.voiceChannel.guildID, { player: connection.player, type: connection.type, host: newMember.id, voiceChannel: connection.voiceChannel, originalChannel: connection.originalChannel, loop: connection.loop, shuffle: connection.shuffle, playMessage: connection.playMessage }); - waitMessage.edit({ - content: `🔊 ${newMember.mention} is the new voice channel host.` - }); - } else { - try { - await waitMessage.delete(); - } catch { - logger.warn(`Failed to delete wait message ${waitMessage.id}`); - } - } - } else { - try { - if (waitMessage.channel.messages.has(waitMessage.id)) await waitMessage.delete(); - } catch { - logger.warn(`Failed to delete wait message ${waitMessage.id}`); - } - if (cancel) return; - try { - connection.player.node.leaveChannel(connection.originalChannel.guildID); - } catch { - logger.warn(`Failed to leave voice channel ${connection.originalChannel.guildID}`); - } - players.delete(connection.originalChannel.guildID); - queues.delete(connection.originalChannel.guildID); - skipVotes.delete(connection.originalChannel.guildID); - client.rest.channels.createMessage(connection.originalChannel.id, { - content: `🔊 The voice channel session in \`${connection.voiceChannel.name}\` has ended.` - }); - } - }); - } else if (member.id === connection.host) { - if (isWaiting.has(oldChannel.id)) return; - isWaiting.set(oldChannel.id, true); - const waitMessage = await client.rest.channels.createMessage(connection.originalChannel.id, { - content: "🔊 Waiting 10 seconds for the host to return..." - }); - const awaitRejoin = new AwaitRejoin(oldChannel, false, member.id); - awaitRejoin.once("end", async (rejoined) => { - isWaiting.delete(oldChannel.id); - if (rejoined) { - try { - if (waitMessage.channel.messages.has(waitMessage.id)) await waitMessage.delete(); - } catch { - logger.warn(`Failed to delete wait message ${waitMessage.id}`); - } - } else { - const members = oldChannel.voiceMembers.filter((i) => i.id !== client.user.id && !i.bot); - if (members.length === 0) { - try { - if (waitMessage.channel.messages.has(waitMessage.id)) await waitMessage.delete(); - } catch { - logger.warn(`Failed to delete wait message ${waitMessage.id}`); - } - try { - connection.player.node.leaveChannel(connection.originalChannel.guildID); - } catch { - logger.warn(`Failed to leave voice channel ${connection.originalChannel.guildID}`); - } - players.delete(connection.originalChannel.guildID); - queues.delete(connection.originalChannel.guildID); - skipVotes.delete(connection.originalChannel.guildID); - client.rest.channels.createMessage(connection.originalChannel.id, { - content: `🔊 The voice channel session in \`${connection.voiceChannel.name}\` has ended.` - }); - } else { - const randomMember = random(members); - players.set(connection.voiceChannel.guildID, { player: connection.player, type: connection.type, host: randomMember.id, voiceChannel: connection.voiceChannel, originalChannel: connection.originalChannel, loop: connection.loop, shuffle: connection.shuffle, playMessage: connection.playMessage }); - waitMessage.edit({ - content: `🔊 ${randomMember.mention} is the new voice channel host.` - }); - } - } - }); - } else if (member.id === client.user.id) { - isWaiting.delete(oldChannel.id); - try { - connection.player.node.leaveChannel(connection.originalChannel.guildID); - } catch { - logger.warn(`Failed to leave voice channel ${connection.originalChannel.guildID}`); - } - players.delete(connection.originalChannel.guildID); - queues.delete(connection.originalChannel.guildID); - skipVotes.delete(connection.originalChannel.guildID); - await client.rest.channels.createMessage(connection.originalChannel.id, { - content: `🔊 The voice channel session in \`${connection.voiceChannel.name}\` has ended.` - }); - } - } -}; diff --git a/events/voiceChannelSwitch.js b/events/voiceChannelSwitch.js deleted file mode 100644 index 3ab416e..0000000 --- a/events/voiceChannelSwitch.js +++ /dev/null @@ -1,5 +0,0 @@ -import leaveHandler from "./voiceChannelLeave.js"; - -export default async (client, member, newChannel, oldChannel) => { - await leaveHandler(client, member, oldChannel); -}; \ No newline at end of file diff --git a/events/warn.js b/events/warn.js index ca1556e..0a467f8 100644 --- a/events/warn.js +++ b/events/warn.js @@ -1,5 +1,5 @@ -import { warn } from "../utils/logger.js"; - -export default async (client, message) => { - warn(message); +import { warn } from "../utils/logger.js"; + +export default async (client, message) => { + warn(message); }; \ No newline at end of file diff --git a/mkdocs.yml b/mkdocs.yml index 7f68add..28bea5e 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -1,51 +1,51 @@ -site_name: esmBot -docs_dir: docs/ -repo_name: 'esmBot/esmBot' -repo_url: 'https://github.com/esmBot/esmBot' -copyright: Copyright © 2018 - 2023 Essem -nav: - - Home: index.md - - setup.md - - config.md - - custom-commands.md - - docker.md - - postgresql.md -markdown_extensions: - - def_list - - attr_list - - pymdownx.tasklist: - custom_checkbox: true - - admonition - - pymdownx.details - - pymdownx.superfences - - pymdownx.tabbed: - alternate_style: true -theme: - name: 'material' - features: - - toc.integrate - - content.tabs.link - logo: assets/esmbot.png - favicon: assets/esmbot.png - palette: - - media: "(prefers-color-scheme: light)" - scheme: default - toggle: - icon: material/weather-night - name: Switch to dark mode - - media: "(prefers-color-scheme: dark)" - scheme: slate - toggle: - icon: material/weather-sunny - name: Switch to light mode -plugins: - - glightbox - - git-revision-date-localized -extra: - social: - - icon: fontawesome/brands/mastodon - link: https://wetdry.world/@esmBot - - icon: fontawesome/brands/github - link: https://github.com/esmBot - - icon: fontawesome/brands/patreon +site_name: esmBot +docs_dir: docs/ +repo_name: 'esmBot/esmBot' +repo_url: 'https://github.com/esmBot/esmBot' +copyright: Copyright © 2018 - 2023 Essem +nav: + - Home: index.md + - setup.md + - config.md + - custom-commands.md + - docker.md + - postgresql.md +markdown_extensions: + - def_list + - attr_list + - pymdownx.tasklist: + custom_checkbox: true + - admonition + - pymdownx.details + - pymdownx.superfences + - pymdownx.tabbed: + alternate_style: true +theme: + name: 'material' + features: + - toc.integrate + - content.tabs.link + logo: assets/esmbot.png + favicon: assets/esmbot.png + palette: + - media: "(prefers-color-scheme: light)" + scheme: default + toggle: + icon: material/weather-night + name: Switch to dark mode + - media: "(prefers-color-scheme: dark)" + scheme: slate + toggle: + icon: material/weather-sunny + name: Switch to light mode +plugins: + - glightbox + - git-revision-date-localized +extra: + social: + - icon: fontawesome/brands/mastodon + link: https://wetdry.world/@esmBot + - icon: fontawesome/brands/github + link: https://github.com/esmBot + - icon: fontawesome/brands/patreon link: https://patreon.com/TheEssem \ No newline at end of file diff --git a/natives/blur.cc b/natives/blur.cc index 5347f8e..3d19460 100644 --- a/natives/blur.cc +++ b/natives/blur.cc @@ -1,31 +1,31 @@ -#include -#include -#include - -#include "common.h" - -using namespace std; -using namespace vips; - -char *Blur(string type, string *outType, char *BufferData, size_t BufferLength, - ArgumentMap Arguments, size_t *DataSize) { - bool sharp = GetArgument(Arguments, "sharp"); - VOption *options = VImage::option()->set("access", "sequential"); - - VImage in = - VImage::new_from_buffer(BufferData, BufferLength, "", - type == "gif" ? options->set("n", -1) : options) - .colourspace(VIPS_INTERPRETATION_sRGB); - - if (!in.has_alpha()) in = in.bandjoin(255); - - // TODO: find a better way to calculate the intensity for GIFs without - // splitting frames - VImage out = - sharp ? in.sharpen(VImage::option()->set("sigma", 3)) : in.gaussblur(15); - - void *buf; - out.write_to_buffer(("." + *outType).c_str(), &buf, DataSize); - - return (char *)buf; -} +#include +#include +#include + +#include "common.h" + +using namespace std; +using namespace vips; + +char *Blur(string type, string *outType, char *BufferData, size_t BufferLength, + ArgumentMap Arguments, size_t *DataSize) { + bool sharp = GetArgument(Arguments, "sharp"); + VOption *options = VImage::option()->set("access", "sequential"); + + VImage in = + VImage::new_from_buffer(BufferData, BufferLength, "", + type == "gif" ? options->set("n", -1) : options) + .colourspace(VIPS_INTERPRETATION_sRGB); + + if (!in.has_alpha()) in = in.bandjoin(255); + + // TODO: find a better way to calculate the intensity for GIFs without + // splitting frames + VImage out = + sharp ? in.sharpen(VImage::option()->set("sigma", 3)) : in.gaussblur(15); + + void *buf; + out.write_to_buffer(("." + *outType).c_str(), &buf, DataSize); + + return (char *)buf; +} diff --git a/natives/blur.h b/natives/blur.h index 4f4b96d..8645e77 100644 --- a/natives/blur.h +++ b/natives/blur.h @@ -1,8 +1,8 @@ -#pragma once - -#include "common.h" - -using std::string; - -char* Blur(string type, string* outType, char* BufferData, size_t BufferLength, +#pragma once + +#include "common.h" + +using std::string; + +char* Blur(string type, string* outType, char* BufferData, size_t BufferLength, ArgumentMap Arguments, size_t* DataSize); \ No newline at end of file diff --git a/natives/bounce.cc b/natives/bounce.cc index e0d27a7..74d15b1 100644 --- a/natives/bounce.cc +++ b/natives/bounce.cc @@ -1,51 +1,51 @@ -#include - -#include - -#include "common.h" - -using namespace std; -using namespace vips; - -char *Bounce(string type, string *outType, char *BufferData, - size_t BufferLength, [[maybe_unused]] ArgumentMap Arguments, - size_t *DataSize) { - VOption *options = VImage::option(); - - VImage in = - VImage::new_from_buffer( - BufferData, BufferLength, "", - type == "gif" ? options->set("n", -1)->set("access", "sequential") - : options) - .colourspace(VIPS_INTERPRETATION_sRGB); - if (!in.has_alpha()) in = in.bandjoin(255); - - int width = in.width(); - int pageHeight = vips_image_get_page_height(in.get_image()); - int nPages = type == "gif" ? vips_image_get_n_pages(in.get_image()) : 15; - double mult = M_PI / nPages; - int halfHeight = pageHeight / 2; - - vector img; - for (int i = 0; i < nPages; i++) { - VImage img_frame = - type == "gif" ? in.crop(0, i * pageHeight, width, pageHeight) : in; - double height = halfHeight * ((abs(sin(i * mult)) * -1) + 1); - VImage embedded = - img_frame.embed(0, height, width, pageHeight + halfHeight); - img.push_back(embedded); - } - VImage final = VImage::arrayjoin(img, VImage::option()->set("across", 1)); - final.set(VIPS_META_PAGE_HEIGHT, pageHeight + halfHeight); - if (type != "gif") { - vector delay(30, 50); - final.set("delay", delay); - } - - void *buf; - final.write_to_buffer(".gif", &buf, DataSize); - - *outType = "gif"; - - return (char *)buf; +#include + +#include + +#include "common.h" + +using namespace std; +using namespace vips; + +char *Bounce(string type, string *outType, char *BufferData, + size_t BufferLength, [[maybe_unused]] ArgumentMap Arguments, + size_t *DataSize) { + VOption *options = VImage::option(); + + VImage in = + VImage::new_from_buffer( + BufferData, BufferLength, "", + type == "gif" ? options->set("n", -1)->set("access", "sequential") + : options) + .colourspace(VIPS_INTERPRETATION_sRGB); + if (!in.has_alpha()) in = in.bandjoin(255); + + int width = in.width(); + int pageHeight = vips_image_get_page_height(in.get_image()); + int nPages = type == "gif" ? vips_image_get_n_pages(in.get_image()) : 15; + double mult = M_PI / nPages; + int halfHeight = pageHeight / 2; + + vector img; + for (int i = 0; i < nPages; i++) { + VImage img_frame = + type == "gif" ? in.crop(0, i * pageHeight, width, pageHeight) : in; + double height = halfHeight * ((abs(sin(i * mult)) * -1) + 1); + VImage embedded = + img_frame.embed(0, height, width, pageHeight + halfHeight); + img.push_back(embedded); + } + VImage final = VImage::arrayjoin(img, VImage::option()->set("across", 1)); + final.set(VIPS_META_PAGE_HEIGHT, pageHeight + halfHeight); + if (type != "gif") { + vector delay(30, 50); + final.set("delay", delay); + } + + void *buf; + final.write_to_buffer(".gif", &buf, DataSize); + + *outType = "gif"; + + return (char *)buf; } \ No newline at end of file diff --git a/natives/bounce.h b/natives/bounce.h index 0ea273a..ef4c53a 100644 --- a/natives/bounce.h +++ b/natives/bounce.h @@ -1,8 +1,8 @@ -#pragma once - -#include "common.h" - -using std::string; - -char* Bounce(string type, string* outType, char* BufferData, size_t BufferLength, +#pragma once + +#include "common.h" + +using std::string; + +char* Bounce(string type, string* outType, char* BufferData, size_t BufferLength, ArgumentMap Arguments, size_t* DataSize); \ No newline at end of file diff --git a/natives/caption.cc b/natives/caption.cc index 1979c4f..a9f9e45 100644 --- a/natives/caption.cc +++ b/natives/caption.cc @@ -1,77 +1,77 @@ -#include -#include - -#include "common.h" - -using namespace std; -using namespace vips; - -char *Caption(string type, string *outType, char *BufferData, - size_t BufferLength, ArgumentMap Arguments, size_t *DataSize) { - string caption = GetArgument(Arguments, "caption"); - string font = GetArgument(Arguments, "font"); - string basePath = GetArgument(Arguments, "basePath"); - - VOption *options = VImage::option()->set("access", "sequential"); - - VImage in = - VImage::new_from_buffer(BufferData, BufferLength, "", - type == "gif" ? options->set("n", -1) : options) - .colourspace(VIPS_INTERPRETATION_sRGB); - - if (!in.has_alpha()) in = in.bandjoin(255); - - int width = in.width(); - int size = width / 10; - int pageHeight = vips_image_get_page_height(in.get_image()); - int nPages = vips_image_get_n_pages(in.get_image()); - int textWidth = width - ((width / 25) * 2); - - string font_string = (font == "roboto" ? "Roboto Condensed" : font) + " " + - (font != "impact" ? "bold" : "normal") + " " + - to_string(size); - - string captionText = "" + caption + ""; - - VImage text; - auto findResult = fontPaths.find(font); - if (findResult != fontPaths.end()) { - text = VImage::text( - ".", VImage::option()->set("fontfile", - (basePath + findResult->second).c_str())); - } - text = VImage::text( - captionText.c_str(), - VImage::option() - ->set("rgba", true) - ->set("align", VIPS_ALIGN_CENTRE) - ->set("font", font_string.c_str()) - ->set("fontfile", (basePath + "assets/fonts/twemoji.otf").c_str()) - ->set("width", textWidth)); - VImage captionImage = - ((text == (vector){0, 0, 0, 0}).bandand()) - .ifthenelse(255, text) - .gravity(VIPS_COMPASS_DIRECTION_CENTRE, width, text.height() + size, - VImage::option()->set("extend", "white")); - - vector img; - for (int i = 0; i < nPages; i++) { - VImage img_frame = - type == "gif" ? in.crop(0, i * pageHeight, width, pageHeight) : in; - VImage frame = captionImage.join( - img_frame, VIPS_DIRECTION_VERTICAL, - VImage::option()->set("background", 0xffffff)->set("expand", true)); - img.push_back(frame); - } - VImage final = VImage::arrayjoin(img, VImage::option()->set("across", 1)); - final.set(VIPS_META_PAGE_HEIGHT, pageHeight + captionImage.height()); - - void *buf; - final.write_to_buffer( - ("." + *outType).c_str(), &buf, DataSize, - *outType == "gif" - ? VImage::option()->set("dither", 0)->set("reoptimise", 1) - : 0); - - return (char *)buf; -} +#include +#include + +#include "common.h" + +using namespace std; +using namespace vips; + +char *Caption(string type, string *outType, char *BufferData, + size_t BufferLength, ArgumentMap Arguments, size_t *DataSize) { + string caption = GetArgument(Arguments, "caption"); + string font = GetArgument(Arguments, "font"); + string basePath = GetArgument(Arguments, "basePath"); + + VOption *options = VImage::option()->set("access", "sequential"); + + VImage in = + VImage::new_from_buffer(BufferData, BufferLength, "", + type == "gif" ? options->set("n", -1) : options) + .colourspace(VIPS_INTERPRETATION_sRGB); + + if (!in.has_alpha()) in = in.bandjoin(255); + + int width = in.width(); + int size = width / 10; + int pageHeight = vips_image_get_page_height(in.get_image()); + int nPages = vips_image_get_n_pages(in.get_image()); + int textWidth = width - ((width / 25) * 2); + + string font_string = (font == "roboto" ? "Roboto Condensed" : font) + " " + + (font != "impact" ? "bold" : "normal") + " " + + to_string(size); + + string captionText = "" + caption + ""; + + VImage text; + auto findResult = fontPaths.find(font); + if (findResult != fontPaths.end()) { + text = VImage::text( + ".", VImage::option()->set("fontfile", + (basePath + findResult->second).c_str())); + } + text = VImage::text( + captionText.c_str(), + VImage::option() + ->set("rgba", true) + ->set("align", VIPS_ALIGN_CENTRE) + ->set("font", font_string.c_str()) + ->set("fontfile", (basePath + "assets/fonts/twemoji.otf").c_str()) + ->set("width", textWidth)); + VImage captionImage = + ((text == (vector){0, 0, 0, 0}).bandand()) + .ifthenelse(255, text) + .gravity(VIPS_COMPASS_DIRECTION_CENTRE, width, text.height() + size, + VImage::option()->set("extend", "white")); + + vector img; + for (int i = 0; i < nPages; i++) { + VImage img_frame = + type == "gif" ? in.crop(0, i * pageHeight, width, pageHeight) : in; + VImage frame = captionImage.join( + img_frame, VIPS_DIRECTION_VERTICAL, + VImage::option()->set("background", 0xffffff)->set("expand", true)); + img.push_back(frame); + } + VImage final = VImage::arrayjoin(img, VImage::option()->set("across", 1)); + final.set(VIPS_META_PAGE_HEIGHT, pageHeight + captionImage.height()); + + void *buf; + final.write_to_buffer( + ("." + *outType).c_str(), &buf, DataSize, + *outType == "gif" + ? VImage::option()->set("dither", 0)->set("reoptimise", 1) + : 0); + + return (char *)buf; +} diff --git a/natives/caption.h b/natives/caption.h index 6ff73c1..c8255ed 100644 --- a/natives/caption.h +++ b/natives/caption.h @@ -1,8 +1,8 @@ -#pragma once - -#include "common.h" - -using std::string; - -char* Caption(string type, string* outType, char* BufferData, size_t BufferLength, +#pragma once + +#include "common.h" + +using std::string; + +char* Caption(string type, string* outType, char* BufferData, size_t BufferLength, ArgumentMap Arguments, size_t* DataSize); \ No newline at end of file diff --git a/natives/caption2.cc b/natives/caption2.cc index 0b9026e..688f7f3 100644 --- a/natives/caption2.cc +++ b/natives/caption2.cc @@ -1,81 +1,81 @@ -#include -#include -#include - -#include "common.h" - -using namespace std; -using namespace vips; - -char *CaptionTwo(string type, string *outType, char *BufferData, - size_t BufferLength, ArgumentMap Arguments, size_t *DataSize) { - bool top = GetArgument(Arguments, "top"); - string caption = GetArgument(Arguments, "caption"); - string font = GetArgument(Arguments, "font"); - string basePath = GetArgument(Arguments, "basePath"); - - VOption *options = VImage::option()->set("access", "sequential"); - - VImage in = - VImage::new_from_buffer(BufferData, BufferLength, "", - type == "gif" ? options->set("n", -1) : options) - .colourspace(VIPS_INTERPRETATION_sRGB); - - if (!in.has_alpha()) in = in.bandjoin(255); - - int width = in.width(); - int size = width / 13; - int pageHeight = vips_image_get_page_height(in.get_image()); - int nPages = vips_image_get_n_pages(in.get_image()); - int textWidth = width - ((width / 25) * 2); - - string font_string = (font == "roboto" ? "Roboto Condensed" : font) + - ", Twemoji Color Emoji " + to_string(size); - - string captionText = "" + caption + ""; - - VImage text; - auto findResult = fontPaths.find(font); - if (findResult != fontPaths.end()) { - text = VImage::text( - ".", VImage::option()->set("fontfile", - (basePath + findResult->second).c_str())); - } - text = VImage::text( - captionText.c_str(), - VImage::option() - ->set("rgba", true) - ->set("font", font_string.c_str()) - ->set("fontfile", (basePath + "assets/fonts/twemoji.otf").c_str()) - ->set("align", VIPS_ALIGN_LOW) - ->set("width", textWidth)); - VImage captionImage = - ((text == (vector){0, 0, 0, 0}).bandand()) - .ifthenelse(255, text) - .embed(width / 25, width / 25, width, text.height() + size, - VImage::option()->set("extend", "white")); - - vector img; - for (int i = 0; i < nPages; i++) { - VImage img_frame = - type == "gif" ? in.crop(0, i * pageHeight, width, pageHeight) : in; - VImage frame = - (top ? captionImage : img_frame) - .join(top ? img_frame : captionImage, VIPS_DIRECTION_VERTICAL, - VImage::option() - ->set("background", 0xffffff) - ->set("expand", true)); - img.push_back(frame); - } - VImage final = VImage::arrayjoin(img, VImage::option()->set("across", 1)); - final.set(VIPS_META_PAGE_HEIGHT, pageHeight + captionImage.height()); - - void *buf; - final.write_to_buffer( - ("." + *outType).c_str(), &buf, DataSize, - *outType == "gif" - ? VImage::option()->set("dither", 0)->set("reoptimise", 1) - : 0); - - return (char *)buf; -} +#include +#include +#include + +#include "common.h" + +using namespace std; +using namespace vips; + +char *CaptionTwo(string type, string *outType, char *BufferData, + size_t BufferLength, ArgumentMap Arguments, size_t *DataSize) { + bool top = GetArgument(Arguments, "top"); + string caption = GetArgument(Arguments, "caption"); + string font = GetArgument(Arguments, "font"); + string basePath = GetArgument(Arguments, "basePath"); + + VOption *options = VImage::option()->set("access", "sequential"); + + VImage in = + VImage::new_from_buffer(BufferData, BufferLength, "", + type == "gif" ? options->set("n", -1) : options) + .colourspace(VIPS_INTERPRETATION_sRGB); + + if (!in.has_alpha()) in = in.bandjoin(255); + + int width = in.width(); + int size = width / 13; + int pageHeight = vips_image_get_page_height(in.get_image()); + int nPages = vips_image_get_n_pages(in.get_image()); + int textWidth = width - ((width / 25) * 2); + + string font_string = (font == "roboto" ? "Roboto Condensed" : font) + + ", Twemoji Color Emoji " + to_string(size); + + string captionText = "" + caption + ""; + + VImage text; + auto findResult = fontPaths.find(font); + if (findResult != fontPaths.end()) { + text = VImage::text( + ".", VImage::option()->set("fontfile", + (basePath + findResult->second).c_str())); + } + text = VImage::text( + captionText.c_str(), + VImage::option() + ->set("rgba", true) + ->set("font", font_string.c_str()) + ->set("fontfile", (basePath + "assets/fonts/twemoji.otf").c_str()) + ->set("align", VIPS_ALIGN_LOW) + ->set("width", textWidth)); + VImage captionImage = + ((text == (vector){0, 0, 0, 0}).bandand()) + .ifthenelse(255, text) + .embed(width / 25, width / 25, width, text.height() + size, + VImage::option()->set("extend", "white")); + + vector img; + for (int i = 0; i < nPages; i++) { + VImage img_frame = + type == "gif" ? in.crop(0, i * pageHeight, width, pageHeight) : in; + VImage frame = + (top ? captionImage : img_frame) + .join(top ? img_frame : captionImage, VIPS_DIRECTION_VERTICAL, + VImage::option() + ->set("background", 0xffffff) + ->set("expand", true)); + img.push_back(frame); + } + VImage final = VImage::arrayjoin(img, VImage::option()->set("across", 1)); + final.set(VIPS_META_PAGE_HEIGHT, pageHeight + captionImage.height()); + + void *buf; + final.write_to_buffer( + ("." + *outType).c_str(), &buf, DataSize, + *outType == "gif" + ? VImage::option()->set("dither", 0)->set("reoptimise", 1) + : 0); + + return (char *)buf; +} diff --git a/natives/caption2.h b/natives/caption2.h index 6ee4790..f20b0bb 100644 --- a/natives/caption2.h +++ b/natives/caption2.h @@ -1,8 +1,8 @@ -#pragma once - -#include "common.h" - -using std::string; - -char* CaptionTwo(string type, string* outType, char* BufferData, size_t BufferLength, +#pragma once + +#include "common.h" + +using std::string; + +char* CaptionTwo(string type, string* outType, char* BufferData, size_t BufferLength, ArgumentMap Arguments, size_t* DataSize); \ No newline at end of file diff --git a/natives/circle.cc b/natives/circle.cc index 96a1768..7a7bb34 100644 --- a/natives/circle.cc +++ b/natives/circle.cc @@ -1,54 +1,54 @@ -#include - -#include -#include -#include -#include -#include - -#include "common.h" - -using namespace std; -using namespace Magick; - -char *Circle(string type, string *outType, char *BufferData, - size_t BufferLength, [[maybe_unused]] ArgumentMap Arguments, - size_t *DataSize) { - Blob blob; - - list frames; - list coalesced; - list blurred; - try { - readImages(&frames, Blob(BufferData, BufferLength)); - } catch (Magick::WarningCoder &warning) { - cerr << "Coder Warning: " << warning.what() << endl; - } catch (Magick::Warning &warning) { - cerr << "Warning: " << warning.what() << endl; - } - coalesceImages(&coalesced, frames.begin(), frames.end()); - - for (Image &image : coalesced) { - image.rotationalBlur(10); - image.magick(*outType); - blurred.push_back(image); - } - - optimizeTransparency(blurred.begin(), blurred.end()); - - if (*outType == "gif") { - for (Image &image : blurred) { - image.quantizeDitherMethod(FloydSteinbergDitherMethod); - image.quantize(); - } - } - - writeImages(blurred.begin(), blurred.end(), &blob); - - *DataSize = blob.length(); - - // workaround because the data is tied to the blob - char *data = (char *)malloc(*DataSize); - memcpy(data, blob.data(), *DataSize); - return data; +#include + +#include +#include +#include +#include +#include + +#include "common.h" + +using namespace std; +using namespace Magick; + +char *Circle(string type, string *outType, char *BufferData, + size_t BufferLength, [[maybe_unused]] ArgumentMap Arguments, + size_t *DataSize) { + Blob blob; + + list frames; + list coalesced; + list blurred; + try { + readImages(&frames, Blob(BufferData, BufferLength)); + } catch (Magick::WarningCoder &warning) { + cerr << "Coder Warning: " << warning.what() << endl; + } catch (Magick::Warning &warning) { + cerr << "Warning: " << warning.what() << endl; + } + coalesceImages(&coalesced, frames.begin(), frames.end()); + + for (Image &image : coalesced) { + image.rotationalBlur(10); + image.magick(*outType); + blurred.push_back(image); + } + + optimizeTransparency(blurred.begin(), blurred.end()); + + if (*outType == "gif") { + for (Image &image : blurred) { + image.quantizeDitherMethod(FloydSteinbergDitherMethod); + image.quantize(); + } + } + + writeImages(blurred.begin(), blurred.end(), &blob); + + *DataSize = blob.length(); + + // workaround because the data is tied to the blob + char *data = (char *)malloc(*DataSize); + memcpy(data, blob.data(), *DataSize); + return data; } \ No newline at end of file diff --git a/natives/circle.h b/natives/circle.h index 4ee7f96..580b5d3 100644 --- a/natives/circle.h +++ b/natives/circle.h @@ -1,8 +1,8 @@ -#pragma once - -#include "common.h" - -using std::string; - -char* Circle(string type, string* outType, char* BufferData, size_t BufferLength, +#pragma once + +#include "common.h" + +using std::string; + +char* Circle(string type, string* outType, char* BufferData, size_t BufferLength, ArgumentMap Arguments, size_t* DataSize); \ No newline at end of file diff --git a/natives/cli/image.cc b/natives/cli/image.cc index 344d0bb..92fcd67 100644 --- a/natives/cli/image.cc +++ b/natives/cli/image.cc @@ -1,26 +1,26 @@ -#include -#include - -#include "../common.h" - -void showUsage(char *path) { - std::cout << "Usage: " << path << " operation [--arg=\"param\"] [...]" << std::endl; -} - -int main(int argc, char *argv[]) { - if (argc < 1 || - (argc == 1 && !strcmp(argv[1], "-h"))) { - showUsage(argv[0]); -#ifdef _WIN32 - system("PAUSE"); -#endif - return 1; - } - - char *op = argv[1]; - - //handleArguments(argc, argv); - - std::cout << "This does nothing yet, but it might in the future!" << std::endl; - return 0; +#include +#include + +#include "../common.h" + +void showUsage(char *path) { + std::cout << "Usage: " << path << " operation [--arg=\"param\"] [...]" << std::endl; +} + +int main(int argc, char *argv[]) { + if (argc < 1 || + (argc == 1 && !strcmp(argv[1], "-h"))) { + showUsage(argv[0]); +#ifdef _WIN32 + system("PAUSE"); +#endif + return 1; + } + + char *op = argv[1]; + + //handleArguments(argc, argv); + + std::cout << "This does nothing yet, but it might in the future!" << std::endl; + return 0; } \ No newline at end of file diff --git a/natives/colors.cc b/natives/colors.cc index b488600..d7b7b01 100644 --- a/natives/colors.cc +++ b/natives/colors.cc @@ -1,36 +1,36 @@ -#include -#include -#include - -#include "common.h" - -using namespace std; -using namespace vips; - -VImage sepia = VImage::new_matrixv(3, 3, 0.3588, 0.7044, 0.1368, 0.2990, 0.5870, - 0.1140, 0.2392, 0.4696, 0.0912); - -char *Colors(string type, string *outType, char *BufferData, - size_t BufferLength, ArgumentMap Arguments, size_t *DataSize) { - string color = GetArgument(Arguments, "color"); - - VOption *options = VImage::option()->set("access", "sequential"); - - VImage in = - VImage::new_from_buffer(BufferData, BufferLength, "", - type == "gif" ? options->set("n", -1) : options) - .colourspace(VIPS_INTERPRETATION_sRGB); - - VImage out; - - if (color == "grayscale") { - out = in.colourspace(VIPS_INTERPRETATION_B_W); - } else if (color == "sepia") { - out = in.extract_band(0, VImage::option()->set("n", 3)).recomb(sepia); - } - - void *buf; - out.write_to_buffer(("." + *outType).c_str(), &buf, DataSize); - - return (char *)buf; -} +#include +#include +#include + +#include "common.h" + +using namespace std; +using namespace vips; + +VImage sepia = VImage::new_matrixv(3, 3, 0.3588, 0.7044, 0.1368, 0.2990, 0.5870, + 0.1140, 0.2392, 0.4696, 0.0912); + +char *Colors(string type, string *outType, char *BufferData, + size_t BufferLength, ArgumentMap Arguments, size_t *DataSize) { + string color = GetArgument(Arguments, "color"); + + VOption *options = VImage::option()->set("access", "sequential"); + + VImage in = + VImage::new_from_buffer(BufferData, BufferLength, "", + type == "gif" ? options->set("n", -1) : options) + .colourspace(VIPS_INTERPRETATION_sRGB); + + VImage out; + + if (color == "grayscale") { + out = in.colourspace(VIPS_INTERPRETATION_B_W); + } else if (color == "sepia") { + out = in.extract_band(0, VImage::option()->set("n", 3)).recomb(sepia); + } + + void *buf; + out.write_to_buffer(("." + *outType).c_str(), &buf, DataSize); + + return (char *)buf; +} diff --git a/natives/colors.h b/natives/colors.h index 4d00d0b..4f8a50c 100644 --- a/natives/colors.h +++ b/natives/colors.h @@ -1,8 +1,8 @@ -#pragma once - -#include "common.h" - -using std::string; - -char* Colors(string type, string* outType, char* BufferData, size_t BufferLength, +#pragma once + +#include "common.h" + +using std::string; + +char* Colors(string type, string* outType, char* BufferData, size_t BufferLength, ArgumentMap Arguments, size_t* DataSize); \ No newline at end of file diff --git a/natives/common.h b/natives/common.h index 1d71efb..b051984 100644 --- a/natives/common.h +++ b/natives/common.h @@ -1,130 +1,130 @@ -#pragma once - -#include -#include -#include -#include - -using std::map; -using std::string; -using std::variant; - -typedef variant ArgumentVariant; -typedef map ArgumentMap; - -#include "blur.h" -#include "bounce.h" -#include "caption.h" -#include "caption2.h" -#include "circle.h" -#include "colors.h" -#include "crop.h" -#include "deepfry.h" -#include "explode.h" -#include "flag.h" -#include "flip.h" -#include "freeze.h" -#include "gamexplain.h" -#include "globe.h" -#include "homebrew.h" -#include "invert.h" -#include "jpeg.h" -#include "magik.h" -#include "meme.h" -#include "mirror.h" -#include "motivate.h" -#include "reddit.h" -#include "resize.h" -#include "reverse.h" -#include "scott.h" -#include "snapchat.h" -#include "sonic.h" -#include "speed.h" -#include "spin.h" -#include "squish.h" -#include "swirl.h" -#include "tile.h" -#include "togif.h" -#include "uncanny.h" -#include "uncaption.h" -#include "wall.h" -#include "watermark.h" -#include "whisper.h" -#include "zamn.h" - -template -T GetArgument(ArgumentMap map, string key) { - try { - return std::get(map.at(key)); - } catch (std::bad_variant_access&) { - throw "Invalid requested type from variant."; - } -} - -template -T GetArgumentWithFallback(ArgumentMap map, string key, T fallback) { - try { - return std::get(map.at(key)); - } catch (...) { // this is, not great... - return fallback; - } -} - -#define MAP_HAS(ARRAY, KEY) (ARRAY.count(KEY) > 0) -#define MAP_GET(ARRAY, KEY, TYPE) \ - (MAP_HAS(ARRAY, KEY) ? get(ARRAY.at(KEY)) \ - : NULL) // C++ has forced my hand -#define MAP_GET_FALLBACK(ARRAY, KEY, TYPE, FALLBACK) \ - (MAP_HAS(ARRAY, KEY) ? get(ARRAY.at(KEY)) : FALLBACK) - -#define ARG_TYPES std::variant - -const std::unordered_map fontPaths{ - {"futura", "assets/fonts/caption.otf"}, - {"helvetica", "assets/fonts/caption2.ttf"}, - {"roboto", "assets/fonts/reddit.ttf"}}; - -const std::map - FunctionMap = {{"blur", &Blur}, - {"bounce", &Bounce}, - {"caption", &Caption}, - {"captionTwo", &CaptionTwo}, - {"circle", &Circle}, - {"colors", &Colors}, - {"crop", &Crop}, - {"deepfry", &Deepfry}, - {"explode", &Explode}, - {"flag", &Flag}, - {"flip", &Flip}, - {"freeze", &Freeze}, - {"gamexplain", Gamexplain}, - {"globe", Globe}, - {"invert", Invert}, - {"jpeg", Jpeg}, - {"magik", Magik}, - {"meme", Meme}, - {"mirror", Mirror}, - {"motivate", Motivate}, - {"reddit", Reddit}, - {"resize", Resize}, - {"reverse", Reverse}, - {"scott", Scott}, - {"snapchat", Snapchat}, - {"speed", &Speed}, - {"spin", Spin}, - {"squish", Squish}, - {"swirl", Swirl}, - {"tile", Tile}, - {"togif", ToGif}, - {"uncanny", Uncanny}, - {"uncaption", &Uncaption}, - {"wall", Wall}, - {"watermark", &Watermark}, - {"whisper", Whisper}, - {"zamn", Zamn}}; - -const std::map +#pragma once + +#include +#include +#include +#include + +using std::map; +using std::string; +using std::variant; + +typedef variant ArgumentVariant; +typedef map ArgumentMap; + +#include "blur.h" +#include "bounce.h" +#include "caption.h" +#include "caption2.h" +#include "circle.h" +#include "colors.h" +#include "crop.h" +#include "deepfry.h" +#include "explode.h" +#include "flag.h" +#include "flip.h" +#include "freeze.h" +#include "gamexplain.h" +#include "globe.h" +#include "homebrew.h" +#include "invert.h" +#include "jpeg.h" +#include "magik.h" +#include "meme.h" +#include "mirror.h" +#include "motivate.h" +#include "reddit.h" +#include "resize.h" +#include "reverse.h" +#include "scott.h" +#include "snapchat.h" +#include "sonic.h" +#include "speed.h" +#include "spin.h" +#include "squish.h" +#include "swirl.h" +#include "tile.h" +#include "togif.h" +#include "uncanny.h" +#include "uncaption.h" +#include "wall.h" +#include "watermark.h" +#include "whisper.h" +#include "zamn.h" + +template +T GetArgument(ArgumentMap map, string key) { + try { + return std::get(map.at(key)); + } catch (std::bad_variant_access&) { + throw "Invalid requested type from variant."; + } +} + +template +T GetArgumentWithFallback(ArgumentMap map, string key, T fallback) { + try { + return std::get(map.at(key)); + } catch (...) { // this is, not great... + return fallback; + } +} + +#define MAP_HAS(ARRAY, KEY) (ARRAY.count(KEY) > 0) +#define MAP_GET(ARRAY, KEY, TYPE) \ + (MAP_HAS(ARRAY, KEY) ? get(ARRAY.at(KEY)) \ + : NULL) // C++ has forced my hand +#define MAP_GET_FALLBACK(ARRAY, KEY, TYPE, FALLBACK) \ + (MAP_HAS(ARRAY, KEY) ? get(ARRAY.at(KEY)) : FALLBACK) + +#define ARG_TYPES std::variant + +const std::unordered_map fontPaths{ + {"futura", "assets/fonts/caption.otf"}, + {"helvetica", "assets/fonts/caption2.ttf"}, + {"roboto", "assets/fonts/reddit.ttf"}}; + +const std::map + FunctionMap = {{"blur", &Blur}, + {"bounce", &Bounce}, + {"caption", &Caption}, + {"captionTwo", &CaptionTwo}, + {"circle", &Circle}, + {"colors", &Colors}, + {"crop", &Crop}, + {"deepfry", &Deepfry}, + {"explode", &Explode}, + {"flag", &Flag}, + {"flip", &Flip}, + {"freeze", &Freeze}, + {"gamexplain", Gamexplain}, + {"globe", Globe}, + {"invert", Invert}, + {"jpeg", Jpeg}, + {"magik", Magik}, + {"meme", Meme}, + {"mirror", Mirror}, + {"motivate", Motivate}, + {"reddit", Reddit}, + {"resize", Resize}, + {"reverse", Reverse}, + {"scott", Scott}, + {"snapchat", Snapchat}, + {"speed", &Speed}, + {"spin", Spin}, + {"squish", Squish}, + {"swirl", Swirl}, + {"tile", Tile}, + {"togif", ToGif}, + {"uncanny", Uncanny}, + {"uncaption", &Uncaption}, + {"wall", Wall}, + {"watermark", &Watermark}, + {"whisper", Whisper}, + {"zamn", Zamn}}; + +const std::map NoInputFunctionMap = {{"homebrew", Homebrew}, {"sonic", Sonic}}; \ No newline at end of file diff --git a/natives/crop.cc b/natives/crop.cc index 9faba5b..df720ea 100644 --- a/natives/crop.cc +++ b/natives/crop.cc @@ -1,51 +1,51 @@ -#include -#include -#include - -#include "common.h" - -using namespace std; -using namespace vips; - -char *Crop(string type, string *outType, char *BufferData, size_t BufferLength, - [[maybe_unused]] ArgumentMap Arguments, size_t *DataSize) { - VOption *options = VImage::option()->set("access", "sequential"); - - VImage in = - VImage::new_from_buffer(BufferData, BufferLength, "", - type == "gif" ? options->set("n", -1) : options) - .colourspace(VIPS_INTERPRETATION_sRGB); - - int width = in.width(); - int pageHeight = vips_image_get_page_height(in.get_image()); - int nPages = vips_image_get_n_pages(in.get_image()); - - vector img; - int finalHeight = 0; - for (int i = 0; i < nPages; i++) { - VImage img_frame = - type == "gif" ? in.crop(0, i * pageHeight, width, pageHeight) : in; - int frameWidth = img_frame.width(); - int frameHeight = img_frame.height(); - bool widthOrHeight = frameWidth / frameHeight >= 1; - int size = widthOrHeight ? frameHeight : frameWidth; - // img_frame.crop(frameWidth - size, frameHeight - size, size, size); - VImage result = img_frame.smartcrop( - size, size, - VImage::option()->set("interesting", VIPS_INTERESTING_CENTRE)); - finalHeight = size; - img.push_back(result); - } - - VImage final = VImage::arrayjoin(img, VImage::option()->set("across", 1)); - final.set(VIPS_META_PAGE_HEIGHT, finalHeight); - - void *buf; - final.write_to_buffer( - ("." + *outType).c_str(), &buf, DataSize, - *outType == "gif" - ? VImage::option()->set("dither", 0)->set("reoptimise", 1) - : 0); - - return (char *)buf; +#include +#include +#include + +#include "common.h" + +using namespace std; +using namespace vips; + +char *Crop(string type, string *outType, char *BufferData, size_t BufferLength, + [[maybe_unused]] ArgumentMap Arguments, size_t *DataSize) { + VOption *options = VImage::option()->set("access", "sequential"); + + VImage in = + VImage::new_from_buffer(BufferData, BufferLength, "", + type == "gif" ? options->set("n", -1) : options) + .colourspace(VIPS_INTERPRETATION_sRGB); + + int width = in.width(); + int pageHeight = vips_image_get_page_height(in.get_image()); + int nPages = vips_image_get_n_pages(in.get_image()); + + vector img; + int finalHeight = 0; + for (int i = 0; i < nPages; i++) { + VImage img_frame = + type == "gif" ? in.crop(0, i * pageHeight, width, pageHeight) : in; + int frameWidth = img_frame.width(); + int frameHeight = img_frame.height(); + bool widthOrHeight = frameWidth / frameHeight >= 1; + int size = widthOrHeight ? frameHeight : frameWidth; + // img_frame.crop(frameWidth - size, frameHeight - size, size, size); + VImage result = img_frame.smartcrop( + size, size, + VImage::option()->set("interesting", VIPS_INTERESTING_CENTRE)); + finalHeight = size; + img.push_back(result); + } + + VImage final = VImage::arrayjoin(img, VImage::option()->set("across", 1)); + final.set(VIPS_META_PAGE_HEIGHT, finalHeight); + + void *buf; + final.write_to_buffer( + ("." + *outType).c_str(), &buf, DataSize, + *outType == "gif" + ? VImage::option()->set("dither", 0)->set("reoptimise", 1) + : 0); + + return (char *)buf; } \ No newline at end of file diff --git a/natives/crop.h b/natives/crop.h index c7cad96..4c5270c 100644 --- a/natives/crop.h +++ b/natives/crop.h @@ -1,8 +1,8 @@ -#pragma once - -#include "common.h" - -using std::string; - -char* Crop(string type, string* outType, char* BufferData, size_t BufferLength, +#pragma once + +#include "common.h" + +using std::string; + +char* Crop(string type, string* outType, char* BufferData, size_t BufferLength, ArgumentMap Arguments, size_t* DataSize); \ No newline at end of file diff --git a/natives/deepfry.cc b/natives/deepfry.cc index 9fe72a0..4d2cda9 100644 --- a/natives/deepfry.cc +++ b/natives/deepfry.cc @@ -1,61 +1,61 @@ -#include -#include - -#include "common.h" - -using namespace std; -using namespace vips; - -char *Deepfry(string type, string *outType, char *BufferData, - size_t BufferLength, [[maybe_unused]] ArgumentMap Arguments, - size_t *DataSize) { - VOption *options = VImage::option()->set("access", "sequential"); - - VImage in = - VImage::new_from_buffer(BufferData, BufferLength, "", - type == "gif" ? options->set("n", -1) : options) - .colourspace(VIPS_INTERPRETATION_sRGB); - - if (!in.has_alpha()) in = in.bandjoin(255); - - int width = in.width(); - int pageHeight = vips_image_get_page_height(in.get_image()); - int totalHeight = in.height(); - int nPages = vips_image_get_n_pages(in.get_image()); - - VImage fried = (in * 1.3 - (255.0 * 1.3 - 255.0)) * 1.5; - - VImage final; - if (totalHeight > 65500 && type == "gif") { - vector img; - for (int i = 0; i < nPages; i++) { - VImage img_frame = in.crop(0, i * pageHeight, width, pageHeight); - void *jpgBuf; - size_t jpgLength; - img_frame.write_to_buffer( - ".jpg", &jpgBuf, &jpgLength, - VImage::option()->set("Q", 1)->set("strip", true)); - VImage jpeged = VImage::new_from_buffer(jpgBuf, jpgLength, ""); - jpeged.set(VIPS_META_PAGE_HEIGHT, pageHeight); - jpeged.set("delay", in.get_array_int("delay")); - img.push_back(jpeged); - } - final = VImage::arrayjoin(img, VImage::option()->set("across", 1)); - final.set(VIPS_META_PAGE_HEIGHT, pageHeight); - } else { - void *jpgBuf; - size_t jpgLength; - fried.write_to_buffer(".jpg", &jpgBuf, &jpgLength, - VImage::option()->set("Q", 1)->set("strip", true)); - final = VImage::new_from_buffer(jpgBuf, jpgLength, ""); - final.set(VIPS_META_PAGE_HEIGHT, pageHeight); - if (type == "gif") final.set("delay", fried.get_array_int("delay")); - } - - void *buf; - final.write_to_buffer( - ("." + *outType).c_str(), &buf, DataSize, - *outType == "gif" ? VImage::option()->set("dither", 0) : 0); - - return (char *)buf; +#include +#include + +#include "common.h" + +using namespace std; +using namespace vips; + +char *Deepfry(string type, string *outType, char *BufferData, + size_t BufferLength, [[maybe_unused]] ArgumentMap Arguments, + size_t *DataSize) { + VOption *options = VImage::option()->set("access", "sequential"); + + VImage in = + VImage::new_from_buffer(BufferData, BufferLength, "", + type == "gif" ? options->set("n", -1) : options) + .colourspace(VIPS_INTERPRETATION_sRGB); + + if (!in.has_alpha()) in = in.bandjoin(255); + + int width = in.width(); + int pageHeight = vips_image_get_page_height(in.get_image()); + int totalHeight = in.height(); + int nPages = vips_image_get_n_pages(in.get_image()); + + VImage fried = (in * 1.3 - (255.0 * 1.3 - 255.0)) * 1.5; + + VImage final; + if (totalHeight > 65500 && type == "gif") { + vector img; + for (int i = 0; i < nPages; i++) { + VImage img_frame = in.crop(0, i * pageHeight, width, pageHeight); + void *jpgBuf; + size_t jpgLength; + img_frame.write_to_buffer( + ".jpg", &jpgBuf, &jpgLength, + VImage::option()->set("Q", 1)->set("strip", true)); + VImage jpeged = VImage::new_from_buffer(jpgBuf, jpgLength, ""); + jpeged.set(VIPS_META_PAGE_HEIGHT, pageHeight); + jpeged.set("delay", in.get_array_int("delay")); + img.push_back(jpeged); + } + final = VImage::arrayjoin(img, VImage::option()->set("across", 1)); + final.set(VIPS_META_PAGE_HEIGHT, pageHeight); + } else { + void *jpgBuf; + size_t jpgLength; + fried.write_to_buffer(".jpg", &jpgBuf, &jpgLength, + VImage::option()->set("Q", 1)->set("strip", true)); + final = VImage::new_from_buffer(jpgBuf, jpgLength, ""); + final.set(VIPS_META_PAGE_HEIGHT, pageHeight); + if (type == "gif") final.set("delay", fried.get_array_int("delay")); + } + + void *buf; + final.write_to_buffer( + ("." + *outType).c_str(), &buf, DataSize, + *outType == "gif" ? VImage::option()->set("dither", 0) : 0); + + return (char *)buf; } \ No newline at end of file diff --git a/natives/deepfry.h b/natives/deepfry.h index 9196438..b7d9057 100644 --- a/natives/deepfry.h +++ b/natives/deepfry.h @@ -1,8 +1,8 @@ -#pragma once - -#include "common.h" - -using std::string; - -char* Deepfry(string type, string* outType, char* BufferData, size_t BufferLength, +#pragma once + +#include "common.h" + +using std::string; + +char* Deepfry(string type, string* outType, char* BufferData, size_t BufferLength, ArgumentMap Arguments, size_t* DataSize); \ No newline at end of file diff --git a/natives/explode.cc b/natives/explode.cc index bc167e0..ae9ba41 100644 --- a/natives/explode.cc +++ b/natives/explode.cc @@ -1,52 +1,52 @@ -#include - -#include "common.h" - -using namespace std; -using namespace vips; - -char *Explode(string type, string *outType, char *BufferData, - size_t BufferLength, ArgumentMap Arguments, size_t *DataSize) { - bool implode = GetArgument(Arguments, "implode"); - string basePath = GetArgument(Arguments, "basePath"); - - VOption *options = VImage::option(); - - VImage in = - VImage::new_from_buffer( - BufferData, BufferLength, "", - type == "gif" ? options->set("n", -1)->set("access", "sequential") - : options) - .colourspace(VIPS_INTERPRETATION_sRGB); - if (!in.has_alpha()) in = in.bandjoin(255); - - int width = in.width(); - int pageHeight = vips_image_get_page_height(in.get_image()); - int nPages = vips_image_get_n_pages(in.get_image()); - - string distortPath = basePath + "assets/images/" + - (implode ? "linearimplode.png" : "linearexplode.png"); - VImage distort = - (VImage::new_from_file(distortPath.c_str()) - .resize(width / 500.0, VImage::option() - ->set("vscale", pageHeight / 500.0) - ->set("kernel", VIPS_KERNEL_CUBIC)) / - 65535); - - VImage distortImage = (distort[0] * width).bandjoin(distort[1] * pageHeight); - - vector img; - for (int i = 0; i < nPages; i++) { - VImage img_frame = - type == "gif" ? in.crop(0, i * pageHeight, width, pageHeight) : in; - VImage mapped = img_frame.mapim(distortImage); - img.push_back(mapped); - } - VImage final = VImage::arrayjoin(img, VImage::option()->set("across", 1)); - final.set(VIPS_META_PAGE_HEIGHT, pageHeight); - - void *buf; - final.write_to_buffer(("." + *outType).c_str(), &buf, DataSize); - - return (char *)buf; +#include + +#include "common.h" + +using namespace std; +using namespace vips; + +char *Explode(string type, string *outType, char *BufferData, + size_t BufferLength, ArgumentMap Arguments, size_t *DataSize) { + bool implode = GetArgument(Arguments, "implode"); + string basePath = GetArgument(Arguments, "basePath"); + + VOption *options = VImage::option(); + + VImage in = + VImage::new_from_buffer( + BufferData, BufferLength, "", + type == "gif" ? options->set("n", -1)->set("access", "sequential") + : options) + .colourspace(VIPS_INTERPRETATION_sRGB); + if (!in.has_alpha()) in = in.bandjoin(255); + + int width = in.width(); + int pageHeight = vips_image_get_page_height(in.get_image()); + int nPages = vips_image_get_n_pages(in.get_image()); + + string distortPath = basePath + "assets/images/" + + (implode ? "linearimplode.png" : "linearexplode.png"); + VImage distort = + (VImage::new_from_file(distortPath.c_str()) + .resize(width / 500.0, VImage::option() + ->set("vscale", pageHeight / 500.0) + ->set("kernel", VIPS_KERNEL_CUBIC)) / + 65535); + + VImage distortImage = (distort[0] * width).bandjoin(distort[1] * pageHeight); + + vector img; + for (int i = 0; i < nPages; i++) { + VImage img_frame = + type == "gif" ? in.crop(0, i * pageHeight, width, pageHeight) : in; + VImage mapped = img_frame.mapim(distortImage); + img.push_back(mapped); + } + VImage final = VImage::arrayjoin(img, VImage::option()->set("across", 1)); + final.set(VIPS_META_PAGE_HEIGHT, pageHeight); + + void *buf; + final.write_to_buffer(("." + *outType).c_str(), &buf, DataSize); + + return (char *)buf; } \ No newline at end of file diff --git a/natives/explode.h b/natives/explode.h index 3c07169..2915bb1 100644 --- a/natives/explode.h +++ b/natives/explode.h @@ -1,8 +1,8 @@ -#pragma once - -#include "common.h" - -using std::string; - -char* Explode(string type, string* outType, char* BufferData, size_t BufferLength, +#pragma once + +#include "common.h" + +using std::string; + +char* Explode(string type, string* outType, char* BufferData, size_t BufferLength, ArgumentMap Arguments, size_t* DataSize); \ No newline at end of file diff --git a/natives/flag.cc b/natives/flag.cc index adfaeb2..4406b2f 100644 --- a/natives/flag.cc +++ b/natives/flag.cc @@ -1,49 +1,49 @@ -#include - -#include "common.h" - -using namespace std; -using namespace vips; - -char *Flag(string type, string *outType, char *BufferData, size_t BufferLength, - ArgumentMap Arguments, size_t *DataSize) { - string overlay = GetArgument(Arguments, "overlay"); - string basePath = GetArgument(Arguments, "basePath"); - - VOption *options = VImage::option()->set("access", "sequential"); - - VImage in = - VImage::new_from_buffer(BufferData, BufferLength, "", - type == "gif" ? options->set("n", -1) : options) - .colourspace(VIPS_INTERPRETATION_sRGB); - - if (!in.has_alpha()) in = in.bandjoin(255); - - int width = in.width(); - int pageHeight = vips_image_get_page_height(in.get_image()); - int nPages = vips_image_get_n_pages(in.get_image()); - - string assetPath = basePath + overlay; - VImage overlayInput = VImage::new_from_file(assetPath.c_str()); - VImage overlayImage = overlayInput.resize( - (double)width / (double)overlayInput.width(), - VImage::option()->set( - "vscale", (double)pageHeight / (double)overlayInput.height())); - if (!overlayImage.has_alpha()) { - overlayImage = overlayImage.bandjoin(127); - } else { - // this is a pretty cool line, just saying - overlayImage = overlayImage * vector{1, 1, 1, 0.5}; - } - VImage replicated = overlayImage.replicate(1, nPages); - VImage final = in.composite2(replicated, VIPS_BLEND_MODE_OVER); - - void *buf; - final.write_to_buffer( - ("." + *outType).c_str(), &buf, DataSize, - *outType == "gif" - ? VImage::option()->set("dither", 0)->set("reoptimise", 1) - : 0); - - return (char *)buf; +#include + +#include "common.h" + +using namespace std; +using namespace vips; + +char *Flag(string type, string *outType, char *BufferData, size_t BufferLength, + ArgumentMap Arguments, size_t *DataSize) { + string overlay = GetArgument(Arguments, "overlay"); + string basePath = GetArgument(Arguments, "basePath"); + + VOption *options = VImage::option()->set("access", "sequential"); + + VImage in = + VImage::new_from_buffer(BufferData, BufferLength, "", + type == "gif" ? options->set("n", -1) : options) + .colourspace(VIPS_INTERPRETATION_sRGB); + + if (!in.has_alpha()) in = in.bandjoin(255); + + int width = in.width(); + int pageHeight = vips_image_get_page_height(in.get_image()); + int nPages = vips_image_get_n_pages(in.get_image()); + + string assetPath = basePath + overlay; + VImage overlayInput = VImage::new_from_file(assetPath.c_str()); + VImage overlayImage = overlayInput.resize( + (double)width / (double)overlayInput.width(), + VImage::option()->set( + "vscale", (double)pageHeight / (double)overlayInput.height())); + if (!overlayImage.has_alpha()) { + overlayImage = overlayImage.bandjoin(127); + } else { + // this is a pretty cool line, just saying + overlayImage = overlayImage * vector{1, 1, 1, 0.5}; + } + VImage replicated = overlayImage.replicate(1, nPages); + VImage final = in.composite2(replicated, VIPS_BLEND_MODE_OVER); + + void *buf; + final.write_to_buffer( + ("." + *outType).c_str(), &buf, DataSize, + *outType == "gif" + ? VImage::option()->set("dither", 0)->set("reoptimise", 1) + : 0); + + return (char *)buf; } \ No newline at end of file diff --git a/natives/flag.h b/natives/flag.h index 123aa73..ff33d87 100644 --- a/natives/flag.h +++ b/natives/flag.h @@ -1,8 +1,8 @@ -#pragma once - -#include "common.h" - -using std::string; - -char* Flag(string type, string* outType, char* BufferData, size_t BufferLength, +#pragma once + +#include "common.h" + +using std::string; + +char* Flag(string type, string* outType, char* BufferData, size_t BufferLength, ArgumentMap Arguments, size_t* DataSize); \ No newline at end of file diff --git a/natives/flip.cc b/natives/flip.cc index 1344404..4c23e6c 100644 --- a/natives/flip.cc +++ b/natives/flip.cc @@ -1,48 +1,48 @@ -#include -#include - -#include "common.h" - -using namespace std; -using namespace vips; - -char *Flip(string type, string *outType, char *BufferData, size_t BufferLength, - ArgumentMap Arguments, size_t *DataSize) { - bool flop = GetArgument(Arguments, "flop"); - - VImage in = VImage::new_from_buffer(BufferData, BufferLength, "", - type == "gif" - ? VImage::option()->set("n", -1)->set( - "access", "sequential") - : 0) - .colourspace(VIPS_INTERPRETATION_sRGB); - if (!in.has_alpha()) in = in.bandjoin(255); - - VImage out; - if (flop) { - out = in.flip(VIPS_DIRECTION_HORIZONTAL); - } else if (type == "gif") { - // libvips gif handling is both a blessing and a curse - vector img; - int pageHeight = vips_image_get_page_height(in.get_image()); - int nPages = vips_image_get_n_pages(in.get_image()); - for (int i = 0; i < nPages; i++) { - VImage img_frame = in.crop(0, i * pageHeight, in.width(), pageHeight); - VImage flipped = img_frame.flip(VIPS_DIRECTION_VERTICAL); - img.push_back(flipped); - } - out = VImage::arrayjoin(img, VImage::option()->set("across", 1)); - out.set(VIPS_META_PAGE_HEIGHT, pageHeight); - } else { - out = in.flip(VIPS_DIRECTION_VERTICAL); - } - - void *buf; - out.write_to_buffer( - ("." + *outType).c_str(), &buf, DataSize, - *outType == "gif" - ? VImage::option()->set("dither", 0)->set("reoptimise", 1) - : 0); - - return (char *)buf; +#include +#include + +#include "common.h" + +using namespace std; +using namespace vips; + +char *Flip(string type, string *outType, char *BufferData, size_t BufferLength, + ArgumentMap Arguments, size_t *DataSize) { + bool flop = GetArgument(Arguments, "flop"); + + VImage in = VImage::new_from_buffer(BufferData, BufferLength, "", + type == "gif" + ? VImage::option()->set("n", -1)->set( + "access", "sequential") + : 0) + .colourspace(VIPS_INTERPRETATION_sRGB); + if (!in.has_alpha()) in = in.bandjoin(255); + + VImage out; + if (flop) { + out = in.flip(VIPS_DIRECTION_HORIZONTAL); + } else if (type == "gif") { + // libvips gif handling is both a blessing and a curse + vector img; + int pageHeight = vips_image_get_page_height(in.get_image()); + int nPages = vips_image_get_n_pages(in.get_image()); + for (int i = 0; i < nPages; i++) { + VImage img_frame = in.crop(0, i * pageHeight, in.width(), pageHeight); + VImage flipped = img_frame.flip(VIPS_DIRECTION_VERTICAL); + img.push_back(flipped); + } + out = VImage::arrayjoin(img, VImage::option()->set("across", 1)); + out.set(VIPS_META_PAGE_HEIGHT, pageHeight); + } else { + out = in.flip(VIPS_DIRECTION_VERTICAL); + } + + void *buf; + out.write_to_buffer( + ("." + *outType).c_str(), &buf, DataSize, + *outType == "gif" + ? VImage::option()->set("dither", 0)->set("reoptimise", 1) + : 0); + + return (char *)buf; } \ No newline at end of file diff --git a/natives/flip.h b/natives/flip.h index d9ff128..c3327c8 100644 --- a/natives/flip.h +++ b/natives/flip.h @@ -1,8 +1,8 @@ -#pragma once - -#include "common.h" - -using std::string; - -char* Flip(string type, string* outType, char* BufferData, size_t BufferLength, +#pragma once + +#include "common.h" + +using std::string; + +char* Flip(string type, string* outType, char* BufferData, size_t BufferLength, ArgumentMap Arguments, size_t* DataSize); \ No newline at end of file diff --git a/natives/freeze.cc b/natives/freeze.cc index 2eb15c4..2fcb4a8 100644 --- a/natives/freeze.cc +++ b/natives/freeze.cc @@ -1,83 +1,83 @@ -#include -#include -#include - -#include "common.h" - -using namespace std; -using namespace vips; - -char *Freeze(string type, string *outType, char *BufferData, - size_t BufferLength, ArgumentMap Arguments, size_t *DataSize) { - bool loop = GetArgumentWithFallback(Arguments, "loop", false); - int frame = GetArgumentWithFallback(Arguments, "frame", -1); - - char *fileData = (char *)malloc(BufferLength); - memcpy(fileData, BufferData, BufferLength); - - char *match = (char *)"\x21\xFF\x0BNETSCAPE2.0\x03\x01"; - char *descriptor = (char *)"\x2C\x00\x00\x00\x00"; - char *lastPos; - - bool none = true; - - if (loop) { - char *newData = (char *)malloc(BufferLength + 19); - memcpy(newData, fileData, BufferLength); - lastPos = (char *)memchr(newData, '\x2C', BufferLength); - while (lastPos != NULL) { - if (memcmp(lastPos, descriptor, 5) != 0) { - lastPos = (char *)memchr(lastPos + 1, '\x2C', - (BufferLength - (lastPos - newData)) - 1); - continue; - } - - memcpy(lastPos + 19, lastPos, (BufferLength - (lastPos - newData))); - memcpy(lastPos, match, 16); - memcpy(lastPos + 16, "\x00\x00\x00", 3); - - none = false; - *DataSize = BufferLength + 19; - break; - } - if (none) *DataSize = BufferLength; - - return newData; - } else if (frame >= 0 && !loop) { - VOption *options = VImage::option()->set("access", "sequential"); - - VImage in = - VImage::new_from_buffer(BufferData, BufferLength, "", - type == "gif" ? options->set("n", -1) : options) - .colourspace(VIPS_INTERPRETATION_sRGB); - if (!in.has_alpha()) in = in.bandjoin(255); - - int pageHeight = vips_image_get_page_height(in.get_image()); - int nPages = vips_image_get_n_pages(in.get_image()); - int framePos = clamp(frame, 0, (int)nPages); - VImage out = in.crop(0, 0, in.width(), pageHeight * (framePos + 1)); - out.set(VIPS_META_PAGE_HEIGHT, pageHeight); - out.set("loop", 1); - - void *buf; - out.write_to_buffer(("." + *outType).c_str(), &buf, DataSize); - - return (char *)buf; - } else { - lastPos = (char *)memchr(fileData, '\x21', BufferLength); - while (lastPos != NULL) { - if (memcmp(lastPos, match, 16) != 0) { - lastPos = (char *)memchr(lastPos + 1, '\x21', - (BufferLength - (lastPos - fileData)) - 1); - continue; - } - memcpy(lastPos, lastPos + 19, (BufferLength - (lastPos - fileData)) - 19); - *DataSize = BufferLength - 19; - none = false; - break; - } - if (none) *DataSize = BufferLength; - - return fileData; - } -} +#include +#include +#include + +#include "common.h" + +using namespace std; +using namespace vips; + +char *Freeze(string type, string *outType, char *BufferData, + size_t BufferLength, ArgumentMap Arguments, size_t *DataSize) { + bool loop = GetArgumentWithFallback(Arguments, "loop", false); + int frame = GetArgumentWithFallback(Arguments, "frame", -1); + + char *fileData = (char *)malloc(BufferLength); + memcpy(fileData, BufferData, BufferLength); + + char *match = (char *)"\x21\xFF\x0BNETSCAPE2.0\x03\x01"; + char *descriptor = (char *)"\x2C\x00\x00\x00\x00"; + char *lastPos; + + bool none = true; + + if (loop) { + char *newData = (char *)malloc(BufferLength + 19); + memcpy(newData, fileData, BufferLength); + lastPos = (char *)memchr(newData, '\x2C', BufferLength); + while (lastPos != NULL) { + if (memcmp(lastPos, descriptor, 5) != 0) { + lastPos = (char *)memchr(lastPos + 1, '\x2C', + (BufferLength - (lastPos - newData)) - 1); + continue; + } + + memcpy(lastPos + 19, lastPos, (BufferLength - (lastPos - newData))); + memcpy(lastPos, match, 16); + memcpy(lastPos + 16, "\x00\x00\x00", 3); + + none = false; + *DataSize = BufferLength + 19; + break; + } + if (none) *DataSize = BufferLength; + + return newData; + } else if (frame >= 0 && !loop) { + VOption *options = VImage::option()->set("access", "sequential"); + + VImage in = + VImage::new_from_buffer(BufferData, BufferLength, "", + type == "gif" ? options->set("n", -1) : options) + .colourspace(VIPS_INTERPRETATION_sRGB); + if (!in.has_alpha()) in = in.bandjoin(255); + + int pageHeight = vips_image_get_page_height(in.get_image()); + int nPages = vips_image_get_n_pages(in.get_image()); + int framePos = clamp(frame, 0, (int)nPages); + VImage out = in.crop(0, 0, in.width(), pageHeight * (framePos + 1)); + out.set(VIPS_META_PAGE_HEIGHT, pageHeight); + out.set("loop", 1); + + void *buf; + out.write_to_buffer(("." + *outType).c_str(), &buf, DataSize); + + return (char *)buf; + } else { + lastPos = (char *)memchr(fileData, '\x21', BufferLength); + while (lastPos != NULL) { + if (memcmp(lastPos, match, 16) != 0) { + lastPos = (char *)memchr(lastPos + 1, '\x21', + (BufferLength - (lastPos - fileData)) - 1); + continue; + } + memcpy(lastPos, lastPos + 19, (BufferLength - (lastPos - fileData)) - 19); + *DataSize = BufferLength - 19; + none = false; + break; + } + if (none) *DataSize = BufferLength; + + return fileData; + } +} diff --git a/natives/freeze.h b/natives/freeze.h index 4c0e931..7c3d7ef 100644 --- a/natives/freeze.h +++ b/natives/freeze.h @@ -1,8 +1,8 @@ -#pragma once - -#include "common.h" - -using std::string; - -char* Freeze(string type, string* outType, char* BufferData, size_t BufferLength, +#pragma once + +#include "common.h" + +using std::string; + +char* Freeze(string type, string* outType, char* BufferData, size_t BufferLength, ArgumentMap Arguments, size_t* DataSize); \ No newline at end of file diff --git a/natives/gamexplain.cc b/natives/gamexplain.cc index a05b371..c2d9986 100644 --- a/natives/gamexplain.cc +++ b/natives/gamexplain.cc @@ -1,50 +1,50 @@ -#include - -#include "common.h" - -using namespace std; -using namespace vips; - -char *Gamexplain(string type, string *outType, char *BufferData, - size_t BufferLength, ArgumentMap Arguments, size_t *DataSize) { - string basePath = GetArgument(Arguments, "basePath"); - - VOption *options = VImage::option()->set("access", "sequential"); - - VImage in = - VImage::new_from_buffer(BufferData, BufferLength, "", - type == "gif" ? options->set("n", -1) : options) - .colourspace(VIPS_INTERPRETATION_sRGB); - if (!in.has_alpha()) in = in.bandjoin(255); - - string assetPath = basePath + "assets/images/gamexplain.png"; - VImage tmpl = VImage::new_from_file(assetPath.c_str()); - - int width = in.width(); - int pageHeight = vips_image_get_page_height(in.get_image()); - int nPages = vips_image_get_n_pages(in.get_image()); - - vector img; - for (int i = 0; i < nPages; i++) { - VImage img_frame = - type == "gif" ? in.crop(0, i * pageHeight, width, pageHeight) : in; - VImage resized = - img_frame - .resize(1181.0 / (double)width, - VImage::option()->set("vscale", 571.0 / (double)pageHeight)) - .embed(10, 92, 1200, 675, VImage::option()->set("extend", "white")); - VImage composited = resized.composite2(tmpl, VIPS_BLEND_MODE_OVER); - img.push_back(composited); - } - VImage final = VImage::arrayjoin(img, VImage::option()->set("across", 1)); - final.set(VIPS_META_PAGE_HEIGHT, 675); - - void *buf; - final.write_to_buffer( - ("." + *outType).c_str(), &buf, DataSize, - *outType == "gif" - ? VImage::option()->set("dither", 0)->set("reoptimise", 1) - : 0); - - return (char *)buf; +#include + +#include "common.h" + +using namespace std; +using namespace vips; + +char *Gamexplain(string type, string *outType, char *BufferData, + size_t BufferLength, ArgumentMap Arguments, size_t *DataSize) { + string basePath = GetArgument(Arguments, "basePath"); + + VOption *options = VImage::option()->set("access", "sequential"); + + VImage in = + VImage::new_from_buffer(BufferData, BufferLength, "", + type == "gif" ? options->set("n", -1) : options) + .colourspace(VIPS_INTERPRETATION_sRGB); + if (!in.has_alpha()) in = in.bandjoin(255); + + string assetPath = basePath + "assets/images/gamexplain.png"; + VImage tmpl = VImage::new_from_file(assetPath.c_str()); + + int width = in.width(); + int pageHeight = vips_image_get_page_height(in.get_image()); + int nPages = vips_image_get_n_pages(in.get_image()); + + vector img; + for (int i = 0; i < nPages; i++) { + VImage img_frame = + type == "gif" ? in.crop(0, i * pageHeight, width, pageHeight) : in; + VImage resized = + img_frame + .resize(1181.0 / (double)width, + VImage::option()->set("vscale", 571.0 / (double)pageHeight)) + .embed(10, 92, 1200, 675, VImage::option()->set("extend", "white")); + VImage composited = resized.composite2(tmpl, VIPS_BLEND_MODE_OVER); + img.push_back(composited); + } + VImage final = VImage::arrayjoin(img, VImage::option()->set("across", 1)); + final.set(VIPS_META_PAGE_HEIGHT, 675); + + void *buf; + final.write_to_buffer( + ("." + *outType).c_str(), &buf, DataSize, + *outType == "gif" + ? VImage::option()->set("dither", 0)->set("reoptimise", 1) + : 0); + + return (char *)buf; } \ No newline at end of file diff --git a/natives/gamexplain.h b/natives/gamexplain.h index 7c07c6d..d4a113a 100644 --- a/natives/gamexplain.h +++ b/natives/gamexplain.h @@ -1,8 +1,8 @@ -#pragma once - -#include "common.h" - -using std::string; - -char* Gamexplain(string type, string* outType, char* BufferData, size_t BufferLength, +#pragma once + +#include "common.h" + +using std::string; + +char* Gamexplain(string type, string* outType, char* BufferData, size_t BufferLength, ArgumentMap Arguments, size_t* DataSize); \ No newline at end of file diff --git a/natives/globe.cc b/natives/globe.cc index 595329e..025252b 100644 --- a/natives/globe.cc +++ b/natives/globe.cc @@ -1,73 +1,73 @@ -#include - -#include "common.h" - -using namespace std; -using namespace vips; - -char *Globe(string type, string *outType, char *BufferData, size_t BufferLength, - ArgumentMap Arguments, size_t *DataSize) { - string basePath = GetArgument(Arguments, "basePath"); - - VOption *options = VImage::option(); - - VImage in = - VImage::new_from_buffer( - BufferData, BufferLength, "", - type == "gif" ? options->set("n", -1)->set("access", "sequential") - : options) - .colourspace(VIPS_INTERPRETATION_sRGB); - if (!in.has_alpha()) in = in.bandjoin(255); - - int width = in.width(); - int pageHeight = vips_image_get_page_height(in.get_image()); - int nPages = type == "gif" ? vips_image_get_n_pages(in.get_image()) : 30; - - double size = min(width, pageHeight); - - string diffPath = basePath + "assets/images/globediffuse.png"; - VImage diffuse = VImage::new_from_file(diffPath.c_str()) - .resize(size / 500.0, VImage::option()->set( - "kernel", VIPS_KERNEL_CUBIC)) / - 255; - - string specPath = basePath + "assets/images/globespec.png"; - VImage specular = VImage::new_from_file(specPath.c_str()) - .resize(size / 500.0, VImage::option()->set( - "kernel", VIPS_KERNEL_CUBIC)); - - string distortPath = basePath + "assets/images/spheremap.png"; - VImage distort = - (VImage::new_from_file(distortPath.c_str()) - .resize(size / 500.0, - VImage::option()->set("kernel", VIPS_KERNEL_CUBIC)) / - 65535) * - size; - - vector img; - for (int i = 0; i < nPages; i++) { - VImage img_frame = - type == "gif" ? in.crop(0, i * pageHeight, width, pageHeight) : in; - VImage resized = img_frame.resize( - size / (double)width, - VImage::option()->set("vscale", size / (double)pageHeight)); - VImage rolled = img_frame.wrap( - VImage::option()->set("x", width * i / nPages)->set("y", 0)); - VImage extracted = rolled.extract_band(0, VImage::option()->set("n", 3)); - VImage mapped = extracted.mapim(distort); - VImage composited = mapped * diffuse + specular; - VImage frame = composited.bandjoin(diffuse > 0.0); - img.push_back(frame); - } - VImage final = VImage::arrayjoin(img, VImage::option()->set("across", 1)); - final.set(VIPS_META_PAGE_HEIGHT, size); - if (type != "gif") { - vector delay(30, 50); - final.set("delay", delay); - } - - void *buf; - final.write_to_buffer(".gif", &buf, DataSize); - - return (char *)buf; +#include + +#include "common.h" + +using namespace std; +using namespace vips; + +char *Globe(string type, string *outType, char *BufferData, size_t BufferLength, + ArgumentMap Arguments, size_t *DataSize) { + string basePath = GetArgument(Arguments, "basePath"); + + VOption *options = VImage::option(); + + VImage in = + VImage::new_from_buffer( + BufferData, BufferLength, "", + type == "gif" ? options->set("n", -1)->set("access", "sequential") + : options) + .colourspace(VIPS_INTERPRETATION_sRGB); + if (!in.has_alpha()) in = in.bandjoin(255); + + int width = in.width(); + int pageHeight = vips_image_get_page_height(in.get_image()); + int nPages = type == "gif" ? vips_image_get_n_pages(in.get_image()) : 30; + + double size = min(width, pageHeight); + + string diffPath = basePath + "assets/images/globediffuse.png"; + VImage diffuse = VImage::new_from_file(diffPath.c_str()) + .resize(size / 500.0, VImage::option()->set( + "kernel", VIPS_KERNEL_CUBIC)) / + 255; + + string specPath = basePath + "assets/images/globespec.png"; + VImage specular = VImage::new_from_file(specPath.c_str()) + .resize(size / 500.0, VImage::option()->set( + "kernel", VIPS_KERNEL_CUBIC)); + + string distortPath = basePath + "assets/images/spheremap.png"; + VImage distort = + (VImage::new_from_file(distortPath.c_str()) + .resize(size / 500.0, + VImage::option()->set("kernel", VIPS_KERNEL_CUBIC)) / + 65535) * + size; + + vector img; + for (int i = 0; i < nPages; i++) { + VImage img_frame = + type == "gif" ? in.crop(0, i * pageHeight, width, pageHeight) : in; + VImage resized = img_frame.resize( + size / (double)width, + VImage::option()->set("vscale", size / (double)pageHeight)); + VImage rolled = img_frame.wrap( + VImage::option()->set("x", width * i / nPages)->set("y", 0)); + VImage extracted = rolled.extract_band(0, VImage::option()->set("n", 3)); + VImage mapped = extracted.mapim(distort); + VImage composited = mapped * diffuse + specular; + VImage frame = composited.bandjoin(diffuse > 0.0); + img.push_back(frame); + } + VImage final = VImage::arrayjoin(img, VImage::option()->set("across", 1)); + final.set(VIPS_META_PAGE_HEIGHT, size); + if (type != "gif") { + vector delay(30, 50); + final.set("delay", delay); + } + + void *buf; + final.write_to_buffer(".gif", &buf, DataSize); + + return (char *)buf; } \ No newline at end of file diff --git a/natives/globe.h b/natives/globe.h index ca21889..f51022c 100644 --- a/natives/globe.h +++ b/natives/globe.h @@ -1,8 +1,8 @@ -#pragma once - -#include "common.h" - -using std::string; - -char* Globe(string type, string* outType, char* BufferData, size_t BufferLength, +#pragma once + +#include "common.h" + +using std::string; + +char* Globe(string type, string* outType, char* BufferData, size_t BufferLength, ArgumentMap Arguments, size_t* DataSize); \ No newline at end of file diff --git a/natives/homebrew.cc b/natives/homebrew.cc index 28f2afd..503e42f 100644 --- a/natives/homebrew.cc +++ b/natives/homebrew.cc @@ -1,37 +1,37 @@ -#include - -#include "common.h" - -using namespace std; -using namespace vips; - -char *Homebrew(string type, string *outType, ArgumentMap Arguments, - size_t *DataSize) { - string caption = GetArgument(Arguments, "caption"); - string basePath = GetArgument(Arguments, "basePath"); - - string assetPath = basePath + "assets/images/hbc.png"; - VImage bg = VImage::new_from_file(assetPath.c_str()); - - VImage text = VImage::text( - ".", VImage::option()->set("fontfile", - (basePath + "assets/fonts/hbc.ttf").c_str())); - text = VImage::text( - ("" + caption + "") - .c_str(), - VImage::option() - ->set("rgba", true) - ->set("align", VIPS_ALIGN_CENTRE) - ->set("font", "PF Square Sans Pro, Twemoji Color Font 96") - ->set("fontfile", (basePath + "assets/fonts/twemoji.otf").c_str())); - - VImage out = bg.composite2(text, VIPS_BLEND_MODE_OVER, - VImage::option() - ->set("x", 400 - (text.width() / 2)) - ->set("y", 300 - (text.height() / 2) - 8)); - - void *buf; - out.write_to_buffer(("." + *outType).c_str(), &buf, DataSize); - - return (char *)buf; +#include + +#include "common.h" + +using namespace std; +using namespace vips; + +char *Homebrew(string type, string *outType, ArgumentMap Arguments, + size_t *DataSize) { + string caption = GetArgument(Arguments, "caption"); + string basePath = GetArgument(Arguments, "basePath"); + + string assetPath = basePath + "assets/images/hbc.png"; + VImage bg = VImage::new_from_file(assetPath.c_str()); + + VImage text = VImage::text( + ".", VImage::option()->set("fontfile", + (basePath + "assets/fonts/hbc.ttf").c_str())); + text = VImage::text( + ("" + caption + "") + .c_str(), + VImage::option() + ->set("rgba", true) + ->set("align", VIPS_ALIGN_CENTRE) + ->set("font", "PF Square Sans Pro, Twemoji Color Font 96") + ->set("fontfile", (basePath + "assets/fonts/twemoji.otf").c_str())); + + VImage out = bg.composite2(text, VIPS_BLEND_MODE_OVER, + VImage::option() + ->set("x", 400 - (text.width() / 2)) + ->set("y", 300 - (text.height() / 2) - 8)); + + void *buf; + out.write_to_buffer(("." + *outType).c_str(), &buf, DataSize); + + return (char *)buf; } \ No newline at end of file diff --git a/natives/homebrew.h b/natives/homebrew.h index 3304c4e..61bbb10 100644 --- a/natives/homebrew.h +++ b/natives/homebrew.h @@ -1,7 +1,7 @@ -#pragma once - -#include "common.h" - -using std::string; - +#pragma once + +#include "common.h" + +using std::string; + char *Homebrew(string type, string *outType, ArgumentMap Arguments, size_t *DataSize); \ No newline at end of file diff --git a/natives/invert.cc b/natives/invert.cc index a46fc10..570428c 100644 --- a/natives/invert.cc +++ b/natives/invert.cc @@ -1,28 +1,28 @@ -#include - -#include "common.h" - -using namespace std; -using namespace vips; - -char *Invert(string type, string *outType, char *BufferData, - size_t BufferLength, [[maybe_unused]] ArgumentMap Arguments, - size_t *DataSize) { - VOption *options = VImage::option()->set("access", "sequential"); - - VImage in = - VImage::new_from_buffer(BufferData, BufferLength, "", - type == "gif" ? options->set("n", -1) : options) - .colourspace(VIPS_INTERPRETATION_sRGB); - if (!in.has_alpha()) in = in.bandjoin(255); - - VImage noAlpha = - in.extract_band(0, VImage::option()->set("n", in.bands() - 1)); - VImage inverted = noAlpha.invert(); - VImage out = inverted.bandjoin(in.extract_band(3)); - - void *buf; - out.write_to_buffer(("." + *outType).c_str(), &buf, DataSize); - - return (char *)buf; +#include + +#include "common.h" + +using namespace std; +using namespace vips; + +char *Invert(string type, string *outType, char *BufferData, + size_t BufferLength, [[maybe_unused]] ArgumentMap Arguments, + size_t *DataSize) { + VOption *options = VImage::option()->set("access", "sequential"); + + VImage in = + VImage::new_from_buffer(BufferData, BufferLength, "", + type == "gif" ? options->set("n", -1) : options) + .colourspace(VIPS_INTERPRETATION_sRGB); + if (!in.has_alpha()) in = in.bandjoin(255); + + VImage noAlpha = + in.extract_band(0, VImage::option()->set("n", in.bands() - 1)); + VImage inverted = noAlpha.invert(); + VImage out = inverted.bandjoin(in.extract_band(3)); + + void *buf; + out.write_to_buffer(("." + *outType).c_str(), &buf, DataSize); + + return (char *)buf; } \ No newline at end of file diff --git a/natives/invert.h b/natives/invert.h index 7db64e3..84dd3d4 100644 --- a/natives/invert.h +++ b/natives/invert.h @@ -1,8 +1,8 @@ -#pragma once - -#include "common.h" - -using std::string; - -char* Invert(string type, string* outType, char* BufferData, size_t BufferLength, +#pragma once + +#include "common.h" + +using std::string; + +char* Invert(string type, string* outType, char* BufferData, size_t BufferLength, ArgumentMap Arguments, size_t* DataSize); \ No newline at end of file diff --git a/natives/jpeg.cc b/natives/jpeg.cc index d1eff63..5133014 100644 --- a/natives/jpeg.cc +++ b/natives/jpeg.cc @@ -1,75 +1,75 @@ -#include - -#include "common.h" - -using namespace std; -using namespace vips; - -char *Jpeg(string type, string *outType, char *BufferData, size_t BufferLength, - ArgumentMap Arguments, size_t *DataSize) { - int quality = GetArgumentWithFallback(Arguments, "quality", 0); - - void *buf; - - if (type == "gif") { - VImage in = VImage::new_from_buffer( - BufferData, BufferLength, "", - VImage::option()->set("access", "sequential")->set("n", -1)) - .colourspace(VIPS_INTERPRETATION_sRGB); - if (!in.has_alpha()) in = in.bandjoin(255); - - int width = in.width(); - int pageHeight = vips_image_get_page_height(in.get_image()); - int totalHeight = in.height(); - int nPages = vips_image_get_n_pages(in.get_image()); - - VImage final; - - if (totalHeight > 65500) { - vector img; - for (int i = 0; i < nPages; i++) { - VImage img_frame = in.crop(0, i * pageHeight, width, pageHeight); - void *jpgBuf; - size_t jpgLength; - img_frame.write_to_buffer( - ".jpg", &jpgBuf, &jpgLength, - VImage::option()->set("Q", quality)->set("strip", true)); - VImage jpeged = VImage::new_from_buffer(jpgBuf, jpgLength, ""); - jpeged.set(VIPS_META_PAGE_HEIGHT, pageHeight); - jpeged.set("delay", in.get_array_int("delay")); - img.push_back(jpeged); - } - final = VImage::arrayjoin(img, VImage::option()->set("across", 1)); - final.set(VIPS_META_PAGE_HEIGHT, pageHeight); - } else { - void *jpgBuf; - size_t jpgLength; - in.write_to_buffer( - ".jpg", &jpgBuf, &jpgLength, - VImage::option()->set("Q", quality)->set("strip", true)); - final = VImage::new_from_buffer(jpgBuf, jpgLength, ""); - final.set(VIPS_META_PAGE_HEIGHT, pageHeight); - final.set("delay", in.get_array_int("delay")); - } - - final.write_to_buffer( - ("." + *outType).c_str(), &buf, DataSize, - *outType == "gif" ? VImage::option()->set("dither", 0) : 0); - } else { - VImage in = VImage::new_from_buffer(BufferData, BufferLength, ""); - void *jpgBuf; - in.write_to_buffer(".jpg", &jpgBuf, DataSize, - VImage::option()->set("Q", quality)->set("strip", true)); - if (*outType == "gif") { - VImage gifIn = VImage::new_from_buffer((char *)jpgBuf, *DataSize, ""); - gifIn.write_to_buffer( - ".gif", &buf, DataSize, - VImage::option()->set("Q", quality)->set("strip", true)); - } else { - *outType = "jpg"; - buf = jpgBuf; - } - } - - return (char *)buf; -} +#include + +#include "common.h" + +using namespace std; +using namespace vips; + +char *Jpeg(string type, string *outType, char *BufferData, size_t BufferLength, + ArgumentMap Arguments, size_t *DataSize) { + int quality = GetArgumentWithFallback(Arguments, "quality", 0); + + void *buf; + + if (type == "gif") { + VImage in = VImage::new_from_buffer( + BufferData, BufferLength, "", + VImage::option()->set("access", "sequential")->set("n", -1)) + .colourspace(VIPS_INTERPRETATION_sRGB); + if (!in.has_alpha()) in = in.bandjoin(255); + + int width = in.width(); + int pageHeight = vips_image_get_page_height(in.get_image()); + int totalHeight = in.height(); + int nPages = vips_image_get_n_pages(in.get_image()); + + VImage final; + + if (totalHeight > 65500) { + vector img; + for (int i = 0; i < nPages; i++) { + VImage img_frame = in.crop(0, i * pageHeight, width, pageHeight); + void *jpgBuf; + size_t jpgLength; + img_frame.write_to_buffer( + ".jpg", &jpgBuf, &jpgLength, + VImage::option()->set("Q", quality)->set("strip", true)); + VImage jpeged = VImage::new_from_buffer(jpgBuf, jpgLength, ""); + jpeged.set(VIPS_META_PAGE_HEIGHT, pageHeight); + jpeged.set("delay", in.get_array_int("delay")); + img.push_back(jpeged); + } + final = VImage::arrayjoin(img, VImage::option()->set("across", 1)); + final.set(VIPS_META_PAGE_HEIGHT, pageHeight); + } else { + void *jpgBuf; + size_t jpgLength; + in.write_to_buffer( + ".jpg", &jpgBuf, &jpgLength, + VImage::option()->set("Q", quality)->set("strip", true)); + final = VImage::new_from_buffer(jpgBuf, jpgLength, ""); + final.set(VIPS_META_PAGE_HEIGHT, pageHeight); + final.set("delay", in.get_array_int("delay")); + } + + final.write_to_buffer( + ("." + *outType).c_str(), &buf, DataSize, + *outType == "gif" ? VImage::option()->set("dither", 0) : 0); + } else { + VImage in = VImage::new_from_buffer(BufferData, BufferLength, ""); + void *jpgBuf; + in.write_to_buffer(".jpg", &jpgBuf, DataSize, + VImage::option()->set("Q", quality)->set("strip", true)); + if (*outType == "gif") { + VImage gifIn = VImage::new_from_buffer((char *)jpgBuf, *DataSize, ""); + gifIn.write_to_buffer( + ".gif", &buf, DataSize, + VImage::option()->set("Q", quality)->set("strip", true)); + } else { + *outType = "jpg"; + buf = jpgBuf; + } + } + + return (char *)buf; +} diff --git a/natives/jpeg.h b/natives/jpeg.h index 2e6912c..7282133 100644 --- a/natives/jpeg.h +++ b/natives/jpeg.h @@ -1,8 +1,8 @@ -#pragma once - -#include "common.h" - -using std::string; - -char* Jpeg(string type, string* outType, char* BufferData, size_t BufferLength, +#pragma once + +#include "common.h" + +using std::string; + +char* Jpeg(string type, string* outType, char* BufferData, size_t BufferLength, ArgumentMap Arguments, size_t* DataSize); \ No newline at end of file diff --git a/natives/magik.cc b/natives/magik.cc index 4aae8f6..92e7ba1 100644 --- a/natives/magik.cc +++ b/natives/magik.cc @@ -1,52 +1,52 @@ -#include - -#include -#include -#include - -#include "common.h" - -using namespace std; -using namespace Magick; - -char *Magik(string type, string *outType, char *BufferData, size_t BufferLength, - [[maybe_unused]] ArgumentMap Arguments, size_t *DataSize) { - Blob blob; - - list frames; - list coalesced; - list blurred; - try { - readImages(&frames, Blob(BufferData, BufferLength)); - } catch (Magick::WarningCoder &warning) { - cerr << "Coder Warning: " << warning.what() << endl; - } catch (Magick::Warning &warning) { - cerr << "Warning: " << warning.what() << endl; - } - coalesceImages(&coalesced, frames.begin(), frames.end()); - - for (Image &image : coalesced) { - image.scale(Geometry("350x350")); - image.liquidRescale(Geometry("175x175")); - image.liquidRescale(Geometry("350x350")); - image.magick(*outType); - blurred.push_back(image); - } - - optimizeTransparency(blurred.begin(), blurred.end()); - - if (*outType == "gif") { - for (Image &image : blurred) { - image.quantizeDitherMethod(FloydSteinbergDitherMethod); - image.quantize(); - } - } - - writeImages(blurred.begin(), blurred.end(), &blob); - - *DataSize = blob.length(); - - char *data = (char *)malloc(*DataSize); - memcpy(data, blob.data(), *DataSize); - return data; +#include + +#include +#include +#include + +#include "common.h" + +using namespace std; +using namespace Magick; + +char *Magik(string type, string *outType, char *BufferData, size_t BufferLength, + [[maybe_unused]] ArgumentMap Arguments, size_t *DataSize) { + Blob blob; + + list frames; + list coalesced; + list blurred; + try { + readImages(&frames, Blob(BufferData, BufferLength)); + } catch (Magick::WarningCoder &warning) { + cerr << "Coder Warning: " << warning.what() << endl; + } catch (Magick::Warning &warning) { + cerr << "Warning: " << warning.what() << endl; + } + coalesceImages(&coalesced, frames.begin(), frames.end()); + + for (Image &image : coalesced) { + image.scale(Geometry("350x350")); + image.liquidRescale(Geometry("175x175")); + image.liquidRescale(Geometry("350x350")); + image.magick(*outType); + blurred.push_back(image); + } + + optimizeTransparency(blurred.begin(), blurred.end()); + + if (*outType == "gif") { + for (Image &image : blurred) { + image.quantizeDitherMethod(FloydSteinbergDitherMethod); + image.quantize(); + } + } + + writeImages(blurred.begin(), blurred.end(), &blob); + + *DataSize = blob.length(); + + char *data = (char *)malloc(*DataSize); + memcpy(data, blob.data(), *DataSize); + return data; } \ No newline at end of file diff --git a/natives/magik.h b/natives/magik.h index b4c1639..19dc463 100644 --- a/natives/magik.h +++ b/natives/magik.h @@ -1,8 +1,8 @@ -#pragma once - -#include "common.h" - -using std::string; - -char* Magik(string type, string* outType, char* BufferData, size_t BufferLength, +#pragma once + +#include "common.h" + +using std::string; + +char* Magik(string type, string* outType, char* BufferData, size_t BufferLength, ArgumentMap Arguments, size_t* DataSize); \ No newline at end of file diff --git a/natives/meme.cc b/natives/meme.cc index f7cb51a..850576d 100644 --- a/natives/meme.cc +++ b/natives/meme.cc @@ -1,139 +1,139 @@ -#include - -#include "common.h" - -using namespace std; -using namespace vips; - -char *Meme(string type, string *outType, char *BufferData, size_t BufferLength, - ArgumentMap Arguments, size_t *DataSize) { - string top = GetArgument(Arguments, "top"); - string bottom = GetArgument(Arguments, "bottom"); - string font = GetArgument(Arguments, "font"); - string basePath = GetArgument(Arguments, "basePath"); - - VOption *options = VImage::option()->set("access", "sequential"); - - VImage in = - VImage::new_from_buffer(BufferData, BufferLength, "", - type == "gif" ? options->set("n", -1) : options) - .colourspace(VIPS_INTERPRETATION_sRGB); - if (!in.has_alpha()) in = in.bandjoin(255); - - int width = in.width(); - int pageHeight = vips_image_get_page_height(in.get_image()); - int nPages = vips_image_get_n_pages(in.get_image()); - int size = width / 9; - int dividedWidth = width / 1000; - int rad = 1; - vector zeroVec = {0, 0, 0, 0}; - - string font_string = - (font == "roboto" ? "Roboto Condensed" : font) + ", Twemoji Color Font " + - (font != "impact" ? "bold" : "normal") + " " + to_string(size); - - VImage mask = VImage::black(rad * 2 + 1, rad * 2 + 1) + 128; - mask.draw_circle({255}, rad, rad, rad, VImage::option()->set("fill", true)); - - VImage altMask; - - if (dividedWidth >= 1) { - altMask = VImage::black(dividedWidth * 2 + 1, dividedWidth * 2 + 1) + 128; - altMask.draw_circle({255}, dividedWidth, dividedWidth, dividedWidth, - VImage::option()->set("fill", true)); - } - - auto findResult = fontPaths.find(font); - if (findResult != fontPaths.end()) { - VImage::text(".", VImage::option()->set( - "fontfile", (basePath + findResult->second).c_str())); - } - - VImage topText; - if (top != "") { - VImage topIn = VImage::text( - ("" + top + "").c_str(), - VImage::option() - ->set("rgba", true) - ->set("align", VIPS_ALIGN_CENTRE) - ->set("font", font_string.c_str()) - ->set("fontfile", (basePath + "assets/fonts/twemoji.otf").c_str()) - ->set("width", width)); - - topIn = topIn.embed(rad + 10, rad + 10, (topIn.width() + 2 * rad) + 20, - (topIn.height() + 2 * rad) + 20); - - VImage topOutline = - topIn.morph(mask, VIPS_OPERATION_MORPHOLOGY_DILATE) - .gaussblur(0.5, VImage::option()->set("min_ampl", 0.1)); - if (dividedWidth >= 1) { - topOutline = topOutline.morph(altMask, VIPS_OPERATION_MORPHOLOGY_DILATE); - } - topOutline = (topOutline == zeroVec); - VImage topInvert = topOutline.extract_band(3).invert(); - topOutline = - topOutline - .extract_band(0, VImage::option()->set("n", topOutline.bands() - 1)) - .bandjoin(topInvert); - topText = topOutline.composite2(topIn, VIPS_BLEND_MODE_OVER); - } - - VImage bottomText; - if (bottom != "") { - VImage bottomIn = VImage::text( - ("" + bottom + "").c_str(), - VImage::option() - ->set("rgba", true) - ->set("align", VIPS_ALIGN_CENTRE) - ->set("font", font_string.c_str()) - ->set("fontfile", (basePath + "assets/fonts/twemoji.otf").c_str()) - ->set("width", width)); - bottomIn = - bottomIn.embed(rad + 10, rad + 10, (bottomIn.width() + 2 * rad) + 20, - (bottomIn.height() + 2 * rad) + 20); - VImage bottomOutline = - bottomIn.morph(mask, VIPS_OPERATION_MORPHOLOGY_DILATE) - .gaussblur(0.5, VImage::option()->set("min_ampl", 0.1)); - if (dividedWidth >= 1) { - bottomOutline = - bottomOutline.morph(altMask, VIPS_OPERATION_MORPHOLOGY_DILATE); - } - bottomOutline = (bottomOutline == zeroVec); - VImage bottomInvert = bottomOutline.extract_band(3).invert(); - bottomOutline = bottomOutline - .extract_band(0, VImage::option()->set( - "n", bottomOutline.bands() - 1)) - .bandjoin(bottomInvert); - bottomText = bottomOutline.composite2(bottomIn, VIPS_BLEND_MODE_OVER); - } - - vector img; - for (int i = 0; i < nPages; i++) { - VImage img_frame = - type == "gif" ? in.crop(0, i * pageHeight, width, pageHeight) : in; - if (top != "") { - img_frame = img_frame.composite2( - topText, VIPS_BLEND_MODE_OVER, - VImage::option()->set("x", (width / 2) - (topText.width() / 2))); - } - if (bottom != "") { - img_frame = img_frame.composite2( - bottomText, VIPS_BLEND_MODE_OVER, - VImage::option() - ->set("x", (width / 2) - (bottomText.width() / 2)) - ->set("y", pageHeight - bottomText.height())); - } - img.push_back(img_frame); - } - VImage final = VImage::arrayjoin(img, VImage::option()->set("across", 1)); - final.set(VIPS_META_PAGE_HEIGHT, pageHeight); - - void *buf; - final.write_to_buffer( - ("." + *outType).c_str(), &buf, DataSize, - *outType == "gif" - ? VImage::option()->set("dither", 0)->set("reoptimise", 1) - : 0); - - return (char *)buf; +#include + +#include "common.h" + +using namespace std; +using namespace vips; + +char *Meme(string type, string *outType, char *BufferData, size_t BufferLength, + ArgumentMap Arguments, size_t *DataSize) { + string top = GetArgument(Arguments, "top"); + string bottom = GetArgument(Arguments, "bottom"); + string font = GetArgument(Arguments, "font"); + string basePath = GetArgument(Arguments, "basePath"); + + VOption *options = VImage::option()->set("access", "sequential"); + + VImage in = + VImage::new_from_buffer(BufferData, BufferLength, "", + type == "gif" ? options->set("n", -1) : options) + .colourspace(VIPS_INTERPRETATION_sRGB); + if (!in.has_alpha()) in = in.bandjoin(255); + + int width = in.width(); + int pageHeight = vips_image_get_page_height(in.get_image()); + int nPages = vips_image_get_n_pages(in.get_image()); + int size = width / 9; + int dividedWidth = width / 1000; + int rad = 1; + vector zeroVec = {0, 0, 0, 0}; + + string font_string = + (font == "roboto" ? "Roboto Condensed" : font) + ", Twemoji Color Font " + + (font != "impact" ? "bold" : "normal") + " " + to_string(size); + + VImage mask = VImage::black(rad * 2 + 1, rad * 2 + 1) + 128; + mask.draw_circle({255}, rad, rad, rad, VImage::option()->set("fill", true)); + + VImage altMask; + + if (dividedWidth >= 1) { + altMask = VImage::black(dividedWidth * 2 + 1, dividedWidth * 2 + 1) + 128; + altMask.draw_circle({255}, dividedWidth, dividedWidth, dividedWidth, + VImage::option()->set("fill", true)); + } + + auto findResult = fontPaths.find(font); + if (findResult != fontPaths.end()) { + VImage::text(".", VImage::option()->set( + "fontfile", (basePath + findResult->second).c_str())); + } + + VImage topText; + if (top != "") { + VImage topIn = VImage::text( + ("" + top + "").c_str(), + VImage::option() + ->set("rgba", true) + ->set("align", VIPS_ALIGN_CENTRE) + ->set("font", font_string.c_str()) + ->set("fontfile", (basePath + "assets/fonts/twemoji.otf").c_str()) + ->set("width", width)); + + topIn = topIn.embed(rad + 10, rad + 10, (topIn.width() + 2 * rad) + 20, + (topIn.height() + 2 * rad) + 20); + + VImage topOutline = + topIn.morph(mask, VIPS_OPERATION_MORPHOLOGY_DILATE) + .gaussblur(0.5, VImage::option()->set("min_ampl", 0.1)); + if (dividedWidth >= 1) { + topOutline = topOutline.morph(altMask, VIPS_OPERATION_MORPHOLOGY_DILATE); + } + topOutline = (topOutline == zeroVec); + VImage topInvert = topOutline.extract_band(3).invert(); + topOutline = + topOutline + .extract_band(0, VImage::option()->set("n", topOutline.bands() - 1)) + .bandjoin(topInvert); + topText = topOutline.composite2(topIn, VIPS_BLEND_MODE_OVER); + } + + VImage bottomText; + if (bottom != "") { + VImage bottomIn = VImage::text( + ("" + bottom + "").c_str(), + VImage::option() + ->set("rgba", true) + ->set("align", VIPS_ALIGN_CENTRE) + ->set("font", font_string.c_str()) + ->set("fontfile", (basePath + "assets/fonts/twemoji.otf").c_str()) + ->set("width", width)); + bottomIn = + bottomIn.embed(rad + 10, rad + 10, (bottomIn.width() + 2 * rad) + 20, + (bottomIn.height() + 2 * rad) + 20); + VImage bottomOutline = + bottomIn.morph(mask, VIPS_OPERATION_MORPHOLOGY_DILATE) + .gaussblur(0.5, VImage::option()->set("min_ampl", 0.1)); + if (dividedWidth >= 1) { + bottomOutline = + bottomOutline.morph(altMask, VIPS_OPERATION_MORPHOLOGY_DILATE); + } + bottomOutline = (bottomOutline == zeroVec); + VImage bottomInvert = bottomOutline.extract_band(3).invert(); + bottomOutline = bottomOutline + .extract_band(0, VImage::option()->set( + "n", bottomOutline.bands() - 1)) + .bandjoin(bottomInvert); + bottomText = bottomOutline.composite2(bottomIn, VIPS_BLEND_MODE_OVER); + } + + vector img; + for (int i = 0; i < nPages; i++) { + VImage img_frame = + type == "gif" ? in.crop(0, i * pageHeight, width, pageHeight) : in; + if (top != "") { + img_frame = img_frame.composite2( + topText, VIPS_BLEND_MODE_OVER, + VImage::option()->set("x", (width / 2) - (topText.width() / 2))); + } + if (bottom != "") { + img_frame = img_frame.composite2( + bottomText, VIPS_BLEND_MODE_OVER, + VImage::option() + ->set("x", (width / 2) - (bottomText.width() / 2)) + ->set("y", pageHeight - bottomText.height())); + } + img.push_back(img_frame); + } + VImage final = VImage::arrayjoin(img, VImage::option()->set("across", 1)); + final.set(VIPS_META_PAGE_HEIGHT, pageHeight); + + void *buf; + final.write_to_buffer( + ("." + *outType).c_str(), &buf, DataSize, + *outType == "gif" + ? VImage::option()->set("dither", 0)->set("reoptimise", 1) + : 0); + + return (char *)buf; } \ No newline at end of file diff --git a/natives/meme.h b/natives/meme.h index 5c05a29..2071331 100644 --- a/natives/meme.h +++ b/natives/meme.h @@ -1,8 +1,8 @@ -#pragma once - -#include "common.h" - -using std::string; - -char* Meme(string type, string* outType, char* BufferData, size_t BufferLength, +#pragma once + +#include "common.h" + +using std::string; + +char* Meme(string type, string* outType, char* BufferData, size_t BufferLength, ArgumentMap Arguments, size_t* DataSize); \ No newline at end of file diff --git a/natives/mirror.cc b/natives/mirror.cc index bb4db00..a90efe3 100644 --- a/natives/mirror.cc +++ b/natives/mirror.cc @@ -1,64 +1,64 @@ -#include - -#include "common.h" - -using namespace std; -using namespace vips; - -char *Mirror(string type, string *outType, char *BufferData, - size_t BufferLength, ArgumentMap Arguments, size_t *DataSize) { - bool vertical = GetArgumentWithFallback(Arguments, "vertical", false); - bool first = GetArgumentWithFallback(Arguments, "first", false); - - VOption *options = VImage::option()->set("access", "sequential"); - - VImage in = - VImage::new_from_buffer(BufferData, BufferLength, "", - type == "gif" ? options->set("n", -1) : options) - .colourspace(VIPS_INTERPRETATION_sRGB); - if (!in.has_alpha()) in = in.bandjoin(255); - - VImage out; - - if (vertical) { - if (type == "gif") { - // once again, libvips gif handling is both a blessing and a curse - vector img; - int pageHeight = vips_image_get_page_height(in.get_image()); - int nPages = vips_image_get_n_pages(in.get_image()); - bool isOdd = pageHeight % 2; - for (int i = 0; i < nPages; i++) { - int x = (i * pageHeight) + (first ? 0 : (pageHeight / 2)); - VImage cropped = in.crop(0, x, in.width(), pageHeight / 2); - VImage flipped = cropped.flip(VIPS_DIRECTION_VERTICAL); - VImage final = VImage::arrayjoin( - {first ? cropped : flipped, first ? flipped : cropped}, - VImage::option()->set("across", 1)); - img.push_back(final); - } - out = VImage::arrayjoin(img, VImage::option()->set("across", 1)); - out.set(VIPS_META_PAGE_HEIGHT, pageHeight - (isOdd ? 1 : 0)); - } else { - VImage cropped = in.extract_area(0, 0, in.width(), in.height() / 2); - VImage flipped = cropped.flip(VIPS_DIRECTION_VERTICAL); - out = VImage::arrayjoin({cropped, flipped}, - VImage::option()->set("across", 1)); - } - } else { - if (first) { - VImage cropped = in.extract_area(0, 0, in.width() / 2, in.height()); - VImage flipped = cropped.flip(VIPS_DIRECTION_HORIZONTAL); - out = VImage::arrayjoin({cropped, flipped}); - } else { - int size = in.width() / 2; - VImage cropped = in.extract_area(size, 0, size, in.height()); - VImage flipped = cropped.flip(VIPS_DIRECTION_HORIZONTAL); - out = VImage::arrayjoin({flipped, cropped}); - } - } - - void *buf; - out.write_to_buffer(("." + *outType).c_str(), &buf, DataSize); - - return (char *)buf; -} +#include + +#include "common.h" + +using namespace std; +using namespace vips; + +char *Mirror(string type, string *outType, char *BufferData, + size_t BufferLength, ArgumentMap Arguments, size_t *DataSize) { + bool vertical = GetArgumentWithFallback(Arguments, "vertical", false); + bool first = GetArgumentWithFallback(Arguments, "first", false); + + VOption *options = VImage::option()->set("access", "sequential"); + + VImage in = + VImage::new_from_buffer(BufferData, BufferLength, "", + type == "gif" ? options->set("n", -1) : options) + .colourspace(VIPS_INTERPRETATION_sRGB); + if (!in.has_alpha()) in = in.bandjoin(255); + + VImage out; + + if (vertical) { + if (type == "gif") { + // once again, libvips gif handling is both a blessing and a curse + vector img; + int pageHeight = vips_image_get_page_height(in.get_image()); + int nPages = vips_image_get_n_pages(in.get_image()); + bool isOdd = pageHeight % 2; + for (int i = 0; i < nPages; i++) { + int x = (i * pageHeight) + (first ? 0 : (pageHeight / 2)); + VImage cropped = in.crop(0, x, in.width(), pageHeight / 2); + VImage flipped = cropped.flip(VIPS_DIRECTION_VERTICAL); + VImage final = VImage::arrayjoin( + {first ? cropped : flipped, first ? flipped : cropped}, + VImage::option()->set("across", 1)); + img.push_back(final); + } + out = VImage::arrayjoin(img, VImage::option()->set("across", 1)); + out.set(VIPS_META_PAGE_HEIGHT, pageHeight - (isOdd ? 1 : 0)); + } else { + VImage cropped = in.extract_area(0, 0, in.width(), in.height() / 2); + VImage flipped = cropped.flip(VIPS_DIRECTION_VERTICAL); + out = VImage::arrayjoin({cropped, flipped}, + VImage::option()->set("across", 1)); + } + } else { + if (first) { + VImage cropped = in.extract_area(0, 0, in.width() / 2, in.height()); + VImage flipped = cropped.flip(VIPS_DIRECTION_HORIZONTAL); + out = VImage::arrayjoin({cropped, flipped}); + } else { + int size = in.width() / 2; + VImage cropped = in.extract_area(size, 0, size, in.height()); + VImage flipped = cropped.flip(VIPS_DIRECTION_HORIZONTAL); + out = VImage::arrayjoin({flipped, cropped}); + } + } + + void *buf; + out.write_to_buffer(("." + *outType).c_str(), &buf, DataSize); + + return (char *)buf; +} diff --git a/natives/mirror.h b/natives/mirror.h index 419f595..6501394 100644 --- a/natives/mirror.h +++ b/natives/mirror.h @@ -1,8 +1,8 @@ -#pragma once - -#include "common.h" - -using std::string; - -char* Mirror(string type, string* outType, char* BufferData, size_t BufferLength, +#pragma once + +#include "common.h" + +using std::string; + +char* Mirror(string type, string* outType, char* BufferData, size_t BufferLength, ArgumentMap Arguments, size_t* DataSize); \ No newline at end of file diff --git a/natives/motivate.cc b/natives/motivate.cc index a2f48bd..fdb8de3 100644 --- a/natives/motivate.cc +++ b/natives/motivate.cc @@ -1,123 +1,123 @@ -#include - -#include "common.h" - -using namespace std; -using namespace vips; - -char *Motivate(string type, string *outType, char *BufferData, - size_t BufferLength, ArgumentMap Arguments, size_t *DataSize) { - string top_text = GetArgument(Arguments, "top"); - string bottom_text = GetArgument(Arguments, "bottom"); - string font = GetArgument(Arguments, "font"); - string basePath = GetArgument(Arguments, "basePath"); - - VOption *options = VImage::option()->set("access", "sequential"); - - VImage in = - VImage::new_from_buffer(BufferData, BufferLength, "", - type == "gif" ? options->set("n", -1) : options) - .colourspace(VIPS_INTERPRETATION_sRGB); - if (!in.has_alpha()) in = in.bandjoin(255); - - int width = in.width(); - int size = width / 5; - int pageHeight = vips_image_get_page_height(in.get_image()); - int nPages = vips_image_get_n_pages(in.get_image()); - int textWidth = width - ((width / 25) * 2); - - string font_string = - (font == "roboto" ? "Roboto Condensed" : font) + ", Twemoji Color Font"; - - auto findResult = fontPaths.find(font); - if (findResult != fontPaths.end()) { - VImage::text(".", VImage::option()->set( - "fontfile", (basePath + findResult->second).c_str())); - } - - VImage topImage; - if (top_text != "") { - string topText = "" + - top_text + ""; - - topImage = VImage::text( - topText.c_str(), - VImage::option() - ->set("rgba", true) - ->set("align", VIPS_ALIGN_CENTRE) - ->set("font", (font_string + " " + to_string(size)).c_str()) - ->set("fontfile", (basePath + "assets/fonts/twemoji.otf").c_str()) - ->set("width", textWidth)); - } - - VImage bottomImage; - if (bottom_text != "") { - string bottomText = "" + - bottom_text + ""; - - bottomImage = VImage::text( - bottomText.c_str(), - VImage::option() - ->set("rgba", true) - ->set("align", VIPS_ALIGN_CENTRE) - ->set("font", (font_string + " " + to_string(size * 0.4)).c_str()) - ->set("fontfile", (basePath + "assets/fonts/twemoji.otf").c_str()) - ->set("width", textWidth)); - } - - vector img; - int height; - for (int i = 0; i < nPages; i++) { - VImage img_frame = - type == "gif" ? in.crop(0, i * pageHeight, width, pageHeight) : in; - - int borderSize = max(2, width / 66); - int borderSize2 = borderSize * 0.5; - VImage bordered = - img_frame.embed(borderSize, borderSize, width + (borderSize * 2), - pageHeight + (borderSize * 2), - VImage::option()->set("extend", "black")); - VImage bordered2 = bordered.embed(borderSize2, borderSize2, - bordered.width() + (borderSize2 * 2), - bordered.height() + (borderSize2 * 2), - VImage::option()->set("extend", "white")); - - int addition = width / 8; - int sideAddition = pageHeight * 0.4; - - VImage bordered3 = bordered2.embed( - sideAddition / 2, addition / 2, bordered2.width() + sideAddition, - bordered2.height() + addition, - VImage::option()->set("extend", "black")); - VImage frame; - if (top_text != "") { - frame = bordered3.join( - topImage.gravity(VIPS_COMPASS_DIRECTION_NORTH, bordered3.width(), - topImage.height() + (size / 4), - VImage::option()->set("extend", "black")), - VIPS_DIRECTION_VERTICAL, - VImage::option()->set("background", 0x000000)->set("expand", true)); - } - if (bottom_text != "") { - if (top_text == "") frame = bordered3; - frame = frame.join( - bottomImage.gravity(VIPS_COMPASS_DIRECTION_NORTH, bordered3.width(), - bottomImage.height() + (size / 4), - VImage::option()->set("extend", "black")), - VIPS_DIRECTION_VERTICAL, - VImage::option()->set("background", 0x000000)->set("expand", true)); - } - height = frame.height(); - img.push_back(frame); - } - VImage final = VImage::arrayjoin(img, VImage::option()->set("across", 1)) - .extract_band(0, VImage::option()->set("n", 3)); - final.set(VIPS_META_PAGE_HEIGHT, height); - - void *buf; - final.write_to_buffer( - ("." + *outType).c_str(), &buf, DataSize, - *outType == "gif" ? VImage::option()->set("dither", 1) : 0); - - return (char *)buf; -} +#include + +#include "common.h" + +using namespace std; +using namespace vips; + +char *Motivate(string type, string *outType, char *BufferData, + size_t BufferLength, ArgumentMap Arguments, size_t *DataSize) { + string top_text = GetArgument(Arguments, "top"); + string bottom_text = GetArgument(Arguments, "bottom"); + string font = GetArgument(Arguments, "font"); + string basePath = GetArgument(Arguments, "basePath"); + + VOption *options = VImage::option()->set("access", "sequential"); + + VImage in = + VImage::new_from_buffer(BufferData, BufferLength, "", + type == "gif" ? options->set("n", -1) : options) + .colourspace(VIPS_INTERPRETATION_sRGB); + if (!in.has_alpha()) in = in.bandjoin(255); + + int width = in.width(); + int size = width / 5; + int pageHeight = vips_image_get_page_height(in.get_image()); + int nPages = vips_image_get_n_pages(in.get_image()); + int textWidth = width - ((width / 25) * 2); + + string font_string = + (font == "roboto" ? "Roboto Condensed" : font) + ", Twemoji Color Font"; + + auto findResult = fontPaths.find(font); + if (findResult != fontPaths.end()) { + VImage::text(".", VImage::option()->set( + "fontfile", (basePath + findResult->second).c_str())); + } + + VImage topImage; + if (top_text != "") { + string topText = "" + + top_text + ""; + + topImage = VImage::text( + topText.c_str(), + VImage::option() + ->set("rgba", true) + ->set("align", VIPS_ALIGN_CENTRE) + ->set("font", (font_string + " " + to_string(size)).c_str()) + ->set("fontfile", (basePath + "assets/fonts/twemoji.otf").c_str()) + ->set("width", textWidth)); + } + + VImage bottomImage; + if (bottom_text != "") { + string bottomText = "" + + bottom_text + ""; + + bottomImage = VImage::text( + bottomText.c_str(), + VImage::option() + ->set("rgba", true) + ->set("align", VIPS_ALIGN_CENTRE) + ->set("font", (font_string + " " + to_string(size * 0.4)).c_str()) + ->set("fontfile", (basePath + "assets/fonts/twemoji.otf").c_str()) + ->set("width", textWidth)); + } + + vector img; + int height; + for (int i = 0; i < nPages; i++) { + VImage img_frame = + type == "gif" ? in.crop(0, i * pageHeight, width, pageHeight) : in; + + int borderSize = max(2, width / 66); + int borderSize2 = borderSize * 0.5; + VImage bordered = + img_frame.embed(borderSize, borderSize, width + (borderSize * 2), + pageHeight + (borderSize * 2), + VImage::option()->set("extend", "black")); + VImage bordered2 = bordered.embed(borderSize2, borderSize2, + bordered.width() + (borderSize2 * 2), + bordered.height() + (borderSize2 * 2), + VImage::option()->set("extend", "white")); + + int addition = width / 8; + int sideAddition = pageHeight * 0.4; + + VImage bordered3 = bordered2.embed( + sideAddition / 2, addition / 2, bordered2.width() + sideAddition, + bordered2.height() + addition, + VImage::option()->set("extend", "black")); + VImage frame; + if (top_text != "") { + frame = bordered3.join( + topImage.gravity(VIPS_COMPASS_DIRECTION_NORTH, bordered3.width(), + topImage.height() + (size / 4), + VImage::option()->set("extend", "black")), + VIPS_DIRECTION_VERTICAL, + VImage::option()->set("background", 0x000000)->set("expand", true)); + } + if (bottom_text != "") { + if (top_text == "") frame = bordered3; + frame = frame.join( + bottomImage.gravity(VIPS_COMPASS_DIRECTION_NORTH, bordered3.width(), + bottomImage.height() + (size / 4), + VImage::option()->set("extend", "black")), + VIPS_DIRECTION_VERTICAL, + VImage::option()->set("background", 0x000000)->set("expand", true)); + } + height = frame.height(); + img.push_back(frame); + } + VImage final = VImage::arrayjoin(img, VImage::option()->set("across", 1)) + .extract_band(0, VImage::option()->set("n", 3)); + final.set(VIPS_META_PAGE_HEIGHT, height); + + void *buf; + final.write_to_buffer( + ("." + *outType).c_str(), &buf, DataSize, + *outType == "gif" ? VImage::option()->set("dither", 1) : 0); + + return (char *)buf; +} diff --git a/natives/motivate.h b/natives/motivate.h index 7f7624b..422b0f7 100644 --- a/natives/motivate.h +++ b/natives/motivate.h @@ -1,8 +1,8 @@ -#pragma once - -#include "common.h" - -using std::string; - -char* Motivate(string type, string* outType, char* BufferData, size_t BufferLength, +#pragma once + +#include "common.h" + +using std::string; + +char* Motivate(string type, string* outType, char* BufferData, size_t BufferLength, ArgumentMap Arguments, size_t* DataSize); \ No newline at end of file diff --git a/natives/node/image.cc b/natives/node/image.cc index c3ecb9f..75613f3 100644 --- a/natives/node/image.cc +++ b/natives/node/image.cc @@ -1,124 +1,124 @@ -#include - -#include -#include -#include - -#include "../common.h" - -#ifdef _WIN32 -#include -#endif -#include - -using namespace std; - -bool isNapiValueInt(Napi::Env& env, Napi::Value& num) { - return env.Global() - .Get("Number") - .ToObject() - .Get("isInteger") - .As() - .Call({num}) - .ToBoolean() - .Value(); -} - -Napi::Value ProcessImage(const Napi::CallbackInfo& info) { - Napi::Env env = info.Env(); - Napi::Object result = Napi::Object::New(env); - - try { - string command = info[0].As().Utf8Value(); - Napi::Object obj = info[1].As(); - string type = - obj.Has("type") ? obj.Get("type").As().Utf8Value() : "png"; - - Napi::Array properties = obj.GetPropertyNames(); - - ArgumentMap Arguments; - - for (unsigned int i = 0; i < properties.Length(); i++) { - string property = - properties.Get(uint32_t(i)).As().Utf8Value(); - - if (property == "data") { - continue; - } - - auto val = obj.Get(property); - if (val.IsBoolean()) { - Arguments[property] = val.ToBoolean().Value(); - } else if (val.IsString()) { - Arguments[property] = val.ToString().As().Utf8Value(); - } else if (val.IsNumber()) { - auto num = val.ToNumber(); - if (isNapiValueInt(env, num)) { - Arguments[property] = num.Int32Value(); - } else { - Arguments[property] = num.FloatValue(); - } - } else { - throw "Unimplemented value type passed to image native."; - // Arguments[property] = val; - } - } - - string outType = GetArgument(Arguments, "togif") ? "gif" : type; - - size_t length = 0; - char* buf; - if (obj.Has("data")) { - Napi::Buffer data = obj.Has("data") - ? obj.Get("data").As>() - : Napi::Buffer::New(env, 0); - buf = FunctionMap.at(command)(type, &outType, data.Data(), data.Length(), - Arguments, &length); - } else { - buf = NoInputFunctionMap.at(command)(type, &outType, Arguments, &length); - } - - vips_error_clear(); - vips_thread_shutdown(); - - result.Set("data", - Napi::Buffer::New(env, buf, length, - []([[maybe_unused]] Napi::Env env, - void* data) { free(data); })); - result.Set("type", outType); - } catch (std::exception const& err) { - Napi::Error::New(env, err.what()).ThrowAsJavaScriptException(); - } catch (...) { - Napi::Error::New(env, "Unknown error").ThrowAsJavaScriptException(); - } - - return result; -} - -Napi::Object Init(Napi::Env env, Napi::Object exports) { -#ifdef _WIN32 - Magick::InitializeMagick(""); -#endif - if (vips_init("")) vips_error_exit(NULL); - exports.Set(Napi::String::New(env, "image"), - Napi::Function::New(env, ProcessImage)); // new function handler - - Napi::Array arr = Napi::Array::New(env); - size_t i = 0; - for (auto const& imap : FunctionMap) { - Napi::HandleScope scope(env); - arr[i] = Napi::String::New(env, imap.first); - i++; - } - for (auto const& imap : NoInputFunctionMap) { - Napi::HandleScope scope(env); - arr[i] = Napi::String::New(env, imap.first); - i++; - } - - exports.Set(Napi::String::New(env, "funcs"), arr); - - return exports; -} - -NODE_API_MODULE(addon, Init) +#include + +#include +#include +#include + +#include "../common.h" + +#ifdef _WIN32 +#include +#endif +#include + +using namespace std; + +bool isNapiValueInt(Napi::Env& env, Napi::Value& num) { + return env.Global() + .Get("Number") + .ToObject() + .Get("isInteger") + .As() + .Call({num}) + .ToBoolean() + .Value(); +} + +Napi::Value ProcessImage(const Napi::CallbackInfo& info) { + Napi::Env env = info.Env(); + Napi::Object result = Napi::Object::New(env); + + try { + string command = info[0].As().Utf8Value(); + Napi::Object obj = info[1].As(); + string type = + obj.Has("type") ? obj.Get("type").As().Utf8Value() : "png"; + + Napi::Array properties = obj.GetPropertyNames(); + + ArgumentMap Arguments; + + for (unsigned int i = 0; i < properties.Length(); i++) { + string property = + properties.Get(uint32_t(i)).As().Utf8Value(); + + if (property == "data") { + continue; + } + + auto val = obj.Get(property); + if (val.IsBoolean()) { + Arguments[property] = val.ToBoolean().Value(); + } else if (val.IsString()) { + Arguments[property] = val.ToString().As().Utf8Value(); + } else if (val.IsNumber()) { + auto num = val.ToNumber(); + if (isNapiValueInt(env, num)) { + Arguments[property] = num.Int32Value(); + } else { + Arguments[property] = num.FloatValue(); + } + } else { + throw "Unimplemented value type passed to image native."; + // Arguments[property] = val; + } + } + + string outType = GetArgument(Arguments, "togif") ? "gif" : type; + + size_t length = 0; + char* buf; + if (obj.Has("data")) { + Napi::Buffer data = obj.Has("data") + ? obj.Get("data").As>() + : Napi::Buffer::New(env, 0); + buf = FunctionMap.at(command)(type, &outType, data.Data(), data.Length(), + Arguments, &length); + } else { + buf = NoInputFunctionMap.at(command)(type, &outType, Arguments, &length); + } + + vips_error_clear(); + vips_thread_shutdown(); + + result.Set("data", + Napi::Buffer::New(env, buf, length, + []([[maybe_unused]] Napi::Env env, + void* data) { free(data); })); + result.Set("type", outType); + } catch (std::exception const& err) { + Napi::Error::New(env, err.what()).ThrowAsJavaScriptException(); + } catch (...) { + Napi::Error::New(env, "Unknown error").ThrowAsJavaScriptException(); + } + + return result; +} + +Napi::Object Init(Napi::Env env, Napi::Object exports) { +#ifdef _WIN32 + Magick::InitializeMagick(""); +#endif + if (vips_init("")) vips_error_exit(NULL); + exports.Set(Napi::String::New(env, "image"), + Napi::Function::New(env, ProcessImage)); // new function handler + + Napi::Array arr = Napi::Array::New(env); + size_t i = 0; + for (auto const& imap : FunctionMap) { + Napi::HandleScope scope(env); + arr[i] = Napi::String::New(env, imap.first); + i++; + } + for (auto const& imap : NoInputFunctionMap) { + Napi::HandleScope scope(env); + arr[i] = Napi::String::New(env, imap.first); + i++; + } + + exports.Set(Napi::String::New(env, "funcs"), arr); + + return exports; +} + +NODE_API_MODULE(addon, Init) diff --git a/natives/reddit.cc b/natives/reddit.cc index c89b6c7..805492c 100644 --- a/natives/reddit.cc +++ b/natives/reddit.cc @@ -1,67 +1,67 @@ -#include - -#include "common.h" - -using namespace std; -using namespace vips; - -char *Reddit(string type, string *outType, char *BufferData, - size_t BufferLength, ArgumentMap Arguments, size_t *DataSize) { - string text = GetArgument(Arguments, "text"); - string basePath = GetArgument(Arguments, "basePath"); - - VOption *options = VImage::option()->set("access", "sequential"); - - VImage in = - VImage::new_from_buffer(BufferData, BufferLength, "", - type == "gif" ? options->set("n", -1) : options) - .colourspace(VIPS_INTERPRETATION_sRGB); - if (!in.has_alpha()) in = in.bandjoin(255); - - string assetPath = basePath + "assets/images/reddit.png"; - VImage tmpl = VImage::new_from_file(assetPath.c_str()); - - int width = in.width(); - int pageHeight = vips_image_get_page_height(in.get_image()); - int nPages = vips_image_get_n_pages(in.get_image()); - - string captionText = "" + text + ""; - - VImage textImage = VImage::text( - ".", VImage::option()->set( - "fontfile", (basePath + "assets/fonts/reddit.ttf").c_str())); - textImage = VImage::text( - captionText.c_str(), - VImage::option() - ->set("rgba", true) - ->set("font", "Roboto, Twemoji Color Font 62") - ->set("fontfile", (basePath + "assets/fonts/twemoji.otf").c_str()) - ->set("align", VIPS_ALIGN_LOW)); - - VImage composited = - tmpl.composite2(textImage, VIPS_BLEND_MODE_OVER, - VImage::option()->set("x", 375)->set( - "y", (tmpl.height() - textImage.height()) - 64)); - VImage watermark = - composited.resize((double)width / (double)composited.width()); - - vector img; - for (int i = 0; i < nPages; i++) { - VImage img_frame = - type == "gif" ? in.crop(0, i * pageHeight, width, pageHeight) : in; - VImage frame = img_frame.join(watermark, VIPS_DIRECTION_VERTICAL, - VImage::option()->set("expand", true)); - img.push_back(frame); - } - VImage final = VImage::arrayjoin(img, VImage::option()->set("across", 1)); - final.set(VIPS_META_PAGE_HEIGHT, pageHeight + watermark.height()); - - void *buf; - final.write_to_buffer( - ("." + *outType).c_str(), &buf, DataSize, - *outType == "gif" - ? VImage::option()->set("dither", 0)->set("reoptimise", 1) - : 0); - - return (char *)buf; +#include + +#include "common.h" + +using namespace std; +using namespace vips; + +char *Reddit(string type, string *outType, char *BufferData, + size_t BufferLength, ArgumentMap Arguments, size_t *DataSize) { + string text = GetArgument(Arguments, "text"); + string basePath = GetArgument(Arguments, "basePath"); + + VOption *options = VImage::option()->set("access", "sequential"); + + VImage in = + VImage::new_from_buffer(BufferData, BufferLength, "", + type == "gif" ? options->set("n", -1) : options) + .colourspace(VIPS_INTERPRETATION_sRGB); + if (!in.has_alpha()) in = in.bandjoin(255); + + string assetPath = basePath + "assets/images/reddit.png"; + VImage tmpl = VImage::new_from_file(assetPath.c_str()); + + int width = in.width(); + int pageHeight = vips_image_get_page_height(in.get_image()); + int nPages = vips_image_get_n_pages(in.get_image()); + + string captionText = "" + text + ""; + + VImage textImage = VImage::text( + ".", VImage::option()->set( + "fontfile", (basePath + "assets/fonts/reddit.ttf").c_str())); + textImage = VImage::text( + captionText.c_str(), + VImage::option() + ->set("rgba", true) + ->set("font", "Roboto, Twemoji Color Font 62") + ->set("fontfile", (basePath + "assets/fonts/twemoji.otf").c_str()) + ->set("align", VIPS_ALIGN_LOW)); + + VImage composited = + tmpl.composite2(textImage, VIPS_BLEND_MODE_OVER, + VImage::option()->set("x", 375)->set( + "y", (tmpl.height() - textImage.height()) - 64)); + VImage watermark = + composited.resize((double)width / (double)composited.width()); + + vector img; + for (int i = 0; i < nPages; i++) { + VImage img_frame = + type == "gif" ? in.crop(0, i * pageHeight, width, pageHeight) : in; + VImage frame = img_frame.join(watermark, VIPS_DIRECTION_VERTICAL, + VImage::option()->set("expand", true)); + img.push_back(frame); + } + VImage final = VImage::arrayjoin(img, VImage::option()->set("across", 1)); + final.set(VIPS_META_PAGE_HEIGHT, pageHeight + watermark.height()); + + void *buf; + final.write_to_buffer( + ("." + *outType).c_str(), &buf, DataSize, + *outType == "gif" + ? VImage::option()->set("dither", 0)->set("reoptimise", 1) + : 0); + + return (char *)buf; } \ No newline at end of file diff --git a/natives/reddit.h b/natives/reddit.h index 564341f..5c79cc0 100644 --- a/natives/reddit.h +++ b/natives/reddit.h @@ -1,8 +1,8 @@ -#pragma once - -#include "common.h" - -using std::string; - -char* Reddit(string type, string* outType, char* BufferData, size_t BufferLength, +#pragma once + +#include "common.h" + +using std::string; + +char* Reddit(string type, string* outType, char* BufferData, size_t BufferLength, ArgumentMap Arguments, size_t* DataSize); \ No newline at end of file diff --git a/natives/resize.cc b/natives/resize.cc index ae9f93e..4e3eee7 100644 --- a/natives/resize.cc +++ b/natives/resize.cc @@ -1,54 +1,54 @@ -#include - -#include "common.h" - -using namespace std; -using namespace vips; - -char *Resize(string type, string *outType, char *BufferData, - size_t BufferLength, ArgumentMap Arguments, size_t *DataSize) { - bool stretch = GetArgumentWithFallback(Arguments, "stretch", false); - bool wide = GetArgumentWithFallback(Arguments, "wide", false); - - VOption *options = VImage::option()->set("access", "sequential"); - - VImage in = - VImage::new_from_buffer(BufferData, BufferLength, "", - type == "gif" ? options->set("n", -1) : options) - .colourspace(VIPS_INTERPRETATION_sRGB); - - VImage out; - - int width = in.width(); - int pageHeight = vips_image_get_page_height(in.get_image()); - int nPages = vips_image_get_n_pages(in.get_image()); - - int finalHeight; - if (stretch) { - out = - in.resize(512.0 / (double)width, - VImage::option()->set("vscale", 512.0 / (double)pageHeight)); - finalHeight = 512; - } else if (wide) { - out = in.resize(9.5, VImage::option()->set("vscale", 0.5)); - finalHeight = pageHeight / 2; - } else { - // Pain. Pain. Pain. Pain. Pain. - vector img; - for (int i = 0; i < nPages; i++) { - VImage img_frame = - type == "gif" ? in.crop(0, i * pageHeight, width, pageHeight) : in; - VImage resized = img_frame.resize(0.1).resize( - 10, VImage::option()->set("kernel", VIPS_KERNEL_NEAREST)); - img.push_back(resized); - finalHeight = resized.height(); - } - out = VImage::arrayjoin(img, VImage::option()->set("across", 1)); - } - out.set(VIPS_META_PAGE_HEIGHT, finalHeight); - - void *buf; - out.write_to_buffer(("." + *outType).c_str(), &buf, DataSize); - - return (char *)buf; +#include + +#include "common.h" + +using namespace std; +using namespace vips; + +char *Resize(string type, string *outType, char *BufferData, + size_t BufferLength, ArgumentMap Arguments, size_t *DataSize) { + bool stretch = GetArgumentWithFallback(Arguments, "stretch", false); + bool wide = GetArgumentWithFallback(Arguments, "wide", false); + + VOption *options = VImage::option()->set("access", "sequential"); + + VImage in = + VImage::new_from_buffer(BufferData, BufferLength, "", + type == "gif" ? options->set("n", -1) : options) + .colourspace(VIPS_INTERPRETATION_sRGB); + + VImage out; + + int width = in.width(); + int pageHeight = vips_image_get_page_height(in.get_image()); + int nPages = vips_image_get_n_pages(in.get_image()); + + int finalHeight; + if (stretch) { + out = + in.resize(512.0 / (double)width, + VImage::option()->set("vscale", 512.0 / (double)pageHeight)); + finalHeight = 512; + } else if (wide) { + out = in.resize(9.5, VImage::option()->set("vscale", 0.5)); + finalHeight = pageHeight / 2; + } else { + // Pain. Pain. Pain. Pain. Pain. + vector img; + for (int i = 0; i < nPages; i++) { + VImage img_frame = + type == "gif" ? in.crop(0, i * pageHeight, width, pageHeight) : in; + VImage resized = img_frame.resize(0.1).resize( + 10, VImage::option()->set("kernel", VIPS_KERNEL_NEAREST)); + img.push_back(resized); + finalHeight = resized.height(); + } + out = VImage::arrayjoin(img, VImage::option()->set("across", 1)); + } + out.set(VIPS_META_PAGE_HEIGHT, finalHeight); + + void *buf; + out.write_to_buffer(("." + *outType).c_str(), &buf, DataSize); + + return (char *)buf; } \ No newline at end of file diff --git a/natives/resize.h b/natives/resize.h index 61bc26d..171b248 100644 --- a/natives/resize.h +++ b/natives/resize.h @@ -1,8 +1,8 @@ -#pragma once - -#include "common.h" - -using std::string; - -char* Resize(string type, string* outType, char* BufferData, size_t BufferLength, +#pragma once + +#include "common.h" + +using std::string; + +char* Resize(string type, string* outType, char* BufferData, size_t BufferLength, ArgumentMap Arguments, size_t* DataSize); \ No newline at end of file diff --git a/natives/reverse.cc b/natives/reverse.cc index 6444bd0..48b162f 100644 --- a/natives/reverse.cc +++ b/natives/reverse.cc @@ -1,59 +1,59 @@ -#include -#include - -#include "common.h" - -using namespace std; -using namespace vips; - -char *Reverse(string type, string *outType, char *BufferData, - size_t BufferLength, ArgumentMap Arguments, size_t *DataSize) { - bool soos = GetArgumentWithFallback(Arguments, "soos", false); - - VOption *options = - VImage::option()->set("access", "sequential")->set("n", -1); - - VImage in = VImage::new_from_buffer(BufferData, BufferLength, "", options) - .colourspace(VIPS_INTERPRETATION_sRGB); - - int width = in.width(); - int pageHeight = vips_image_get_page_height(in.get_image()); - int nPages = vips_image_get_n_pages(in.get_image()); - - vector split; - // todo: find a better way of getting individual frames (or at least getting - // the frames in reverse order) - for (int i = 0; i < nPages; i++) { - VImage img_frame = in.crop(0, i * pageHeight, width, pageHeight); - split.push_back(img_frame); - } - - vector delays = in.get_array_int("delay"); - if (soos) { - vector copy = split; - vector copy2 = delays; - reverse(copy.begin(), copy.end()); - reverse(copy2.begin(), copy2.end()); - copy.pop_back(); - copy2.pop_back(); - copy.erase(copy.begin()); - copy2.erase(copy2.begin()); - split.insert(split.end(), copy.begin(), copy.end()); - delays.insert(delays.end(), copy2.begin(), copy2.end()); - } else { - reverse(split.begin(), split.end()); - reverse(delays.begin(), delays.end()); - } - - VImage final = VImage::arrayjoin(split, VImage::option()->set("across", 1)); - final.set(VIPS_META_PAGE_HEIGHT, pageHeight); - final.set("delay", delays); - - void *buf; - final.write_to_buffer(".gif", &buf, DataSize, - VImage::option()->set("dither", 0)); - - *outType = "gif"; - - return (char *)buf; +#include +#include + +#include "common.h" + +using namespace std; +using namespace vips; + +char *Reverse(string type, string *outType, char *BufferData, + size_t BufferLength, ArgumentMap Arguments, size_t *DataSize) { + bool soos = GetArgumentWithFallback(Arguments, "soos", false); + + VOption *options = + VImage::option()->set("access", "sequential")->set("n", -1); + + VImage in = VImage::new_from_buffer(BufferData, BufferLength, "", options) + .colourspace(VIPS_INTERPRETATION_sRGB); + + int width = in.width(); + int pageHeight = vips_image_get_page_height(in.get_image()); + int nPages = vips_image_get_n_pages(in.get_image()); + + vector split; + // todo: find a better way of getting individual frames (or at least getting + // the frames in reverse order) + for (int i = 0; i < nPages; i++) { + VImage img_frame = in.crop(0, i * pageHeight, width, pageHeight); + split.push_back(img_frame); + } + + vector delays = in.get_array_int("delay"); + if (soos) { + vector copy = split; + vector copy2 = delays; + reverse(copy.begin(), copy.end()); + reverse(copy2.begin(), copy2.end()); + copy.pop_back(); + copy2.pop_back(); + copy.erase(copy.begin()); + copy2.erase(copy2.begin()); + split.insert(split.end(), copy.begin(), copy.end()); + delays.insert(delays.end(), copy2.begin(), copy2.end()); + } else { + reverse(split.begin(), split.end()); + reverse(delays.begin(), delays.end()); + } + + VImage final = VImage::arrayjoin(split, VImage::option()->set("across", 1)); + final.set(VIPS_META_PAGE_HEIGHT, pageHeight); + final.set("delay", delays); + + void *buf; + final.write_to_buffer(".gif", &buf, DataSize, + VImage::option()->set("dither", 0)); + + *outType = "gif"; + + return (char *)buf; } \ No newline at end of file diff --git a/natives/reverse.h b/natives/reverse.h index 9a7db85..55d2ac5 100644 --- a/natives/reverse.h +++ b/natives/reverse.h @@ -1,8 +1,8 @@ -#pragma once - -#include "common.h" - -using std::string; - -char* Reverse(string type, string* outType, char* BufferData, size_t BufferLength, +#pragma once + +#include "common.h" + +using std::string; + +char* Reverse(string type, string* outType, char* BufferData, size_t BufferLength, ArgumentMap Arguments, size_t* DataSize); \ No newline at end of file diff --git a/natives/scott.cc b/natives/scott.cc index d6e6ea6..f281024 100644 --- a/natives/scott.cc +++ b/natives/scott.cc @@ -1,55 +1,55 @@ -#include - -#include "common.h" - -using namespace std; -using namespace vips; - -char *Scott(string type, string *outType, char *BufferData, size_t BufferLength, - ArgumentMap Arguments, size_t *DataSize) { - string basePath = GetArgument(Arguments, "basePath"); - - VOption *options = VImage::option()->set("access", "sequential"); - - VImage in = - VImage::new_from_buffer(BufferData, BufferLength, "", - type == "gif" ? options->set("n", -1) : options) - .colourspace(VIPS_INTERPRETATION_sRGB); - if (!in.has_alpha()) in = in.bandjoin(255); - - int width = in.width(); - int pageHeight = vips_image_get_page_height(in.get_image()); - int nPages = vips_image_get_n_pages(in.get_image()); - - string assetPath = basePath + "assets/images/scott.png"; - VImage bg = VImage::new_from_file(assetPath.c_str()); - - string distortPath = basePath + "assets/images/scottmap.png"; - VImage distort = VImage::new_from_file(distortPath.c_str()); - - VImage distortImage = - ((distort[1] / 255) * 414).bandjoin((distort[0] / 255) * 233); - - vector img; - for (int i = 0; i < nPages; i++) { - VImage img_frame = - type == "gif" ? in.crop(0, i * pageHeight, width, pageHeight) : in; - VImage resized = img_frame.resize( - 415 / (double)width, - VImage::option()->set("vscale", 234 / (double)pageHeight)); - VImage mapped = resized.mapim(distortImage) - .extract_band(0, VImage::option()->set("n", 3)) - .bandjoin(distort[2]); - VImage offset = mapped.embed(127, 181, 864, 481); - VImage composited = bg.composite2(offset, VIPS_BLEND_MODE_OVER); - img.push_back(composited); - } - VImage final = VImage::arrayjoin(img, VImage::option()->set("across", 1)); - final.set(VIPS_META_PAGE_HEIGHT, 481); - - void *buf; - final.write_to_buffer( - ("." + *outType).c_str(), &buf, DataSize, - *outType == "gif" ? VImage::option()->set("dither", 1) : 0); - return (char *)buf; +#include + +#include "common.h" + +using namespace std; +using namespace vips; + +char *Scott(string type, string *outType, char *BufferData, size_t BufferLength, + ArgumentMap Arguments, size_t *DataSize) { + string basePath = GetArgument(Arguments, "basePath"); + + VOption *options = VImage::option()->set("access", "sequential"); + + VImage in = + VImage::new_from_buffer(BufferData, BufferLength, "", + type == "gif" ? options->set("n", -1) : options) + .colourspace(VIPS_INTERPRETATION_sRGB); + if (!in.has_alpha()) in = in.bandjoin(255); + + int width = in.width(); + int pageHeight = vips_image_get_page_height(in.get_image()); + int nPages = vips_image_get_n_pages(in.get_image()); + + string assetPath = basePath + "assets/images/scott.png"; + VImage bg = VImage::new_from_file(assetPath.c_str()); + + string distortPath = basePath + "assets/images/scottmap.png"; + VImage distort = VImage::new_from_file(distortPath.c_str()); + + VImage distortImage = + ((distort[1] / 255) * 414).bandjoin((distort[0] / 255) * 233); + + vector img; + for (int i = 0; i < nPages; i++) { + VImage img_frame = + type == "gif" ? in.crop(0, i * pageHeight, width, pageHeight) : in; + VImage resized = img_frame.resize( + 415 / (double)width, + VImage::option()->set("vscale", 234 / (double)pageHeight)); + VImage mapped = resized.mapim(distortImage) + .extract_band(0, VImage::option()->set("n", 3)) + .bandjoin(distort[2]); + VImage offset = mapped.embed(127, 181, 864, 481); + VImage composited = bg.composite2(offset, VIPS_BLEND_MODE_OVER); + img.push_back(composited); + } + VImage final = VImage::arrayjoin(img, VImage::option()->set("across", 1)); + final.set(VIPS_META_PAGE_HEIGHT, 481); + + void *buf; + final.write_to_buffer( + ("." + *outType).c_str(), &buf, DataSize, + *outType == "gif" ? VImage::option()->set("dither", 1) : 0); + return (char *)buf; } \ No newline at end of file diff --git a/natives/scott.h b/natives/scott.h index 45313af..6197037 100644 --- a/natives/scott.h +++ b/natives/scott.h @@ -1,8 +1,8 @@ -#pragma once - -#include "common.h" - -using std::string; - -char* Scott(string type, string* outType, char* BufferData, size_t BufferLength, +#pragma once + +#include "common.h" + +using std::string; + +char* Scott(string type, string* outType, char* BufferData, size_t BufferLength, ArgumentMap Arguments, size_t* DataSize); \ No newline at end of file diff --git a/natives/snapchat.cc b/natives/snapchat.cc index 7b5a812..c129d37 100644 --- a/natives/snapchat.cc +++ b/natives/snapchat.cc @@ -1,72 +1,72 @@ -#include - -#include "common.h" - -using namespace std; -using namespace vips; - -char *Snapchat(string type, string *outType, char *BufferData, - size_t BufferLength, ArgumentMap Arguments, size_t *DataSize) { - string caption = GetArgument(Arguments, "caption"); - float pos = GetArgumentWithFallback(Arguments, "pos", 0.5); - string basePath = GetArgument(Arguments, "basePath"); - - VOption *options = VImage::option()->set("access", "sequential"); - - VImage in = - VImage::new_from_buffer(BufferData, BufferLength, "", - type == "gif" ? options->set("n", -1) : options) - .colourspace(VIPS_INTERPRETATION_sRGB); - if (!in.has_alpha()) in = in.bandjoin(255); - - int width = in.width(); - int pageHeight = vips_image_get_page_height(in.get_image()); - int nPages = vips_image_get_n_pages(in.get_image()); - int size = width / 20; - int textWidth = width - ((width / 25) * 2); - - string font_string = "Helvetica Neue, Twemoji Color Font " + to_string(size); - - VImage textIn = VImage::text( - ".", VImage::option()->set( - "fontfile", (basePath + "assets/fonts/caption2.ttf").c_str())); - textIn = VImage::text( - ("" + caption + - "") - .c_str(), - VImage::option() - ->set("rgba", true) - ->set("align", VIPS_ALIGN_CENTRE) - ->set("font", font_string.c_str()) - ->set("fontfile", (basePath + "assets/fonts/twemoji.otf").c_str()) - ->set("width", textWidth)); - int bgHeight = textIn.height() + (width / 25); - textIn = ((textIn == (vector){0, 0, 0, 0}).bandand()) - .ifthenelse({0, 0, 0, 178}, textIn) - .embed((width / 2) - (textIn.width() / 2), - (bgHeight / 2) - (textIn.height() / 2), width, bgHeight, - VImage::option() - ->set("extend", "background") - ->set("background", (vector){0, 0, 0, 178})); - - vector img; - for (int i = 0; i < nPages; i++) { - VImage img_frame = - type == "gif" ? in.crop(0, i * pageHeight, width, pageHeight) : in; - img_frame = img_frame.composite2( - textIn, VIPS_BLEND_MODE_OVER, - VImage::option()->set("x", 0)->set("y", pageHeight * pos)); - img.push_back(img_frame); - } - VImage final = VImage::arrayjoin(img, VImage::option()->set("across", 1)); - final.set(VIPS_META_PAGE_HEIGHT, pageHeight); - - void *buf; - final.write_to_buffer( - ("." + *outType).c_str(), &buf, DataSize, - *outType == "gif" - ? VImage::option()->set("dither", 0)->set("reoptimise", 1) - : 0); - - return (char *)buf; +#include + +#include "common.h" + +using namespace std; +using namespace vips; + +char *Snapchat(string type, string *outType, char *BufferData, + size_t BufferLength, ArgumentMap Arguments, size_t *DataSize) { + string caption = GetArgument(Arguments, "caption"); + float pos = GetArgumentWithFallback(Arguments, "pos", 0.5); + string basePath = GetArgument(Arguments, "basePath"); + + VOption *options = VImage::option()->set("access", "sequential"); + + VImage in = + VImage::new_from_buffer(BufferData, BufferLength, "", + type == "gif" ? options->set("n", -1) : options) + .colourspace(VIPS_INTERPRETATION_sRGB); + if (!in.has_alpha()) in = in.bandjoin(255); + + int width = in.width(); + int pageHeight = vips_image_get_page_height(in.get_image()); + int nPages = vips_image_get_n_pages(in.get_image()); + int size = width / 20; + int textWidth = width - ((width / 25) * 2); + + string font_string = "Helvetica Neue, Twemoji Color Font " + to_string(size); + + VImage textIn = VImage::text( + ".", VImage::option()->set( + "fontfile", (basePath + "assets/fonts/caption2.ttf").c_str())); + textIn = VImage::text( + ("" + caption + + "") + .c_str(), + VImage::option() + ->set("rgba", true) + ->set("align", VIPS_ALIGN_CENTRE) + ->set("font", font_string.c_str()) + ->set("fontfile", (basePath + "assets/fonts/twemoji.otf").c_str()) + ->set("width", textWidth)); + int bgHeight = textIn.height() + (width / 25); + textIn = ((textIn == (vector){0, 0, 0, 0}).bandand()) + .ifthenelse({0, 0, 0, 178}, textIn) + .embed((width / 2) - (textIn.width() / 2), + (bgHeight / 2) - (textIn.height() / 2), width, bgHeight, + VImage::option() + ->set("extend", "background") + ->set("background", (vector){0, 0, 0, 178})); + + vector img; + for (int i = 0; i < nPages; i++) { + VImage img_frame = + type == "gif" ? in.crop(0, i * pageHeight, width, pageHeight) : in; + img_frame = img_frame.composite2( + textIn, VIPS_BLEND_MODE_OVER, + VImage::option()->set("x", 0)->set("y", pageHeight * pos)); + img.push_back(img_frame); + } + VImage final = VImage::arrayjoin(img, VImage::option()->set("across", 1)); + final.set(VIPS_META_PAGE_HEIGHT, pageHeight); + + void *buf; + final.write_to_buffer( + ("." + *outType).c_str(), &buf, DataSize, + *outType == "gif" + ? VImage::option()->set("dither", 0)->set("reoptimise", 1) + : 0); + + return (char *)buf; } \ No newline at end of file diff --git a/natives/snapchat.h b/natives/snapchat.h index a43a4a3..713589e 100644 --- a/natives/snapchat.h +++ b/natives/snapchat.h @@ -1,8 +1,8 @@ -#pragma once - -#include "common.h" - -using std::string; - -char* Snapchat(string type, string* outType, char* BufferData, size_t BufferLength, +#pragma once + +#include "common.h" + +using std::string; + +char* Snapchat(string type, string* outType, char* BufferData, size_t BufferLength, ArgumentMap Arguments, size_t* DataSize); \ No newline at end of file diff --git a/natives/sonic.cc b/natives/sonic.cc index 581700c..eafdd22 100644 --- a/natives/sonic.cc +++ b/natives/sonic.cc @@ -1,35 +1,35 @@ -#include - -#include "common.h" - -using namespace std; -using namespace vips; - -char *Sonic(string type, string *outType, ArgumentMap Arguments, - size_t *DataSize) { - string text = GetArgument(Arguments, "text"); - string basePath = GetArgument(Arguments, "basePath"); - - string assetPath = basePath + "assets/images/sonic.jpg"; - VImage bg = VImage::new_from_file(assetPath.c_str()); - - VImage textImage = - VImage::text( - ("" + text + "").c_str(), - VImage::option() - ->set("rgba", true) - ->set("align", VIPS_ALIGN_CENTRE) - ->set("font", "Verdana, Twemoji Color Font") - ->set("fontfile", (basePath + "assets/fonts/twemoji.otf").c_str()) - ->set("width", 542) - ->set("height", 390)) - .gravity(VIPS_COMPASS_DIRECTION_CENTRE, 542, 390); - - VImage out = bg.composite2(textImage, VIPS_BLEND_MODE_OVER, - VImage::option()->set("x", 391)->set("y", 84)); - - void *buf; - out.write_to_buffer(("." + *outType).c_str(), &buf, DataSize); - - return (char *)buf; +#include + +#include "common.h" + +using namespace std; +using namespace vips; + +char *Sonic(string type, string *outType, ArgumentMap Arguments, + size_t *DataSize) { + string text = GetArgument(Arguments, "text"); + string basePath = GetArgument(Arguments, "basePath"); + + string assetPath = basePath + "assets/images/sonic.jpg"; + VImage bg = VImage::new_from_file(assetPath.c_str()); + + VImage textImage = + VImage::text( + ("" + text + "").c_str(), + VImage::option() + ->set("rgba", true) + ->set("align", VIPS_ALIGN_CENTRE) + ->set("font", "Verdana, Twemoji Color Font") + ->set("fontfile", (basePath + "assets/fonts/twemoji.otf").c_str()) + ->set("width", 542) + ->set("height", 390)) + .gravity(VIPS_COMPASS_DIRECTION_CENTRE, 542, 390); + + VImage out = bg.composite2(textImage, VIPS_BLEND_MODE_OVER, + VImage::option()->set("x", 391)->set("y", 84)); + + void *buf; + out.write_to_buffer(("." + *outType).c_str(), &buf, DataSize); + + return (char *)buf; } \ No newline at end of file diff --git a/natives/sonic.h b/natives/sonic.h index 6b3df86..af988f1 100644 --- a/natives/sonic.h +++ b/natives/sonic.h @@ -1,7 +1,7 @@ -#pragma once - -#include "common.h" - -using std::string; - +#pragma once + +#include "common.h" + +using std::string; + char *Sonic(string type, string *outType, ArgumentMap Arguments, size_t *DataSize); \ No newline at end of file diff --git a/natives/speed.cc b/natives/speed.cc index f7fc116..166236f 100644 --- a/natives/speed.cc +++ b/natives/speed.cc @@ -1,102 +1,102 @@ -#include -#include -#include - -#include "common.h" - -using namespace std; -using namespace vips; - -void *memset16(void *m, uint16_t val, size_t count) { - uint16_t *buf = (uint16_t *)m; - - while (count--) *buf++ = val; - return m; -} - -char *vipsRemove(char *data, size_t length, size_t *DataSize, int speed) { - VOption *options = VImage::option()->set("access", "sequential"); - - VImage in = VImage::new_from_buffer(data, length, "", options->set("n", -1)) - .colourspace(VIPS_INTERPRETATION_sRGB); - if (!in.has_alpha()) in = in.bandjoin(255); - - int width = in.width(); - int pageHeight = vips_image_get_page_height(in.get_image()); - int nPages = vips_image_get_n_pages(in.get_image()); - - vector img; - for (int i = 0; i < nPages; i += speed) { - VImage img_frame = in.crop(0, i * pageHeight, width, pageHeight); - img.push_back(img_frame); - } - VImage out = VImage::arrayjoin(img, VImage::option()->set("across", 1)); - out.set(VIPS_META_PAGE_HEIGHT, pageHeight); - - void *buf; - out.write_to_buffer(".gif", &buf, DataSize); - - return (char *)buf; -} - -char *Speed([[maybe_unused]] string type, string *outType, char *BufferData, - size_t BufferLength, ArgumentMap Arguments, size_t *DataSize) { - bool slow = GetArgumentWithFallback(Arguments, "slow", false); - int speed = GetArgumentWithFallback(Arguments, "speed", 2); - - char *fileData = (char *)malloc(BufferLength); - memcpy(fileData, BufferData, BufferLength); - - char *match = (char *)"\x00\x21\xF9\x04"; - - vector old_delays; - bool removeFrames = false; - char *lastPos; - - // int amount = 0; - - lastPos = (char *)memchr(fileData, '\x00', BufferLength); - while (lastPos != NULL) { - if (memcmp(lastPos, match, 4) != 0) { - lastPos = (char *)memchr(lastPos + 1, '\x00', - (BufferLength - (lastPos - fileData)) - 1); - continue; - } - //++amount; - uint16_t old_delay; - memcpy(&old_delay, lastPos + 5, 2); - old_delays.push_back(old_delay); - lastPos = (char *)memchr(lastPos + 1, '\x00', - (BufferLength - (lastPos - fileData)) - 1); - } - - int currentFrame = 0; - lastPos = (char *)memchr(fileData, '\x00', BufferLength); - while (lastPos != NULL) { - if (memcmp(lastPos, match, 4) != 0) { - lastPos = (char *)memchr(lastPos + 1, '\x00', - (BufferLength - (lastPos - fileData)) - 1); - continue; - } - uint16_t new_delay = slow ? old_delays[currentFrame] * speed - : old_delays[currentFrame] / speed; - if (!slow && new_delay <= 1) { - removeFrames = true; - break; - } - - memset16(lastPos + 5, new_delay, 1); - - lastPos = (char *)memchr(lastPos + 1, '\x00', - (BufferLength - (lastPos - fileData)) - 1); - ++currentFrame; - } - - if (removeFrames) { - fileData = vipsRemove(BufferData, BufferLength, DataSize, speed); - } else { - *DataSize = BufferLength; - } - - return fileData; +#include +#include +#include + +#include "common.h" + +using namespace std; +using namespace vips; + +void *memset16(void *m, uint16_t val, size_t count) { + uint16_t *buf = (uint16_t *)m; + + while (count--) *buf++ = val; + return m; +} + +char *vipsRemove(char *data, size_t length, size_t *DataSize, int speed) { + VOption *options = VImage::option()->set("access", "sequential"); + + VImage in = VImage::new_from_buffer(data, length, "", options->set("n", -1)) + .colourspace(VIPS_INTERPRETATION_sRGB); + if (!in.has_alpha()) in = in.bandjoin(255); + + int width = in.width(); + int pageHeight = vips_image_get_page_height(in.get_image()); + int nPages = vips_image_get_n_pages(in.get_image()); + + vector img; + for (int i = 0; i < nPages; i += speed) { + VImage img_frame = in.crop(0, i * pageHeight, width, pageHeight); + img.push_back(img_frame); + } + VImage out = VImage::arrayjoin(img, VImage::option()->set("across", 1)); + out.set(VIPS_META_PAGE_HEIGHT, pageHeight); + + void *buf; + out.write_to_buffer(".gif", &buf, DataSize); + + return (char *)buf; +} + +char *Speed([[maybe_unused]] string type, string *outType, char *BufferData, + size_t BufferLength, ArgumentMap Arguments, size_t *DataSize) { + bool slow = GetArgumentWithFallback(Arguments, "slow", false); + int speed = GetArgumentWithFallback(Arguments, "speed", 2); + + char *fileData = (char *)malloc(BufferLength); + memcpy(fileData, BufferData, BufferLength); + + char *match = (char *)"\x00\x21\xF9\x04"; + + vector old_delays; + bool removeFrames = false; + char *lastPos; + + // int amount = 0; + + lastPos = (char *)memchr(fileData, '\x00', BufferLength); + while (lastPos != NULL) { + if (memcmp(lastPos, match, 4) != 0) { + lastPos = (char *)memchr(lastPos + 1, '\x00', + (BufferLength - (lastPos - fileData)) - 1); + continue; + } + //++amount; + uint16_t old_delay; + memcpy(&old_delay, lastPos + 5, 2); + old_delays.push_back(old_delay); + lastPos = (char *)memchr(lastPos + 1, '\x00', + (BufferLength - (lastPos - fileData)) - 1); + } + + int currentFrame = 0; + lastPos = (char *)memchr(fileData, '\x00', BufferLength); + while (lastPos != NULL) { + if (memcmp(lastPos, match, 4) != 0) { + lastPos = (char *)memchr(lastPos + 1, '\x00', + (BufferLength - (lastPos - fileData)) - 1); + continue; + } + uint16_t new_delay = slow ? old_delays[currentFrame] * speed + : old_delays[currentFrame] / speed; + if (!slow && new_delay <= 1) { + removeFrames = true; + break; + } + + memset16(lastPos + 5, new_delay, 1); + + lastPos = (char *)memchr(lastPos + 1, '\x00', + (BufferLength - (lastPos - fileData)) - 1); + ++currentFrame; + } + + if (removeFrames) { + fileData = vipsRemove(BufferData, BufferLength, DataSize, speed); + } else { + *DataSize = BufferLength; + } + + return fileData; } \ No newline at end of file diff --git a/natives/speed.h b/natives/speed.h index 0c861ae..c101447 100644 --- a/natives/speed.h +++ b/natives/speed.h @@ -1,8 +1,8 @@ -#pragma once - -#include "common.h" - -using std::string; - -char* Speed(string type, string* outType, char* BufferData, size_t BufferLength, +#pragma once + +#include "common.h" + +using std::string; + +char* Speed(string type, string* outType, char* BufferData, size_t BufferLength, ArgumentMap Arguments, size_t* DataSize); \ No newline at end of file diff --git a/natives/spin.cc b/natives/spin.cc index 2c1d07e..1fdde8a 100644 --- a/natives/spin.cc +++ b/natives/spin.cc @@ -1,72 +1,72 @@ -#include - -#include -#include -#include - -#include "common.h" - -using namespace std; -using namespace Magick; - -char *Spin(string type, string *outType, char *BufferData, size_t BufferLength, - [[maybe_unused]] ArgumentMap Arguments, size_t *DataSize) { - int delay = GetArgumentWithFallback(Arguments, "delay", 0); - - Blob blob; - - list frames; - list coalesced; - list mid; - try { - readImages(&frames, Blob(BufferData, BufferLength)); - } catch (Magick::WarningCoder &warning) { - cerr << "Coder Warning: " << warning.what() << endl; - } catch (Magick::Warning &warning) { - cerr << "Warning: " << warning.what() << endl; - } - coalesceImages(&coalesced, frames.begin(), frames.end()); - - if (type != "gif") { - list::iterator it = coalesced.begin(); - for (int i = 0; i < 29; ++i) { - coalesced.push_back(*it); - } - } - - int i = 0; - for (Image &image : coalesced) { - image.virtualPixelMethod(Magick::TransparentVirtualPixelMethod); - image.scale(Geometry("256x256")); - image.alphaChannel(Magick::SetAlphaChannel); - double rotation[1] = {(double)360 * i / coalesced.size()}; - image.distort(Magick::ScaleRotateTranslateDistortion, 1, rotation); - image.magick("GIF"); - mid.push_back(image); - i++; - } - - for_each(mid.begin(), mid.end(), - gifDisposeMethodImage(Magick::BackgroundDispose)); - - optimizeTransparency(mid.begin(), mid.end()); - if (delay != 0) { - for_each(mid.begin(), mid.end(), animationDelayImage(delay)); - } else if (type != "gif") { - for_each(mid.begin(), mid.end(), animationDelayImage(5)); - } - - for (Image &image : mid) { - image.quantizeDitherMethod(FloydSteinbergDitherMethod); - image.quantize(); - } - - writeImages(mid.begin(), mid.end(), &blob); - - *outType = "gif"; - *DataSize = blob.length(); - - char *data = (char *)malloc(*DataSize); - memcpy(data, blob.data(), *DataSize); - return data; -} +#include + +#include +#include +#include + +#include "common.h" + +using namespace std; +using namespace Magick; + +char *Spin(string type, string *outType, char *BufferData, size_t BufferLength, + [[maybe_unused]] ArgumentMap Arguments, size_t *DataSize) { + int delay = GetArgumentWithFallback(Arguments, "delay", 0); + + Blob blob; + + list frames; + list coalesced; + list mid; + try { + readImages(&frames, Blob(BufferData, BufferLength)); + } catch (Magick::WarningCoder &warning) { + cerr << "Coder Warning: " << warning.what() << endl; + } catch (Magick::Warning &warning) { + cerr << "Warning: " << warning.what() << endl; + } + coalesceImages(&coalesced, frames.begin(), frames.end()); + + if (type != "gif") { + list::iterator it = coalesced.begin(); + for (int i = 0; i < 29; ++i) { + coalesced.push_back(*it); + } + } + + int i = 0; + for (Image &image : coalesced) { + image.virtualPixelMethod(Magick::TransparentVirtualPixelMethod); + image.scale(Geometry("256x256")); + image.alphaChannel(Magick::SetAlphaChannel); + double rotation[1] = {(double)360 * i / coalesced.size()}; + image.distort(Magick::ScaleRotateTranslateDistortion, 1, rotation); + image.magick("GIF"); + mid.push_back(image); + i++; + } + + for_each(mid.begin(), mid.end(), + gifDisposeMethodImage(Magick::BackgroundDispose)); + + optimizeTransparency(mid.begin(), mid.end()); + if (delay != 0) { + for_each(mid.begin(), mid.end(), animationDelayImage(delay)); + } else if (type != "gif") { + for_each(mid.begin(), mid.end(), animationDelayImage(5)); + } + + for (Image &image : mid) { + image.quantizeDitherMethod(FloydSteinbergDitherMethod); + image.quantize(); + } + + writeImages(mid.begin(), mid.end(), &blob); + + *outType = "gif"; + *DataSize = blob.length(); + + char *data = (char *)malloc(*DataSize); + memcpy(data, blob.data(), *DataSize); + return data; +} diff --git a/natives/spin.h b/natives/spin.h index 74f16b8..423c5b2 100644 --- a/natives/spin.h +++ b/natives/spin.h @@ -1,8 +1,8 @@ -#pragma once - -#include "common.h" - -using std::string; - -char* Spin(string type, string* outType, char* BufferData, size_t BufferLength, +#pragma once + +#include "common.h" + +using std::string; + +char* Spin(string type, string* outType, char* BufferData, size_t BufferLength, ArgumentMap Arguments, size_t* DataSize); \ No newline at end of file diff --git a/natives/squish.cc b/natives/squish.cc index a28463b..a64da94 100644 --- a/natives/squish.cc +++ b/natives/squish.cc @@ -1,52 +1,52 @@ -#include - -#include - -#include "common.h" - -using namespace std; -using namespace vips; - -char *Squish(string type, string *outType, char *BufferData, - size_t BufferLength, [[maybe_unused]] ArgumentMap Arguments, - size_t *DataSize) { - VOption *options = VImage::option(); - - VImage in = - VImage::new_from_buffer( - BufferData, BufferLength, "", - type == "gif" ? options->set("n", -1)->set("access", "sequential") - : options) - .colourspace(VIPS_INTERPRETATION_sRGB); - if (!in.has_alpha()) in = in.bandjoin(255); - - int width = in.width(); - int pageHeight = vips_image_get_page_height(in.get_image()); - int nPages = type == "gif" ? vips_image_get_n_pages(in.get_image()) : 30; - double mult = (2 * M_PI) / nPages; - - vector img; - for (int i = 0; i < nPages; i++) { - VImage img_frame = - type == "gif" ? in.crop(0, i * pageHeight, width, pageHeight) : in; - double newWidth = (sin(i * mult) / 4) + 0.75; - double newHeight = (cos(i * mult) / 4) + 0.75; - VImage resized = - img_frame.resize(newWidth, VImage::option()->set("vscale", newHeight)) - .gravity(VIPS_COMPASS_DIRECTION_CENTRE, width, pageHeight); - img.push_back(resized); - } - VImage final = VImage::arrayjoin(img, VImage::option()->set("across", 1)); - final.set(VIPS_META_PAGE_HEIGHT, pageHeight); - if (type != "gif") { - vector delay(30, 50); - final.set("delay", delay); - } - - void *buf; - final.write_to_buffer(".gif", &buf, DataSize); - - *outType = "gif"; - - return (char *)buf; +#include + +#include + +#include "common.h" + +using namespace std; +using namespace vips; + +char *Squish(string type, string *outType, char *BufferData, + size_t BufferLength, [[maybe_unused]] ArgumentMap Arguments, + size_t *DataSize) { + VOption *options = VImage::option(); + + VImage in = + VImage::new_from_buffer( + BufferData, BufferLength, "", + type == "gif" ? options->set("n", -1)->set("access", "sequential") + : options) + .colourspace(VIPS_INTERPRETATION_sRGB); + if (!in.has_alpha()) in = in.bandjoin(255); + + int width = in.width(); + int pageHeight = vips_image_get_page_height(in.get_image()); + int nPages = type == "gif" ? vips_image_get_n_pages(in.get_image()) : 30; + double mult = (2 * M_PI) / nPages; + + vector img; + for (int i = 0; i < nPages; i++) { + VImage img_frame = + type == "gif" ? in.crop(0, i * pageHeight, width, pageHeight) : in; + double newWidth = (sin(i * mult) / 4) + 0.75; + double newHeight = (cos(i * mult) / 4) + 0.75; + VImage resized = + img_frame.resize(newWidth, VImage::option()->set("vscale", newHeight)) + .gravity(VIPS_COMPASS_DIRECTION_CENTRE, width, pageHeight); + img.push_back(resized); + } + VImage final = VImage::arrayjoin(img, VImage::option()->set("across", 1)); + final.set(VIPS_META_PAGE_HEIGHT, pageHeight); + if (type != "gif") { + vector delay(30, 50); + final.set("delay", delay); + } + + void *buf; + final.write_to_buffer(".gif", &buf, DataSize); + + *outType = "gif"; + + return (char *)buf; } \ No newline at end of file diff --git a/natives/squish.h b/natives/squish.h index 47c5118..b145b64 100644 --- a/natives/squish.h +++ b/natives/squish.h @@ -1,8 +1,8 @@ -#pragma once - -#include "common.h" - -using std::string; - -char* Squish(string type, string* outType, char* BufferData, size_t BufferLength, +#pragma once + +#include "common.h" + +using std::string; + +char* Squish(string type, string* outType, char* BufferData, size_t BufferLength, ArgumentMap Arguments, size_t* DataSize); \ No newline at end of file diff --git a/natives/swirl.cc b/natives/swirl.cc index 8ae3be4..ac58ae1 100644 --- a/natives/swirl.cc +++ b/natives/swirl.cc @@ -1,76 +1,76 @@ -#include - -#include "common.h" - -using namespace std; -using namespace vips; - -char *Swirl(string type, string *outType, char *BufferData, size_t BufferLength, - [[maybe_unused]] ArgumentMap Arguments, size_t *DataSize) { - VOption *options = VImage::option()->set("access", "sequential"); - - VImage in = - VImage::new_from_buffer(BufferData, BufferLength, "", - type == "gif" ? options->set("n", -1) : options) - .colourspace(VIPS_INTERPRETATION_sRGB); - if (!in.has_alpha()) in = in.bandjoin(255); - - int pageHeight = vips_image_get_page_height(in.get_image()); - int nPages = vips_image_get_n_pages(in.get_image()); - int width = in.width(); - double newWidth = width * 3; - double newHeight = pageHeight * 3; - vector divSize = {newWidth / 2, newHeight / 2}; - - VImage index = VImage::xyz(newWidth, newHeight); - VImage center = index - divSize; - VImage polar = center - .copy(VImage::option() - ->set("format", VIPS_FORMAT_COMPLEX) - ->set("bands", 1)) - .polar() - .copy(VImage::option() - ->set("format", VIPS_FORMAT_FLOAT) - ->set("bands", 2)); - - int size = min(width, pageHeight) / 2; - - VImage test = (1 - polar.extract_band(0) / size); - VImage degrees = test.cast(VIPS_FORMAT_FLOAT).pow(2); - - VImage angle = polar.extract_band(1) + degrees * 180; - - VImage distortion = polar.extract_band(0) - .bandjoin(angle) - .copy(VImage::option() - ->set("format", VIPS_FORMAT_COMPLEX) - ->set("bands", 1)) - .rect() - .copy(VImage::option() - ->set("format", VIPS_FORMAT_FLOAT) - ->set("bands", 2)) + - divSize; - - vector img; - for (int i = 0; i < nPages; i++) { - VImage img_frame = - type == "gif" ? in.crop(0, i * pageHeight, width, pageHeight) : in; - - VImage distort = - img_frame - .gravity(VIPS_COMPASS_DIRECTION_CENTRE, newWidth, newHeight, - VImage::option()->set("extend", VIPS_EXTEND_COPY)) - .mapim(distortion, - VImage::option()->set( - "interpolate", VInterpolate::new_from_name("bicubic"))); - VImage frame = distort.crop(width, pageHeight, width, pageHeight); - img.push_back(frame); - } - VImage final = VImage::arrayjoin(img, VImage::option()->set("across", 1)); - final.set(VIPS_META_PAGE_HEIGHT, pageHeight); - - void *buf; - final.write_to_buffer(("." + *outType).c_str(), &buf, DataSize); - - return (char *)buf; +#include + +#include "common.h" + +using namespace std; +using namespace vips; + +char *Swirl(string type, string *outType, char *BufferData, size_t BufferLength, + [[maybe_unused]] ArgumentMap Arguments, size_t *DataSize) { + VOption *options = VImage::option()->set("access", "sequential"); + + VImage in = + VImage::new_from_buffer(BufferData, BufferLength, "", + type == "gif" ? options->set("n", -1) : options) + .colourspace(VIPS_INTERPRETATION_sRGB); + if (!in.has_alpha()) in = in.bandjoin(255); + + int pageHeight = vips_image_get_page_height(in.get_image()); + int nPages = vips_image_get_n_pages(in.get_image()); + int width = in.width(); + double newWidth = width * 3; + double newHeight = pageHeight * 3; + vector divSize = {newWidth / 2, newHeight / 2}; + + VImage index = VImage::xyz(newWidth, newHeight); + VImage center = index - divSize; + VImage polar = center + .copy(VImage::option() + ->set("format", VIPS_FORMAT_COMPLEX) + ->set("bands", 1)) + .polar() + .copy(VImage::option() + ->set("format", VIPS_FORMAT_FLOAT) + ->set("bands", 2)); + + int size = min(width, pageHeight) / 2; + + VImage test = (1 - polar.extract_band(0) / size); + VImage degrees = test.cast(VIPS_FORMAT_FLOAT).pow(2); + + VImage angle = polar.extract_band(1) + degrees * 180; + + VImage distortion = polar.extract_band(0) + .bandjoin(angle) + .copy(VImage::option() + ->set("format", VIPS_FORMAT_COMPLEX) + ->set("bands", 1)) + .rect() + .copy(VImage::option() + ->set("format", VIPS_FORMAT_FLOAT) + ->set("bands", 2)) + + divSize; + + vector img; + for (int i = 0; i < nPages; i++) { + VImage img_frame = + type == "gif" ? in.crop(0, i * pageHeight, width, pageHeight) : in; + + VImage distort = + img_frame + .gravity(VIPS_COMPASS_DIRECTION_CENTRE, newWidth, newHeight, + VImage::option()->set("extend", VIPS_EXTEND_COPY)) + .mapim(distortion, + VImage::option()->set( + "interpolate", VInterpolate::new_from_name("bicubic"))); + VImage frame = distort.crop(width, pageHeight, width, pageHeight); + img.push_back(frame); + } + VImage final = VImage::arrayjoin(img, VImage::option()->set("across", 1)); + final.set(VIPS_META_PAGE_HEIGHT, pageHeight); + + void *buf; + final.write_to_buffer(("." + *outType).c_str(), &buf, DataSize); + + return (char *)buf; } \ No newline at end of file diff --git a/natives/swirl.h b/natives/swirl.h index fecb25c..536314f 100644 --- a/natives/swirl.h +++ b/natives/swirl.h @@ -1,8 +1,8 @@ -#pragma once - -#include "common.h" - -using std::string; - -char* Swirl(string type, string* outType, char* BufferData, size_t BufferLength, +#pragma once + +#include "common.h" + +using std::string; + +char* Swirl(string type, string* outType, char* BufferData, size_t BufferLength, ArgumentMap Arguments, size_t* DataSize); \ No newline at end of file diff --git a/natives/tile.cc b/natives/tile.cc index 30feb7b..6f1c0a3 100644 --- a/natives/tile.cc +++ b/natives/tile.cc @@ -1,65 +1,65 @@ -#include - -#include -#include -#include - -#include "common.h" - -using namespace std; -using namespace Magick; - -char *Tile(string type, string *outType, char *BufferData, size_t BufferLength, - [[maybe_unused]] ArgumentMap Arguments, size_t *DataSize) { - Blob blob; - - list frames; - list coalesced; - list mid; - try { - readImages(&frames, Blob(BufferData, BufferLength)); - } catch (Magick::WarningCoder &warning) { - cerr << "Coder Warning: " << warning.what() << endl; - } catch (Magick::Warning &warning) { - cerr << "Warning: " << warning.what() << endl; - } - coalesceImages(&coalesced, frames.begin(), frames.end()); - - for (Image &image : coalesced) { - list duplicated; - Image appended; - list montage; - Image frame; - image.magick(*outType); - for (int i = 0; i < 5; ++i) { - duplicated.push_back(image); - } - appendImages(&appended, duplicated.begin(), duplicated.end()); - appended.repage(); - for (int i = 0; i < 5; ++i) { - montage.push_back(appended); - } - appendImages(&frame, montage.begin(), montage.end(), true); - frame.repage(); - frame.scale(Geometry("800x800>")); - frame.animationDelay(image.animationDelay()); - mid.push_back(frame); - } - - optimizeTransparency(mid.begin(), mid.end()); - - if (*outType == "gif") { - for (Image &image : mid) { - image.quantizeDitherMethod(FloydSteinbergDitherMethod); - image.quantize(); - } - } - - writeImages(mid.begin(), mid.end(), &blob); - - *DataSize = blob.length(); - - char *data = (char *)malloc(*DataSize); - memcpy(data, blob.data(), *DataSize); - return data; +#include + +#include +#include +#include + +#include "common.h" + +using namespace std; +using namespace Magick; + +char *Tile(string type, string *outType, char *BufferData, size_t BufferLength, + [[maybe_unused]] ArgumentMap Arguments, size_t *DataSize) { + Blob blob; + + list frames; + list coalesced; + list mid; + try { + readImages(&frames, Blob(BufferData, BufferLength)); + } catch (Magick::WarningCoder &warning) { + cerr << "Coder Warning: " << warning.what() << endl; + } catch (Magick::Warning &warning) { + cerr << "Warning: " << warning.what() << endl; + } + coalesceImages(&coalesced, frames.begin(), frames.end()); + + for (Image &image : coalesced) { + list duplicated; + Image appended; + list montage; + Image frame; + image.magick(*outType); + for (int i = 0; i < 5; ++i) { + duplicated.push_back(image); + } + appendImages(&appended, duplicated.begin(), duplicated.end()); + appended.repage(); + for (int i = 0; i < 5; ++i) { + montage.push_back(appended); + } + appendImages(&frame, montage.begin(), montage.end(), true); + frame.repage(); + frame.scale(Geometry("800x800>")); + frame.animationDelay(image.animationDelay()); + mid.push_back(frame); + } + + optimizeTransparency(mid.begin(), mid.end()); + + if (*outType == "gif") { + for (Image &image : mid) { + image.quantizeDitherMethod(FloydSteinbergDitherMethod); + image.quantize(); + } + } + + writeImages(mid.begin(), mid.end(), &blob); + + *DataSize = blob.length(); + + char *data = (char *)malloc(*DataSize); + memcpy(data, blob.data(), *DataSize); + return data; } \ No newline at end of file diff --git a/natives/tile.h b/natives/tile.h index a1fe22b..929b9dc 100644 --- a/natives/tile.h +++ b/natives/tile.h @@ -1,8 +1,8 @@ -#pragma once - -#include "common.h" - -using std::string; - -char* Tile(string type, string* outType, char* BufferData, size_t BufferLength, +#pragma once + +#include "common.h" + +using std::string; + +char* Tile(string type, string* outType, char* BufferData, size_t BufferLength, ArgumentMap Arguments, size_t* DataSize); \ No newline at end of file diff --git a/natives/togif.cc b/natives/togif.cc index 678b537..7119254 100644 --- a/natives/togif.cc +++ b/natives/togif.cc @@ -1,28 +1,28 @@ -#include - -#include "common.h" - -using namespace std; -using namespace vips; - -char *ToGif(string type, string *outType, char *BufferData, size_t BufferLength, - [[maybe_unused]] ArgumentMap Arguments, size_t *DataSize) { - if (type == "gif") { - *DataSize = BufferLength; - char *data = (char *)malloc(BufferLength); - memcpy(data, BufferData, BufferLength); - return data; - } else { - VOption *options = VImage::option()->set("access", "sequential"); - - VImage in = VImage::new_from_buffer( - BufferData, BufferLength, "", - type == "webp" ? options->set("n", -1) : options); - - void *buf; - in.write_to_buffer(".gif", &buf, DataSize); - *outType = "gif"; - - return (char *)buf; - } +#include + +#include "common.h" + +using namespace std; +using namespace vips; + +char *ToGif(string type, string *outType, char *BufferData, size_t BufferLength, + [[maybe_unused]] ArgumentMap Arguments, size_t *DataSize) { + if (type == "gif") { + *DataSize = BufferLength; + char *data = (char *)malloc(BufferLength); + memcpy(data, BufferData, BufferLength); + return data; + } else { + VOption *options = VImage::option()->set("access", "sequential"); + + VImage in = VImage::new_from_buffer( + BufferData, BufferLength, "", + type == "webp" ? options->set("n", -1) : options); + + void *buf; + in.write_to_buffer(".gif", &buf, DataSize); + *outType = "gif"; + + return (char *)buf; + } } \ No newline at end of file diff --git a/natives/togif.h b/natives/togif.h index 5e3f8c0..0cf3fc1 100644 --- a/natives/togif.h +++ b/natives/togif.h @@ -1,8 +1,8 @@ -#pragma once - -#include "common.h" - -using std::string; - -char* ToGif(string type, string* outType, char* BufferData, size_t BufferLength, +#pragma once + +#include "common.h" + +using std::string; + +char* ToGif(string type, string* outType, char* BufferData, size_t BufferLength, ArgumentMap Arguments, size_t* DataSize); \ No newline at end of file diff --git a/natives/uncanny.cc b/natives/uncanny.cc index 16a922b..79534f2 100644 --- a/natives/uncanny.cc +++ b/natives/uncanny.cc @@ -1,101 +1,101 @@ -#include - -#include "common.h" - -using namespace std; -using namespace vips; - -char *Uncanny(string type, string *outType, char *BufferData, - size_t BufferLength, ArgumentMap Arguments, size_t *DataSize) { - string caption = GetArgument(Arguments, "caption"); - string caption2 = GetArgument(Arguments, "caption2"); - string font = GetArgument(Arguments, "font"); - string path = GetArgument(Arguments, "path"); - string basePath = GetArgument(Arguments, "basePath"); - - VOption *options = VImage::option()->set("access", "sequential"); - - VImage in = - VImage::new_from_buffer(BufferData, BufferLength, "", - type == "gif" ? options->set("n", -1) : options) - .colourspace(VIPS_INTERPRETATION_sRGB) - .extract_band(0, VImage::option()->set("n", 3)); - - VImage base = VImage::black(1280, 720, VImage::option()->set("bands", 3)); - - string font_string = (font == "roboto" ? "Roboto Condensed" : font) + - ", Twemoji Color Font " + - (font != "impact" ? "bold" : "normal") + " 72"; - - string captionText = - "" + caption + ""; - string caption2Text = - "" + caption2 + ""; - - auto findResult = fontPaths.find(font); - if (findResult != fontPaths.end()) { - VImage::text(".", VImage::option()->set( - "fontfile", (basePath + findResult->second).c_str())); - } - - VImage text = VImage::text( - captionText.c_str(), - VImage::option() - ->set("rgba", true) - ->set("align", VIPS_ALIGN_CENTRE) - ->set("font", font_string.c_str()) - ->set("fontfile", (basePath + "assets/fonts/twemoji.otf").c_str()) - ->set("width", 588) - ->set("height", 90)); - VImage captionImage = - text.extract_band(0, VImage::option()->set("n", 3)) - .gravity(VIPS_COMPASS_DIRECTION_CENTRE, 640, text.height() + 40, - VImage::option()->set("extend", "black")); - - VImage text2 = VImage::text( - caption2Text.c_str(), - VImage::option() - ->set("rgba", true) - ->set("align", VIPS_ALIGN_CENTRE) - ->set("font", font_string.c_str()) - ->set("fontfile", (basePath + "assets/fonts/twemoji.otf").c_str()) - ->set("width", 588) - ->set("height", 90)); - VImage caption2Image = - text2.extract_band(0, VImage::option()->set("n", 3)) - .gravity(VIPS_COMPASS_DIRECTION_CENTRE, 640, text.height() + 40, - VImage::option()->set("extend", "black")); - - base = base.insert(captionImage, 0, 0).insert(caption2Image, 640, 0); - - int width = in.width(); - int pageHeight = vips_image_get_page_height(in.get_image()); - int nPages = vips_image_get_n_pages(in.get_image()); - - VImage uncanny = VImage::new_from_file((basePath + path).c_str()); - - base = base.insert(uncanny, 0, 130); - - vector img; - for (int i = 0; i < nPages; i++) { - VImage img_frame = - type == "gif" ? in.crop(0, i * pageHeight, width, pageHeight) : in; - VImage resized = img_frame.resize(690.0 / (double)width); - if (resized.height() > 590) { - double vscale = 590.0 / (double)resized.height(); - resized = resized.resize(vscale, VImage::option()->set("vscale", vscale)); - } - VImage composited = base.insert(resized, 935 - (resized.width() / 2), - 425 - (resized.height() / 2)); - img.push_back(composited); - } - VImage final = VImage::arrayjoin(img, VImage::option()->set("across", 1)); - final.set(VIPS_META_PAGE_HEIGHT, 720); - - void *buf; - final.write_to_buffer( - ("." + *outType).c_str(), &buf, DataSize, - *outType == "gif" ? VImage::option()->set("reoptimise", 1) : 0); - - return (char *)buf; +#include + +#include "common.h" + +using namespace std; +using namespace vips; + +char *Uncanny(string type, string *outType, char *BufferData, + size_t BufferLength, ArgumentMap Arguments, size_t *DataSize) { + string caption = GetArgument(Arguments, "caption"); + string caption2 = GetArgument(Arguments, "caption2"); + string font = GetArgument(Arguments, "font"); + string path = GetArgument(Arguments, "path"); + string basePath = GetArgument(Arguments, "basePath"); + + VOption *options = VImage::option()->set("access", "sequential"); + + VImage in = + VImage::new_from_buffer(BufferData, BufferLength, "", + type == "gif" ? options->set("n", -1) : options) + .colourspace(VIPS_INTERPRETATION_sRGB) + .extract_band(0, VImage::option()->set("n", 3)); + + VImage base = VImage::black(1280, 720, VImage::option()->set("bands", 3)); + + string font_string = (font == "roboto" ? "Roboto Condensed" : font) + + ", Twemoji Color Font " + + (font != "impact" ? "bold" : "normal") + " 72"; + + string captionText = + "" + caption + ""; + string caption2Text = + "" + caption2 + ""; + + auto findResult = fontPaths.find(font); + if (findResult != fontPaths.end()) { + VImage::text(".", VImage::option()->set( + "fontfile", (basePath + findResult->second).c_str())); + } + + VImage text = VImage::text( + captionText.c_str(), + VImage::option() + ->set("rgba", true) + ->set("align", VIPS_ALIGN_CENTRE) + ->set("font", font_string.c_str()) + ->set("fontfile", (basePath + "assets/fonts/twemoji.otf").c_str()) + ->set("width", 588) + ->set("height", 90)); + VImage captionImage = + text.extract_band(0, VImage::option()->set("n", 3)) + .gravity(VIPS_COMPASS_DIRECTION_CENTRE, 640, text.height() + 40, + VImage::option()->set("extend", "black")); + + VImage text2 = VImage::text( + caption2Text.c_str(), + VImage::option() + ->set("rgba", true) + ->set("align", VIPS_ALIGN_CENTRE) + ->set("font", font_string.c_str()) + ->set("fontfile", (basePath + "assets/fonts/twemoji.otf").c_str()) + ->set("width", 588) + ->set("height", 90)); + VImage caption2Image = + text2.extract_band(0, VImage::option()->set("n", 3)) + .gravity(VIPS_COMPASS_DIRECTION_CENTRE, 640, text.height() + 40, + VImage::option()->set("extend", "black")); + + base = base.insert(captionImage, 0, 0).insert(caption2Image, 640, 0); + + int width = in.width(); + int pageHeight = vips_image_get_page_height(in.get_image()); + int nPages = vips_image_get_n_pages(in.get_image()); + + VImage uncanny = VImage::new_from_file((basePath + path).c_str()); + + base = base.insert(uncanny, 0, 130); + + vector img; + for (int i = 0; i < nPages; i++) { + VImage img_frame = + type == "gif" ? in.crop(0, i * pageHeight, width, pageHeight) : in; + VImage resized = img_frame.resize(690.0 / (double)width); + if (resized.height() > 590) { + double vscale = 590.0 / (double)resized.height(); + resized = resized.resize(vscale, VImage::option()->set("vscale", vscale)); + } + VImage composited = base.insert(resized, 935 - (resized.width() / 2), + 425 - (resized.height() / 2)); + img.push_back(composited); + } + VImage final = VImage::arrayjoin(img, VImage::option()->set("across", 1)); + final.set(VIPS_META_PAGE_HEIGHT, 720); + + void *buf; + final.write_to_buffer( + ("." + *outType).c_str(), &buf, DataSize, + *outType == "gif" ? VImage::option()->set("reoptimise", 1) : 0); + + return (char *)buf; } \ No newline at end of file diff --git a/natives/uncanny.h b/natives/uncanny.h index d9c34e5..f7e975d 100644 --- a/natives/uncanny.h +++ b/natives/uncanny.h @@ -1,8 +1,8 @@ -#pragma once - -#include "common.h" - -using std::string; - -char* Uncanny(string type, string* outType, char* BufferData, size_t BufferLength, +#pragma once + +#include "common.h" + +using std::string; + +char* Uncanny(string type, string* outType, char* BufferData, size_t BufferLength, ArgumentMap Arguments, size_t* DataSize); \ No newline at end of file diff --git a/natives/uncaption.cc b/natives/uncaption.cc index 0145d87..2da184b 100644 --- a/natives/uncaption.cc +++ b/natives/uncaption.cc @@ -1,54 +1,54 @@ -#include -#include - -#include "common.h" - -using namespace std; -using namespace vips; - -char *Uncaption(string type, string *outType, char *BufferData, - size_t BufferLength, ArgumentMap Arguments, size_t *DataSize) { - float tolerance = GetArgumentWithFallback(Arguments, "tolerance", 0.5); - - VOption *options = VImage::option(); - - VImage in = - VImage::new_from_buffer( - BufferData, BufferLength, "", - type == "gif" ? options->set("n", -1)->set("access", "sequential") - : options) - .colourspace(VIPS_INTERPRETATION_sRGB); - if (!in.has_alpha()) in = in.bandjoin(255); - - int width = in.width(); - int pageHeight = vips_image_get_page_height(in.get_image()); - int nPages = vips_image_get_n_pages(in.get_image()); - - VImage first = - in.crop(0, 0, 3, pageHeight).colourspace(VIPS_INTERPRETATION_B_W) > - (255 * tolerance); - int top, captionWidth, captionHeight; - first.find_trim(&top, &captionWidth, &captionHeight); - - vector img; - int newHeight = pageHeight - top; - if (top == pageHeight) { - newHeight = pageHeight; - top = 0; - } - for (int i = 0; i < nPages; i++) { - VImage img_frame = in.crop(0, (i * pageHeight) + top, width, newHeight); - img.push_back(img_frame); - } - VImage final = VImage::arrayjoin(img, VImage::option()->set("across", 1)); - final.set(VIPS_META_PAGE_HEIGHT, newHeight); - - void *buf; - final.write_to_buffer( - ("." + *outType).c_str(), &buf, DataSize, - *outType == "gif" - ? VImage::option()->set("dither", 0)->set("reoptimise", 1) - : 0); - - return (char *)buf; -} +#include +#include + +#include "common.h" + +using namespace std; +using namespace vips; + +char *Uncaption(string type, string *outType, char *BufferData, + size_t BufferLength, ArgumentMap Arguments, size_t *DataSize) { + float tolerance = GetArgumentWithFallback(Arguments, "tolerance", 0.5); + + VOption *options = VImage::option(); + + VImage in = + VImage::new_from_buffer( + BufferData, BufferLength, "", + type == "gif" ? options->set("n", -1)->set("access", "sequential") + : options) + .colourspace(VIPS_INTERPRETATION_sRGB); + if (!in.has_alpha()) in = in.bandjoin(255); + + int width = in.width(); + int pageHeight = vips_image_get_page_height(in.get_image()); + int nPages = vips_image_get_n_pages(in.get_image()); + + VImage first = + in.crop(0, 0, 3, pageHeight).colourspace(VIPS_INTERPRETATION_B_W) > + (255 * tolerance); + int top, captionWidth, captionHeight; + first.find_trim(&top, &captionWidth, &captionHeight); + + vector img; + int newHeight = pageHeight - top; + if (top == pageHeight) { + newHeight = pageHeight; + top = 0; + } + for (int i = 0; i < nPages; i++) { + VImage img_frame = in.crop(0, (i * pageHeight) + top, width, newHeight); + img.push_back(img_frame); + } + VImage final = VImage::arrayjoin(img, VImage::option()->set("across", 1)); + final.set(VIPS_META_PAGE_HEIGHT, newHeight); + + void *buf; + final.write_to_buffer( + ("." + *outType).c_str(), &buf, DataSize, + *outType == "gif" + ? VImage::option()->set("dither", 0)->set("reoptimise", 1) + : 0); + + return (char *)buf; +} diff --git a/natives/uncaption.h b/natives/uncaption.h index 74b9f4d..6a283ff 100644 --- a/natives/uncaption.h +++ b/natives/uncaption.h @@ -1,8 +1,8 @@ -#pragma once - -#include "common.h" - -using std::string; - -char* Uncaption(string type, string* outType, char* BufferData, size_t BufferLength, +#pragma once + +#include "common.h" + +using std::string; + +char* Uncaption(string type, string* outType, char* BufferData, size_t BufferLength, ArgumentMap Arguments, size_t* DataSize); \ No newline at end of file diff --git a/natives/wall.cc b/natives/wall.cc index aa87fc9..d1dc800 100644 --- a/natives/wall.cc +++ b/natives/wall.cc @@ -1,58 +1,58 @@ -#include - -#include -#include -#include - -#include "common.h" - -using namespace std; -using namespace Magick; - -char *Wall(string type, string *outType, char *BufferData, size_t BufferLength, - [[maybe_unused]] ArgumentMap Arguments, size_t *DataSize) { - Blob blob; - - list frames; - list coalesced; - list mid; - try { - readImages(&frames, Blob(BufferData, BufferLength)); - } catch (Magick::WarningCoder &warning) { - cerr << "Coder Warning: " << warning.what() << endl; - } catch (Magick::Warning &warning) { - cerr << "Warning: " << warning.what() << endl; - } - coalesceImages(&coalesced, frames.begin(), frames.end()); - - for (Image &image : coalesced) { - image.resize(Geometry("128x128")); - image.virtualPixelMethod(Magick::TileVirtualPixelMethod); - image.matteColor("none"); - image.backgroundColor("none"); - image.scale(Geometry("512x512")); - double arguments[16] = {0, 0, 57, 42, 0, 128, 63, 130, - 128, 0, 140, 60, 128, 128, 140, 140}; - image.distort(Magick::PerspectiveDistortion, 16, arguments); - image.scale(Geometry("800x800>")); - image.magick(*outType); - mid.push_back(image); - } - - optimizeTransparency(mid.begin(), mid.end()); - - if (*outType == "gif") { - for (Image &image : mid) { - image.quantizeDitherMethod(FloydSteinbergDitherMethod); - image.quantize(); - } - } - - writeImages(mid.begin(), mid.end(), &blob); - - *DataSize = blob.length(); - - char *data = (char *)malloc(*DataSize); - memcpy(data, blob.data(), *DataSize); - return data; +#include + +#include +#include +#include + +#include "common.h" + +using namespace std; +using namespace Magick; + +char *Wall(string type, string *outType, char *BufferData, size_t BufferLength, + [[maybe_unused]] ArgumentMap Arguments, size_t *DataSize) { + Blob blob; + + list frames; + list coalesced; + list mid; + try { + readImages(&frames, Blob(BufferData, BufferLength)); + } catch (Magick::WarningCoder &warning) { + cerr << "Coder Warning: " << warning.what() << endl; + } catch (Magick::Warning &warning) { + cerr << "Warning: " << warning.what() << endl; + } + coalesceImages(&coalesced, frames.begin(), frames.end()); + + for (Image &image : coalesced) { + image.resize(Geometry("128x128")); + image.virtualPixelMethod(Magick::TileVirtualPixelMethod); + image.matteColor("none"); + image.backgroundColor("none"); + image.scale(Geometry("512x512")); + double arguments[16] = {0, 0, 57, 42, 0, 128, 63, 130, + 128, 0, 140, 60, 128, 128, 140, 140}; + image.distort(Magick::PerspectiveDistortion, 16, arguments); + image.scale(Geometry("800x800>")); + image.magick(*outType); + mid.push_back(image); + } + + optimizeTransparency(mid.begin(), mid.end()); + + if (*outType == "gif") { + for (Image &image : mid) { + image.quantizeDitherMethod(FloydSteinbergDitherMethod); + image.quantize(); + } + } + + writeImages(mid.begin(), mid.end(), &blob); + + *DataSize = blob.length(); + + char *data = (char *)malloc(*DataSize); + memcpy(data, blob.data(), *DataSize); + return data; } \ No newline at end of file diff --git a/natives/wall.h b/natives/wall.h index 93119d3..917fc99 100644 --- a/natives/wall.h +++ b/natives/wall.h @@ -1,8 +1,8 @@ -#pragma once - -#include "common.h" - -using std::string; - -char* Wall(string type, string* outType, char* BufferData, size_t BufferLength, +#pragma once + +#include "common.h" + +using std::string; + +char* Wall(string type, string* outType, char* BufferData, size_t BufferLength, ArgumentMap Arguments, size_t* DataSize); \ No newline at end of file diff --git a/natives/watermark.cc b/natives/watermark.cc index 7193067..4b0ac4e 100644 --- a/natives/watermark.cc +++ b/natives/watermark.cc @@ -1,154 +1,154 @@ -#include -#include - -#include "common.h" - -using namespace std; -using namespace vips; - -char *Watermark(string type, string *outType, char *BufferData, - size_t BufferLength, ArgumentMap Arguments, size_t *DataSize) { - string water = GetArgument(Arguments, "water"); - int gravity = GetArgument(Arguments, "gravity"); - - bool resize = GetArgumentWithFallback(Arguments, "resize", false); - - float yscale = GetArgumentWithFallback(Arguments, "yscale", false); - - bool append = GetArgumentWithFallback(Arguments, "append", false); - - bool alpha = GetArgumentWithFallback(Arguments, "alpha", false); - bool flip = GetArgumentWithFallback(Arguments, "flip", false); - - bool mc = MAP_HAS(Arguments, "mc"); - - string basePath = GetArgument(Arguments, "basePath"); - - VOption *options = VImage::option()->set("access", "sequential"); - - VImage in = - VImage::new_from_buffer(BufferData, BufferLength, "", - type == "gif" ? options->set("n", -1) : options) - .colourspace(VIPS_INTERPRETATION_sRGB); - if (!in.has_alpha()) in = in.bandjoin(255); - - string merged = basePath + water; - VImage watermark = VImage::new_from_file(merged.c_str()); - - int width = in.width(); - int pageHeight = vips_image_get_page_height(in.get_image()); - int nPages = vips_image_get_n_pages(in.get_image()); - - if (flip) { - watermark = watermark.flip(VIPS_DIRECTION_HORIZONTAL); - } - - if (resize && append) { - watermark = watermark.resize((double)width / (double)watermark.width()); - } else if (resize && yscale) { - watermark = watermark.resize( - (double)width / (double)watermark.width(), - VImage::option()->set("vscale", (double)(pageHeight * yscale) / - (double)watermark.height())); - } else if (resize) { - watermark = - watermark.resize((double)pageHeight / (double)watermark.height()); - } - - int x = 0, y = 0; - switch (gravity) { - case 1: - break; - case 2: - x = (width / 2) - (watermark.width() / 2); - break; - case 3: - x = width - watermark.width(); - break; - case 5: - x = (width / 2) - (watermark.width() / 2); - y = (pageHeight / 2) - (watermark.height() / 2); - break; - case 6: - x = width - watermark.width(); - y = (pageHeight / 2) - (watermark.height() / 2); - break; - case 8: - x = (width / 2) - (watermark.width() / 2); - y = pageHeight - watermark.height(); - break; - case 9: - x = width - watermark.width(); - y = pageHeight - watermark.height(); - break; - } - - vector img; - int addedHeight = 0; - VImage contentAlpha; - VImage frameAlpha; - VImage bg; - VImage frame; - for (int i = 0; i < nPages; i++) { - VImage img_frame = - type == "gif" ? in.crop(0, i * pageHeight, width, pageHeight) : in; - if (append) { - VImage appended = img_frame.join(watermark, VIPS_DIRECTION_VERTICAL, - VImage::option()->set("expand", true)); - addedHeight = watermark.height(); - img.push_back(appended); - } else if (mc) { - VImage padded = - img_frame.embed(0, 0, width, pageHeight + 15, - VImage::option()->set("background", 0xffffff)); - VImage composited = - padded.composite2(watermark, VIPS_BLEND_MODE_OVER, - VImage::option() - ->set("x", width - 190) - ->set("y", padded.height() - 22)); - addedHeight = 15; - img.push_back(composited); - } else { - VImage composited; - if (alpha) { - if (i == 0) { - contentAlpha = watermark.extract_band(0).embed( - x, y, width, pageHeight, - VImage::option()->set("extend", "white")); - frameAlpha = watermark.extract_band(1).embed( - x, y, width, pageHeight, - VImage::option()->set("extend", "black")); - bg = frameAlpha.new_from_image({0, 0, 0}).copy(VImage::option()->set( - "interpretation", VIPS_INTERPRETATION_sRGB)); - frame = bg.bandjoin(frameAlpha); - if (*outType == "jpg" || *outType == "jpeg") { - *outType = "png"; - } - } - VImage content = - img_frame.extract_band(0, VImage::option()->set("n", 3)) - .bandjoin(contentAlpha & img_frame.extract_band(3)); - - composited = - content.composite2(frame, VIPS_BLEND_MODE_OVER, - VImage::option()->set("x", x)->set("y", y)); - } else { - composited = - img_frame.composite2(watermark, VIPS_BLEND_MODE_OVER, - VImage::option()->set("x", x)->set("y", y)); - } - img.push_back(composited); - } - } - VImage final = VImage::arrayjoin(img, VImage::option()->set("across", 1)); - final.set(VIPS_META_PAGE_HEIGHT, pageHeight + addedHeight); - - void *buf; - final.write_to_buffer( - ("." + *outType).c_str(), &buf, DataSize, - *outType == "gif" - ? VImage::option()->set("dither", 0)->set("reoptimise", 1) - : 0); - - return (char *)buf; -} +#include +#include + +#include "common.h" + +using namespace std; +using namespace vips; + +char *Watermark(string type, string *outType, char *BufferData, + size_t BufferLength, ArgumentMap Arguments, size_t *DataSize) { + string water = GetArgument(Arguments, "water"); + int gravity = GetArgument(Arguments, "gravity"); + + bool resize = GetArgumentWithFallback(Arguments, "resize", false); + + float yscale = GetArgumentWithFallback(Arguments, "yscale", false); + + bool append = GetArgumentWithFallback(Arguments, "append", false); + + bool alpha = GetArgumentWithFallback(Arguments, "alpha", false); + bool flip = GetArgumentWithFallback(Arguments, "flip", false); + + bool mc = MAP_HAS(Arguments, "mc"); + + string basePath = GetArgument(Arguments, "basePath"); + + VOption *options = VImage::option()->set("access", "sequential"); + + VImage in = + VImage::new_from_buffer(BufferData, BufferLength, "", + type == "gif" ? options->set("n", -1) : options) + .colourspace(VIPS_INTERPRETATION_sRGB); + if (!in.has_alpha()) in = in.bandjoin(255); + + string merged = basePath + water; + VImage watermark = VImage::new_from_file(merged.c_str()); + + int width = in.width(); + int pageHeight = vips_image_get_page_height(in.get_image()); + int nPages = vips_image_get_n_pages(in.get_image()); + + if (flip) { + watermark = watermark.flip(VIPS_DIRECTION_HORIZONTAL); + } + + if (resize && append) { + watermark = watermark.resize((double)width / (double)watermark.width()); + } else if (resize && yscale) { + watermark = watermark.resize( + (double)width / (double)watermark.width(), + VImage::option()->set("vscale", (double)(pageHeight * yscale) / + (double)watermark.height())); + } else if (resize) { + watermark = + watermark.resize((double)pageHeight / (double)watermark.height()); + } + + int x = 0, y = 0; + switch (gravity) { + case 1: + break; + case 2: + x = (width / 2) - (watermark.width() / 2); + break; + case 3: + x = width - watermark.width(); + break; + case 5: + x = (width / 2) - (watermark.width() / 2); + y = (pageHeight / 2) - (watermark.height() / 2); + break; + case 6: + x = width - watermark.width(); + y = (pageHeight / 2) - (watermark.height() / 2); + break; + case 8: + x = (width / 2) - (watermark.width() / 2); + y = pageHeight - watermark.height(); + break; + case 9: + x = width - watermark.width(); + y = pageHeight - watermark.height(); + break; + } + + vector img; + int addedHeight = 0; + VImage contentAlpha; + VImage frameAlpha; + VImage bg; + VImage frame; + for (int i = 0; i < nPages; i++) { + VImage img_frame = + type == "gif" ? in.crop(0, i * pageHeight, width, pageHeight) : in; + if (append) { + VImage appended = img_frame.join(watermark, VIPS_DIRECTION_VERTICAL, + VImage::option()->set("expand", true)); + addedHeight = watermark.height(); + img.push_back(appended); + } else if (mc) { + VImage padded = + img_frame.embed(0, 0, width, pageHeight + 15, + VImage::option()->set("background", 0xffffff)); + VImage composited = + padded.composite2(watermark, VIPS_BLEND_MODE_OVER, + VImage::option() + ->set("x", width - 190) + ->set("y", padded.height() - 22)); + addedHeight = 15; + img.push_back(composited); + } else { + VImage composited; + if (alpha) { + if (i == 0) { + contentAlpha = watermark.extract_band(0).embed( + x, y, width, pageHeight, + VImage::option()->set("extend", "white")); + frameAlpha = watermark.extract_band(1).embed( + x, y, width, pageHeight, + VImage::option()->set("extend", "black")); + bg = frameAlpha.new_from_image({0, 0, 0}).copy(VImage::option()->set( + "interpretation", VIPS_INTERPRETATION_sRGB)); + frame = bg.bandjoin(frameAlpha); + if (*outType == "jpg" || *outType == "jpeg") { + *outType = "png"; + } + } + VImage content = + img_frame.extract_band(0, VImage::option()->set("n", 3)) + .bandjoin(contentAlpha & img_frame.extract_band(3)); + + composited = + content.composite2(frame, VIPS_BLEND_MODE_OVER, + VImage::option()->set("x", x)->set("y", y)); + } else { + composited = + img_frame.composite2(watermark, VIPS_BLEND_MODE_OVER, + VImage::option()->set("x", x)->set("y", y)); + } + img.push_back(composited); + } + } + VImage final = VImage::arrayjoin(img, VImage::option()->set("across", 1)); + final.set(VIPS_META_PAGE_HEIGHT, pageHeight + addedHeight); + + void *buf; + final.write_to_buffer( + ("." + *outType).c_str(), &buf, DataSize, + *outType == "gif" + ? VImage::option()->set("dither", 0)->set("reoptimise", 1) + : 0); + + return (char *)buf; +} diff --git a/natives/watermark.h b/natives/watermark.h index da102dc..bd861a7 100644 --- a/natives/watermark.h +++ b/natives/watermark.h @@ -1,8 +1,8 @@ -#pragma once - -#include "common.h" - -using std::string; - -char* Watermark(string type, string* outType, char* BufferData, size_t BufferLength, +#pragma once + +#include "common.h" + +using std::string; + +char* Watermark(string type, string* outType, char* BufferData, size_t BufferLength, ArgumentMap Arguments, size_t* DataSize); \ No newline at end of file diff --git a/natives/whisper.cc b/natives/whisper.cc index da96542..abfd01f 100644 --- a/natives/whisper.cc +++ b/natives/whisper.cc @@ -1,86 +1,86 @@ -#include - -#include "common.h" - -using namespace std; -using namespace vips; - -char *Whisper(string type, string *outType, char *BufferData, - size_t BufferLength, ArgumentMap Arguments, size_t *DataSize) { - string caption = GetArgument(Arguments, "caption"); - string basePath = GetArgument(Arguments, "basePath"); - - VOption *options = VImage::option()->set("access", "sequential"); - - VImage in = - VImage::new_from_buffer(BufferData, BufferLength, "", - type == "gif" ? options->set("n", -1) : options) - .colourspace(VIPS_INTERPRETATION_sRGB); - if (!in.has_alpha()) in = in.bandjoin(255); - - int width = in.width(); - int pageHeight = vips_image_get_page_height(in.get_image()); - int nPages = vips_image_get_n_pages(in.get_image()); - int size = width / 6; - int dividedWidth = width / 175; - int rad = 1; - - string font_string = "Upright, Twemoji Color Font " + to_string(size); - - VImage mask; - if (dividedWidth >= 1) { - mask = VImage::black(dividedWidth * 2 + 1, dividedWidth * 2 + 1) + 128; - mask.draw_circle({255}, dividedWidth, dividedWidth, dividedWidth, - VImage::option()->set("fill", true)); - } else { - mask = VImage::black(rad * 2 + 1, rad * 2 + 1) + 128; - mask.draw_circle({255}, rad, rad, rad, VImage::option()->set("fill", true)); - } - - VImage textIn = VImage::text( - ".", VImage::option()->set( - "fontfile", (basePath + "assets/fonts/whisper.otf").c_str())); - textIn = VImage::text( - ("" + caption + "").c_str(), - VImage::option() - ->set("rgba", true) - ->set("align", VIPS_ALIGN_CENTRE) - ->set("font", font_string.c_str()) - ->set("fontfile", (basePath + "assets/fonts/twemoji.otf").c_str()) - ->set("width", width)); - - textIn = textIn.embed(rad + 10, rad + 10, (textIn.width() + 2 * rad) + 20, - (textIn.height() + 2 * rad) + 20); - - VImage outline = textIn.morph(mask, VIPS_OPERATION_MORPHOLOGY_DILATE) - .gaussblur(0.5, VImage::option()->set("min_ampl", 0.1)); - outline = (outline == (vector){0, 0, 0, 0}); - VImage invert = outline.extract_band(3).invert(); - outline = - outline.extract_band(0, VImage::option()->set("n", outline.bands() - 1)) - .bandjoin(invert); - VImage textImg = outline.composite2(textIn, VIPS_BLEND_MODE_OVER); - - vector img; - for (int i = 0; i < nPages; i++) { - VImage img_frame = - type == "gif" ? in.crop(0, i * pageHeight, width, pageHeight) : in; - img_frame = img_frame.composite2( - textImg, VIPS_BLEND_MODE_OVER, - VImage::option() - ->set("x", (width / 2) - (textImg.width() / 2)) - ->set("y", (pageHeight / 2) - (textImg.height() / 2))); - img.push_back(img_frame); - } - VImage final = VImage::arrayjoin(img, VImage::option()->set("across", 1)); - final.set(VIPS_META_PAGE_HEIGHT, pageHeight); - - void *buf; - final.write_to_buffer( - ("." + *outType).c_str(), &buf, DataSize, - *outType == "gif" - ? VImage::option()->set("dither", 0)->set("reoptimise", 1) - : 0); - - return (char *)buf; +#include + +#include "common.h" + +using namespace std; +using namespace vips; + +char *Whisper(string type, string *outType, char *BufferData, + size_t BufferLength, ArgumentMap Arguments, size_t *DataSize) { + string caption = GetArgument(Arguments, "caption"); + string basePath = GetArgument(Arguments, "basePath"); + + VOption *options = VImage::option()->set("access", "sequential"); + + VImage in = + VImage::new_from_buffer(BufferData, BufferLength, "", + type == "gif" ? options->set("n", -1) : options) + .colourspace(VIPS_INTERPRETATION_sRGB); + if (!in.has_alpha()) in = in.bandjoin(255); + + int width = in.width(); + int pageHeight = vips_image_get_page_height(in.get_image()); + int nPages = vips_image_get_n_pages(in.get_image()); + int size = width / 6; + int dividedWidth = width / 175; + int rad = 1; + + string font_string = "Upright, Twemoji Color Font " + to_string(size); + + VImage mask; + if (dividedWidth >= 1) { + mask = VImage::black(dividedWidth * 2 + 1, dividedWidth * 2 + 1) + 128; + mask.draw_circle({255}, dividedWidth, dividedWidth, dividedWidth, + VImage::option()->set("fill", true)); + } else { + mask = VImage::black(rad * 2 + 1, rad * 2 + 1) + 128; + mask.draw_circle({255}, rad, rad, rad, VImage::option()->set("fill", true)); + } + + VImage textIn = VImage::text( + ".", VImage::option()->set( + "fontfile", (basePath + "assets/fonts/whisper.otf").c_str())); + textIn = VImage::text( + ("" + caption + "").c_str(), + VImage::option() + ->set("rgba", true) + ->set("align", VIPS_ALIGN_CENTRE) + ->set("font", font_string.c_str()) + ->set("fontfile", (basePath + "assets/fonts/twemoji.otf").c_str()) + ->set("width", width)); + + textIn = textIn.embed(rad + 10, rad + 10, (textIn.width() + 2 * rad) + 20, + (textIn.height() + 2 * rad) + 20); + + VImage outline = textIn.morph(mask, VIPS_OPERATION_MORPHOLOGY_DILATE) + .gaussblur(0.5, VImage::option()->set("min_ampl", 0.1)); + outline = (outline == (vector){0, 0, 0, 0}); + VImage invert = outline.extract_band(3).invert(); + outline = + outline.extract_band(0, VImage::option()->set("n", outline.bands() - 1)) + .bandjoin(invert); + VImage textImg = outline.composite2(textIn, VIPS_BLEND_MODE_OVER); + + vector img; + for (int i = 0; i < nPages; i++) { + VImage img_frame = + type == "gif" ? in.crop(0, i * pageHeight, width, pageHeight) : in; + img_frame = img_frame.composite2( + textImg, VIPS_BLEND_MODE_OVER, + VImage::option() + ->set("x", (width / 2) - (textImg.width() / 2)) + ->set("y", (pageHeight / 2) - (textImg.height() / 2))); + img.push_back(img_frame); + } + VImage final = VImage::arrayjoin(img, VImage::option()->set("across", 1)); + final.set(VIPS_META_PAGE_HEIGHT, pageHeight); + + void *buf; + final.write_to_buffer( + ("." + *outType).c_str(), &buf, DataSize, + *outType == "gif" + ? VImage::option()->set("dither", 0)->set("reoptimise", 1) + : 0); + + return (char *)buf; } \ No newline at end of file diff --git a/natives/whisper.h b/natives/whisper.h index df9160e..6a008b8 100644 --- a/natives/whisper.h +++ b/natives/whisper.h @@ -1,8 +1,8 @@ -#pragma once - -#include "common.h" - -using std::string; - -char* Whisper(string type, string* outType, char* BufferData, size_t BufferLength, +#pragma once + +#include "common.h" + +using std::string; + +char* Whisper(string type, string* outType, char* BufferData, size_t BufferLength, ArgumentMap Arguments, size_t* DataSize); \ No newline at end of file diff --git a/natives/zamn.cc b/natives/zamn.cc index 55173c3..564807e 100644 --- a/natives/zamn.cc +++ b/natives/zamn.cc @@ -1,47 +1,47 @@ -#include - -#include "common.h" - -using namespace std; -using namespace vips; - -char *Zamn(string type, string *outType, char *BufferData, size_t BufferLength, - ArgumentMap Arguments, size_t *DataSize) { - string basePath = GetArgument(Arguments, "basePath"); - - VOption *options = VImage::option()->set("access", "sequential"); - - VImage in = - VImage::new_from_buffer(BufferData, BufferLength, "", - type == "gif" ? options->set("n", -1) : options) - .colourspace(VIPS_INTERPRETATION_sRGB); - if (!in.has_alpha()) in = in.bandjoin(255); - - int width = in.width(); - int pageHeight = vips_image_get_page_height(in.get_image()); - int nPages = vips_image_get_n_pages(in.get_image()); - - string assetPath = basePath + "assets/images/zamn.png"; - VImage tmpl = VImage::new_from_file(assetPath.c_str()); - - vector img; - for (int i = 0; i < nPages; i++) { - VImage img_frame = - type == "gif" ? in.crop(0, i * pageHeight, width, pageHeight) : in; - VImage composited = tmpl.insert( - img_frame.extract_band(0, VImage::option()->set("n", 3)) - .bandjoin(255) - .resize( - 303.0 / (double)width, - VImage::option()->set("vscale", 438.0 / (double)pageHeight)), - 310, 76); - img.push_back(composited); - } - VImage final = VImage::arrayjoin(img, VImage::option()->set("across", 1)); - final.set(VIPS_META_PAGE_HEIGHT, 516); - - void *buf; - final.write_to_buffer(("." + *outType).c_str(), &buf, DataSize); - - return (char *)buf; +#include + +#include "common.h" + +using namespace std; +using namespace vips; + +char *Zamn(string type, string *outType, char *BufferData, size_t BufferLength, + ArgumentMap Arguments, size_t *DataSize) { + string basePath = GetArgument(Arguments, "basePath"); + + VOption *options = VImage::option()->set("access", "sequential"); + + VImage in = + VImage::new_from_buffer(BufferData, BufferLength, "", + type == "gif" ? options->set("n", -1) : options) + .colourspace(VIPS_INTERPRETATION_sRGB); + if (!in.has_alpha()) in = in.bandjoin(255); + + int width = in.width(); + int pageHeight = vips_image_get_page_height(in.get_image()); + int nPages = vips_image_get_n_pages(in.get_image()); + + string assetPath = basePath + "assets/images/zamn.png"; + VImage tmpl = VImage::new_from_file(assetPath.c_str()); + + vector img; + for (int i = 0; i < nPages; i++) { + VImage img_frame = + type == "gif" ? in.crop(0, i * pageHeight, width, pageHeight) : in; + VImage composited = tmpl.insert( + img_frame.extract_band(0, VImage::option()->set("n", 3)) + .bandjoin(255) + .resize( + 303.0 / (double)width, + VImage::option()->set("vscale", 438.0 / (double)pageHeight)), + 310, 76); + img.push_back(composited); + } + VImage final = VImage::arrayjoin(img, VImage::option()->set("across", 1)); + final.set(VIPS_META_PAGE_HEIGHT, 516); + + void *buf; + final.write_to_buffer(("." + *outType).c_str(), &buf, DataSize); + + return (char *)buf; } \ No newline at end of file diff --git a/natives/zamn.h b/natives/zamn.h index 0eec42c..2478066 100644 --- a/natives/zamn.h +++ b/natives/zamn.h @@ -1,8 +1,8 @@ -#pragma once - -#include "common.h" - -using std::string; - -char* Zamn(string type, string* outType, char* BufferData, size_t BufferLength, +#pragma once + +#include "common.h" + +using std::string; + +char* Zamn(string type, string* outType, char* BufferData, size_t BufferLength, ArgumentMap Arguments, size_t* DataSize); \ No newline at end of file diff --git a/package.json b/package.json index 8eff11b..e349fc2 100644 --- a/package.json +++ b/package.json @@ -31,7 +31,9 @@ "emoji-regex": "^10.2.1", "file-type": "^18.2.1", "format-duration": "^3.0.2", + "image-size": "^1.0.2", "jsqr": "^1.4.0", + "matrix-js-sdk": "^23.4.0", "node-addon-api": "^5.1.0", "node-emoji": "^1.11.0", "oceanic.js": "1.5.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4f4f486..59f0c75 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -15,7 +15,9 @@ specifiers: eslint-plugin-unicorn: ^46.0.0 file-type: ^18.2.1 format-duration: ^3.0.2 + image-size: ^1.0.2 jsqr: ^1.4.0 + matrix-js-sdk: ^23.4.0 node-addon-api: ^5.1.0 node-emoji: ^1.11.0 oceanic.js: 1.5.1 @@ -36,7 +38,9 @@ dependencies: emoji-regex: 10.2.1 file-type: 18.2.1 format-duration: 3.0.2 + image-size: 1.0.2 jsqr: 1.4.0 + matrix-js-sdk: 23.4.0 node-addon-api: 5.1.0 node-emoji: 1.11.0 oceanic.js: 1.5.1_bufferutil@4.0.7 @@ -340,6 +344,13 @@ packages: - supports-color dev: true + /@babel/runtime/7.21.0: + resolution: {integrity: sha512-xwII0//EObnq89Ji5AKYQaRYiW/nZ3llSv29d49IuxPhKbtJoLP+9QUUZ4nVragQVtaVGeZrpB+ZtG/Pdy/POw==} + engines: {node: '>=6.9.0'} + dependencies: + regenerator-runtime: 0.13.11 + dev: false + /@babel/template/7.20.7: resolution: {integrity: sha512-8SegXApWe6VoNw0r9JHpSteLKTpTiLZ4rMlGIm9JQ18KiCtyQiAMEazujAHrUS5flrcqYZa75ukev3P6QmUwUw==} engines: {node: '>=6.9.0'} @@ -499,6 +510,11 @@ packages: '@jridgewell/sourcemap-codec': 1.4.14 dev: true + /@matrix-org/matrix-sdk-crypto-js/0.1.0-alpha.4: + resolution: {integrity: sha512-mdaDKrw3P5ZVCpq0ioW0pV6ihviDEbS8ZH36kpt9stLKHwwDSopPogE6CkQhi0B1jn1yBUtOYi32mBV/zcOR7g==} + engines: {node: '>= 10'} + dev: false + /@nicolo-ribaudo/eslint-scope-5-internals/5.1.1-v1: resolution: {integrity: sha512-54/JRvkLIzzDWshCWfuhadfrfZVPiElY8Fcgmg1HroEly/EDSszzhBAsarCux+D/kOslTRquNzuyGSmUSTTHGg==} dependencies: @@ -636,6 +652,10 @@ packages: dev: false optional: true + /@types/events/3.0.0: + resolution: {integrity: sha512-EaObqwIvayI5a8dCzhFrjKzVwKLxjoG9T6Ppd5CEo07LRKfQ8Yokw54r5+Wq7FaBQ+yXRvQAYPrHwya1/UFt9g==} + dev: false + /@types/node/18.14.6: resolution: {integrity: sha512-93+VvleD3mXwlLI/xASjw0FzKcwzl3OdTCzm1LaRfqgS21gfFtK3zDXM5Op9TeeMsJVOaJ2VRDpT9q4Y3d0AvA==} dev: false @@ -645,6 +665,10 @@ packages: resolution: {integrity: sha512-Gj7cI7z+98M282Tqmp2K5EIsoouUEzbBJhQQzDE3jSIRk6r9gsz0oUokqIUR4u1R3dMHo0pDHM7sNOHyhulypw==} dev: true + /@types/retry/0.12.0: + resolution: {integrity: sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA==} + dev: false + /@types/triple-beam/1.3.2: resolution: {integrity: sha512-txGIh+0eDFzKGC25zORnswy+br1Ha7hj5cMVwKIU7+s0U2AxxJru/jZSMU6OC9MJWP6+pc/hc6ZjyZShpsyY2g==} dev: false @@ -706,6 +730,10 @@ packages: dev: false optional: true + /another-json/0.2.0: + resolution: {integrity: sha512-/Ndrl68UQLhnCdsAzEXLMFuOR546o2qbYRqCglaNHbjXrwG1ayTcdwr3zkSGOGtGXDyR5X9nCFfnyG2AFJIsqg==} + dev: false + /ansi-colors/4.1.3: resolution: {integrity: sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==} engines: {node: '>=6'} @@ -815,6 +843,10 @@ packages: /balanced-match/1.0.2: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + /base-x/4.0.0: + resolution: {integrity: sha512-FuwxlW4H5kh37X/oW59pwTzzTKRzfrrQwhmyspRM7swOEZcHtDZSCt45U6oKgtuFE+WYPblePMVIPR4RZrh/hw==} + dev: false + /base64-js/1.5.1: resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} dev: false @@ -886,6 +918,12 @@ packages: update-browserslist-db: 1.0.10_browserslist@4.21.5 dev: true + /bs58/5.0.0: + resolution: {integrity: sha512-r+ihvQJvahgYT50JD05dyJNKlmmSlMoOGwn1lCcEzanPglg7TxYjioQUYehQ9mAR/+hOSd2jRc/Z2y5UxBymvQ==} + dependencies: + base-x: 4.0.0 + dev: false + /buffer-from/1.1.2: resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} dev: false @@ -1123,6 +1161,11 @@ packages: resolution: {integrity: sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==} dev: true + /content-type/1.0.5: + resolution: {integrity: sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==} + engines: {node: '>= 0.6'} + dev: false + /continuation-local-storage/3.2.1: resolution: {integrity: sha512-jx44cconVqkCEEyLSKWwkvUXwO561jXMa3LPjTPsm5QR22PA0/mhe33FT4Xb5y74JDvt/Cq+5lm8S8rskLv9ZA==} dependencies: @@ -1525,6 +1568,11 @@ packages: dev: false optional: true + /events/3.3.0: + resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==} + engines: {node: '>=0.8.x'} + dev: false + /expand-template/2.0.3: resolution: {integrity: sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==} engines: {node: '>=6'} @@ -1888,6 +1936,14 @@ packages: engines: {node: '>= 4'} dev: true + /image-size/1.0.2: + resolution: {integrity: sha512-xfOoWjceHntRb3qFCrh5ZFORYH8XCdYpASltMhZ/Q0KZiOwjdE/Yl2QCiWdwD+lygV5bMCvauzgu5PxBX/Yerg==} + engines: {node: '>=14.0.0'} + hasBin: true + dependencies: + queue: 6.0.2 + dev: false + /import-fresh/3.3.0: resolution: {integrity: sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==} engines: {node: '>=6'} @@ -2151,6 +2207,11 @@ packages: triple-beam: 1.3.0 dev: false + /loglevel/1.8.1: + resolution: {integrity: sha512-tCRIJM51SHjAayKwC+QAg8hT8vg6z7GSgLJKGvzuPb1Wc+hLzqtuVLxp6/HzSPOozuK+8ErAhy7U/sVzw8Dgfg==} + engines: {node: '>= 0.6.0'} + dev: false + /lru-cache/5.1.1: resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} dependencies: @@ -2162,6 +2223,35 @@ packages: dependencies: yallist: 4.0.0 + /matrix-events-sdk/0.0.1: + resolution: {integrity: sha512-1QEOsXO+bhyCroIe2/A5OwaxHvBm7EsSQ46DEDn8RBIfQwN5HWBpFvyWWR4QY0KHPPnnJdI99wgRiAl7Ad5qaA==} + dev: false + + /matrix-js-sdk/23.4.0: + resolution: {integrity: sha512-3gHT6IrDYBkFYzaZM052uZXv1WFGoN+q83rTvmTpMtxZYrwosLHb6Y/w/Lfl26y1N1gTDdyQ0Vd3NpSycKmpJA==} + engines: {node: '>=16.0.0'} + dependencies: + '@babel/runtime': 7.21.0 + '@matrix-org/matrix-sdk-crypto-js': 0.1.0-alpha.4 + another-json: 0.2.0 + bs58: 5.0.0 + content-type: 1.0.5 + loglevel: 1.8.1 + matrix-events-sdk: 0.0.1 + matrix-widget-api: 1.2.0 + p-retry: 4.6.2 + sdp-transform: 2.14.1 + unhomoglyph: 1.0.6 + uuid: 9.0.0 + dev: false + + /matrix-widget-api/1.2.0: + resolution: {integrity: sha512-BkBTREtXjCUM3Kx4UBgDmKoz39w7AXfIjBIC/jISBdcJkg8upFUhpIy+zUrSCIrmfO2Ke8LOsSoFoQkOyhqGxQ==} + dependencies: + '@types/events': 3.0.0 + events: 3.3.0 + dev: false + /memory-stream/1.0.0: resolution: {integrity: sha512-Wm13VcsPIMdG96dzILfij09PvuS3APtcKNh7M28FsCA/w6+1mjR7hhPmfFNoilX9xU7wTdhsH5lJAm6XNzdtww==} dependencies: @@ -2440,6 +2530,14 @@ packages: p-limit: 3.1.0 dev: true + /p-retry/4.6.2: + resolution: {integrity: sha512-312Id396EbJdvRONlngUx0NydfrIQ5lsYu0znKVUzVvArzEIt08V1qhtyESbGVd1FGX7UKtiFp5uwKZdM8wIuQ==} + engines: {node: '>=8'} + dependencies: + '@types/retry': 0.12.0 + retry: 0.13.1 + dev: false + /p-try/2.2.0: resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==} engines: {node: '>=6'} @@ -2757,6 +2855,12 @@ packages: resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} dev: true + /queue/6.0.2: + resolution: {integrity: sha512-iHZWu+q3IdFZFX36ro/lKBkSvfkztY5Y7HMiPlOUjhupPcG2JMfst2KKEpu5XndviX/3UhFbRngUPNKtgvtZiA==} + dependencies: + inherits: 2.0.4 + dev: false + /raw-body/2.5.2: resolution: {integrity: sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==} engines: {node: '>= 0.8'} @@ -2837,6 +2941,10 @@ packages: dev: false optional: true + /regenerator-runtime/0.13.11: + resolution: {integrity: sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==} + dev: false + /regexp-tree/0.1.24: resolution: {integrity: sha512-s2aEVuLhvnVJW6s/iPgEGK6R+/xngd2jNQ+xy4bXNDKxZKJH6jpPHY6kVeVv1IeLCHgswRj+Kl3ELaDjG6V1iw==} hasBin: true @@ -2887,6 +2995,11 @@ packages: path-parse: 1.0.7 supports-preserve-symlinks-flag: 1.0.0 + /retry/0.13.1: + resolution: {integrity: sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==} + engines: {node: '>= 4'} + dev: false + /reusify/1.0.4: resolution: {integrity: sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==} engines: {iojs: '>=1.0.0', node: '>=0.10.0'} @@ -2934,6 +3047,11 @@ packages: dev: false optional: true + /sdp-transform/2.14.1: + resolution: {integrity: sha512-RjZyX3nVwJyCuTo5tGPx+PZWkDMCg7oOLpSlhjDdZfwUoNqG1mM8nyj31IGHyaPWXhjbP7cdK3qZ2bmkJ1GzRw==} + hasBin: true + dev: false + /semver/5.7.1: resolution: {integrity: sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==} hasBin: true @@ -3331,6 +3449,10 @@ packages: busboy: 1.6.0 dev: false + /unhomoglyph/1.0.6: + resolution: {integrity: sha512-7uvcWI3hWshSADBu4JpnyYbTVc7YlhF5GDW/oPD5AxIxl34k4wXR3WDkPnzLxkN32LiTCTKMQLtKVZiwki3zGg==} + dev: false + /universalify/0.1.2: resolution: {integrity: sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==} engines: {node: '>= 4.0.0'} @@ -3384,7 +3506,6 @@ packages: hasBin: true requiresBuild: true dev: false - optional: true /validate-npm-package-license/3.0.4: resolution: {integrity: sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==} diff --git a/utils/awaitrejoin.js b/utils/awaitrejoin.js index f2d7ed1..fcde9c1 100644 --- a/utils/awaitrejoin.js +++ b/utils/awaitrejoin.js @@ -1,49 +1,49 @@ -// this is a method to wait for someone to rejoin a voice channel -import { EventEmitter } from "events"; -import { random } from "./misc.js"; - -class AwaitRejoin extends EventEmitter { - constructor(channel, anyone, memberID) { - super(); - this.member = memberID; - this.anyone = anyone; - this.channel = channel; - this.rejoined = false; - this.ended = false; - this.bot = channel.client; - this.listener = (member, newChannel) => this.verify(member, newChannel); - this.bot.on("voiceChannelJoin", this.listener); - this.bot.on("voiceChannelSwitch", this.listener); - this.stopTimeout = setTimeout(() => this.stop(), 10000); - this.checkInterval = setInterval(() => this.verify({ id: memberID }, channel, true), 1000); - } - - verify(member, channel, checked) { - if (this.channel.id === channel.id) { - if ((this.member === member.id && this.channel.voiceMembers.has(member.id)) || (this.anyone && !checked)) { - clearTimeout(this.stopTimeout); - this.rejoined = true; - this.stop(member); - return true; - } else if (this.anyone && (!checked || this.channel.voiceMembers.size > 1)) { - clearTimeout(this.stopTimeout); - this.rejoined = true; - this.stop(random(this.channel.voiceMembers.filter((i) => i.id !== this.bot.user.id && !i.bot))); - return true; - } - } else { - return false; - } - } - - stop(member) { - if (this.ended) return; - this.ended = true; - clearInterval(this.checkInterval); - this.bot.removeListener("voiceChannelJoin", this.listener); - this.bot.removeListener("voiceChannelSwitch", this.listener); - this.emit("end", this.rejoined, member); - } -} - +// this is a method to wait for someone to rejoin a voice channel +import { EventEmitter } from "events"; +import { random } from "./misc.js"; + +class AwaitRejoin extends EventEmitter { + constructor(channel, anyone, memberID) { + super(); + this.member = memberID; + this.anyone = anyone; + this.channel = channel; + this.rejoined = false; + this.ended = false; + this.bot = channel.client; + this.listener = (member, newChannel) => this.verify(member, newChannel); + this.bot.on("voiceChannelJoin", this.listener); + this.bot.on("voiceChannelSwitch", this.listener); + this.stopTimeout = setTimeout(() => this.stop(), 10000); + this.checkInterval = setInterval(() => this.verify({ id: memberID }, channel, true), 1000); + } + + verify(member, channel, checked) { + if (this.channel.id === channel.id) { + if ((this.member === member.id && this.channel.voiceMembers.has(member.id)) || (this.anyone && !checked)) { + clearTimeout(this.stopTimeout); + this.rejoined = true; + this.stop(member); + return true; + } else if (this.anyone && (!checked || this.channel.voiceMembers.size > 1)) { + clearTimeout(this.stopTimeout); + this.rejoined = true; + this.stop(random(this.channel.voiceMembers.filter((i) => i.id !== this.bot.user.id && !i.bot))); + return true; + } + } else { + return false; + } + } + + stop(member) { + if (this.ended) return; + this.ended = true; + clearInterval(this.checkInterval); + this.bot.removeListener("voiceChannelJoin", this.listener); + this.bot.removeListener("voiceChannelSwitch", this.listener); + this.emit("end", this.rejoined, member); + } +} + export default AwaitRejoin; \ No newline at end of file diff --git a/utils/collections.js b/utils/collections.js index 98e64d9..9104882 100644 --- a/utils/collections.js +++ b/utils/collections.js @@ -1,39 +1,39 @@ -export const commands = new Map(); -export const messageCommands = new Map(); -export const paths = new Map(); -export const aliases = new Map(); -export const info = new Map(); -export const sounds = new Map(); -export const categories = new Map(); - -class TimedMap extends Map { - constructor(time, values) { - super(values); - this.time = time; - } - set(key, value) { - super.set(key, value); - setTimeout(() => { - if (super.has(key)) super.delete(key); - }, this.time); - } -} - -export const runningCommands = new TimedMap(5000); -export const selectedImages = new TimedMap(180000); - -class Cache extends Map { - constructor(values) { - super(values); - this.maxValues = 2048; - } - - set(key, value) { - super.set(key, value); - if (this.size > this.maxValues) this.delete(this.keys().next().value); - } -} - -export const prefixCache = new Cache(); -export const disabledCache = new Cache(); +export const commands = new Map(); +export const messageCommands = new Map(); +export const paths = new Map(); +export const aliases = new Map(); +export const info = new Map(); +export const sounds = new Map(); +export const categories = new Map(); + +class TimedMap extends Map { + constructor(time, values) { + super(values); + this.time = time; + } + set(key, value) { + super.set(key, value); + setTimeout(() => { + if (super.has(key)) super.delete(key); + }, this.time); + } +} + +export const runningCommands = new TimedMap(5000); +export const selectedImages = new TimedMap(180000); + +class Cache extends Map { + constructor(values) { + super(values); + this.maxValues = 2048; + } + + set(key, value) { + super.set(key, value); + if (this.size > this.maxValues) this.delete(this.keys().next().value); + } +} + +export const prefixCache = new Cache(); +export const disabledCache = new Cache(); export const disabledCmdCache = new Cache(); \ No newline at end of file diff --git a/utils/convertpg.js b/utils/convertpg.js index 9d4e266..732d389 100644 --- a/utils/convertpg.js +++ b/utils/convertpg.js @@ -1,33 +1,33 @@ -import { config } from "dotenv"; -config(); -import { Pool } from "pg"; -const pool = new Pool({ - connectionString: process.env.DB -}); - -(async () => { - const guilds = (await pool.query("SELECT * FROM guilds")).rows; - console.log("Migrating tags..."); - try { - await pool.query("CREATE TABLE tags ( guild_id VARCHAR(30) NOT NULL, name text NOT NULL, content text NOT NULL, author VARCHAR(30) NOT NULL, UNIQUE(guild_id, name) )"); - } catch (e) { - console.error(`Skipping table creation due to error: ${e}`); - } - for (const guild of guilds) { - for (const [name, value] of Object.entries(guild.tags)) { - if ((await pool.query("SELECT * FROM tags WHERE guild_id = $1 AND name = $2", [guild.guild_id, name])).rows.length !== 0) { - await pool.query("UPDATE tags SET content = $1, author = $2 WHERE guild_id = $3 AND name = $4", [value.content, value.author, guild.guild_id, name]); - } else { - await pool.query("INSERT INTO tags (guild_id, name, content, author) VALUES ($1, $2, $3, $4)", [guild.guild_id, name, value.content, value.author]); - } - console.log(`Migrated tag ${name} in guild ${guild.guild_id}`); - } - } - console.log("Migrating disabled commands..."); - for (const guild of guilds) { - await pool.query("UPDATE guilds SET disabled_commands = $1 WHERE guild_id = $2", [guild.tags_disabled ? ["tags"] : [], guild.guild_id]); - console.log(`Migrated disabled commands in guild ${guild.guild_id}`); - } - console.log("Done!"); - return process.exit(0); +import { config } from "dotenv"; +config(); +import { Pool } from "pg"; +const pool = new Pool({ + connectionString: process.env.DB +}); + +(async () => { + const guilds = (await pool.query("SELECT * FROM guilds")).rows; + console.log("Migrating tags..."); + try { + await pool.query("CREATE TABLE tags ( guild_id VARCHAR(30) NOT NULL, name text NOT NULL, content text NOT NULL, author VARCHAR(30) NOT NULL, UNIQUE(guild_id, name) )"); + } catch (e) { + console.error(`Skipping table creation due to error: ${e}`); + } + for (const guild of guilds) { + for (const [name, value] of Object.entries(guild.tags)) { + if ((await pool.query("SELECT * FROM tags WHERE guild_id = $1 AND name = $2", [guild.guild_id, name])).rows.length !== 0) { + await pool.query("UPDATE tags SET content = $1, author = $2 WHERE guild_id = $3 AND name = $4", [value.content, value.author, guild.guild_id, name]); + } else { + await pool.query("INSERT INTO tags (guild_id, name, content, author) VALUES ($1, $2, $3, $4)", [guild.guild_id, name, value.content, value.author]); + } + console.log(`Migrated tag ${name} in guild ${guild.guild_id}`); + } + } + console.log("Migrating disabled commands..."); + for (const guild of guilds) { + await pool.query("UPDATE guilds SET disabled_commands = $1 WHERE guild_id = $2", [guild.tags_disabled ? ["tags"] : [], guild.guild_id]); + console.log(`Migrated disabled commands in guild ${guild.guild_id}`); + } + console.log("Done!"); + return process.exit(0); })(); \ No newline at end of file diff --git a/utils/database.js b/utils/database.js index 24a4d26..2ecd9cb 100644 --- a/utils/database.js +++ b/utils/database.js @@ -1,20 +1,20 @@ -// wrapper for the database drivers in ./database/ -import { config } from "dotenv"; -config(); - -let db = null; - -if (process.env.DB) { - const dbtype = process.env.DB.split("://")[0]; - try { - db = await import(`./database/${dbtype}.js`); - } catch (error) { - if (error.code == "ERR_MODULE_NOT_FOUND") { - console.error(`DB config option has unknown database type '${dbtype}'`); - process.exit(1); - } - throw error; - } -} - -export default db; +// wrapper for the database drivers in ./database/ +import { config } from "dotenv"; +config(); + +let db = null; + +if (process.env.DB) { + const dbtype = process.env.DB.split("://")[0]; + try { + db = await import(`./database/${dbtype}.js`); + } catch (error) { + if (error.code == "ERR_MODULE_NOT_FOUND") { + console.error(`DB config option has unknown database type '${dbtype}'`); + process.exit(1); + } + throw error; + } +} + +export default db; diff --git a/utils/database/postgresql.js b/utils/database/postgresql.js index 93cfad4..0cf5fca 100644 --- a/utils/database/postgresql.js +++ b/utils/database/postgresql.js @@ -1,187 +1,187 @@ -import { prefixCache, disabledCmdCache, disabledCache, commands, messageCommands } from "../collections.js"; - -import Postgres from "postgres"; -const sql = Postgres(process.env.DB, { - onnotice: () => {} -}); - -const settingsSchema = ` -CREATE TABLE IF NOT EXISTS settings ( - id smallint PRIMARY KEY, - version integer NOT NULL, CHECK(id = 1) -); -`; - -const schema = ` -ALTER TABLE settings ADD COLUMN broadcast text; -CREATE TABLE guilds ( - guild_id VARCHAR(30) NOT NULL PRIMARY KEY, - prefix VARCHAR(15) NOT NULL, - disabled text ARRAY NOT NULL, - disabled_commands text ARRAY NOT NULL -); -CREATE TABLE counts ( - command VARCHAR NOT NULL PRIMARY KEY, - count integer NOT NULL -); -CREATE TABLE tags ( - guild_id VARCHAR(30) NOT NULL, - name text NOT NULL, - content text NOT NULL, - author VARCHAR(30) NOT NULL, - UNIQUE(guild_id, name) -); -`; - -const updates = [ - "", // reserved - "CREATE TABLE IF NOT EXISTS settings ( id smallint PRIMARY KEY, version integer NOT NULL, CHECK(id = 1) );\nALTER TABLE guilds ADD COLUMN accessed timestamp;", - "ALTER TABLE guilds DROP COLUMN accessed", - "ALTER TABLE settings ADD COLUMN IF NOT EXISTS broadcast text" -]; - -export async function setup() { - const existingCommands = (await sql`SELECT command FROM counts`).map(x => x.command); - const commandNames = [...commands.keys(), ...messageCommands.keys()]; - for (const command of existingCommands) { - if (!commandNames.includes(command)) { - await sql`DELETE FROM counts WHERE command = ${command}`; - } - } - for (const command of commandNames) { - if (!existingCommands.includes(command)) { - await sql`INSERT INTO counts ${sql({ command, count: 0 }, "command", "count")}`; - } - } -} - -export async function upgrade(logger) { - try { - await sql.begin(async (sql) => { - await sql.unsafe(settingsSchema); - let version; - const settingsrow = (await sql`SELECT version FROM settings WHERE id = 1`); - if (settingsrow.length == 0) { - version = 0; - } else { - version = settingsrow[0].version; - } - const latestVersion = updates.length - 1; - if (version === 0) { - logger.info("Initializing PostgreSQL database..."); - await sql.unsafe(schema); - } else if (version < latestVersion) { - logger.info(`Migrating PostgreSQL database, which is currently at version ${version}...`); - while (version < latestVersion) { - version++; - logger.info(`Running version ${version} update script...`); - await sql.unsafe(updates[version]); - } - } else if (version > latestVersion) { - throw new Error(`PostgreSQL database is at version ${version}, but this version of the bot only supports up to version ${latestVersion}.`); - } else { - return; - } - await sql`INSERT INTO settings ${sql({ id: 1, version: latestVersion })} ON CONFLICT (id) DO UPDATE SET version = ${latestVersion}`; - }); - } catch (e) { - logger.error(`PostgreSQL migration failed: ${e}`); - logger.error("Unable to start the bot, quitting now."); - return 1; - } -} - -export async function getGuild(query) { - let guild; - await sql.begin(async (sql) => { - guild = (await sql`SELECT * FROM guilds WHERE guild_id = ${query}`)[0]; - if (guild == undefined) { - guild = { guild_id: query, prefix: process.env.PREFIX, disabled: [], disabled_commands: [] }; - await sql`INSERT INTO guilds ${sql(guild)}`; - } - }); - return guild; -} - -export async function setPrefix(prefix, guild) { - await sql`UPDATE guilds SET prefix = ${prefix} WHERE guild_id = ${guild.id}`; - prefixCache.set(guild.id, prefix); -} - -export async function getTag(guild, tag) { - const tagResult = await sql`SELECT * FROM tags WHERE guild_id = ${guild} AND name = ${tag}`; - return tagResult[0] ? { content: tagResult[0].content, author: tagResult[0].author } : undefined; -} - -export async function getTags(guild) { - const tagArray = await sql`SELECT * FROM tags WHERE guild_id = ${guild}`; - const tags = {}; - for (const tag of tagArray) { - tags[tag.name] = { content: tag.content, author: tag.author }; - } - return tags; -} - -export async function setTag(name, content, guild) { - await sql`INSERT INTO tags ${sql({ guild_id: guild.id, name, content: content.content, author: content.author }, "guild_id", "name", "content", "author")}`; -} - -export async function editTag(name, content, guild) { - await sql`UPDATE tags SET content = ${content.content}, author = ${content.author} WHERE guild_id = ${guild.id} AND name = ${name}`; -} - -export async function removeTag(name, guild) { - await sql`DELETE FROM tags WHERE guild_id = ${guild.id} AND name = ${name}`; -} - -export async function setBroadcast(msg) { - await sql`UPDATE settings SET broadcast = ${msg} WHERE id = 1`; -} - -export async function getBroadcast() { - const result = await sql`SELECT broadcast FROM settings WHERE id = 1`; - return result[0].broadcast; -} - -export async function disableCommand(guild, command) { - const guildDB = await this.getGuild(guild); - await sql`UPDATE guilds SET disabled_commands = ${(guildDB.disabled_commands ? [...guildDB.disabled_commands, command] : [command]).filter((v) => !!v)} WHERE guild_id = ${guild}`; - disabledCmdCache.set(guild, guildDB.disabled_commands ? [...guildDB.disabled_commands, command] : [command].filter((v) => !!v)); -} - -export async function enableCommand(guild, command) { - const guildDB = await this.getGuild(guild); - const newDisabled = guildDB.disabled_commands ? guildDB.disabled_commands.filter(item => item !== command) : []; - await sql`UPDATE guilds SET disabled_commands = ${newDisabled} WHERE guild_id = ${guild}`; - disabledCmdCache.set(guild, newDisabled); -} - -export async function disableChannel(channel) { - const guildDB = await this.getGuild(channel.guildID); - await sql`UPDATE guilds SET disabled_commands = ${[...guildDB.disabled, channel.id]} WHERE guild_id = ${channel.guildID}`; - disabledCache.set(channel.guildID, [...guildDB.disabled, channel.id]); -} - -export async function enableChannel(channel) { - const guildDB = await this.getGuild(channel.guildID); - const newDisabled = guildDB.disabled.filter(item => item !== channel.id); - await sql`UPDATE guilds SET disabled_commands = ${newDisabled} WHERE guild_id = ${channel.guildID}`; - disabledCache.set(channel.guildID, newDisabled); -} - -export async function getCounts() { - const counts = await sql`SELECT * FROM counts`; - const countObject = {}; - for (const { command, count } of counts) { - countObject[command] = count; - } - return countObject; -} - -export async function addCount(command) { - await sql`INSERT INTO counts ${sql({ command, count: 1 }, "command", "count")} ON CONFLICT (command) DO UPDATE SET count = counts.count + 1 WHERE counts.command = ${command}`; -} - -export async function stop() { - await sql.end(); -} +import { prefixCache, disabledCmdCache, disabledCache, commands, messageCommands } from "../collections.js"; + +import Postgres from "postgres"; +const sql = Postgres(process.env.DB, { + onnotice: () => {} +}); + +const settingsSchema = ` +CREATE TABLE IF NOT EXISTS settings ( + id smallint PRIMARY KEY, + version integer NOT NULL, CHECK(id = 1) +); +`; + +const schema = ` +ALTER TABLE settings ADD COLUMN broadcast text; +CREATE TABLE guilds ( + guild_id VARCHAR(30) NOT NULL PRIMARY KEY, + prefix VARCHAR(15) NOT NULL, + disabled text ARRAY NOT NULL, + disabled_commands text ARRAY NOT NULL +); +CREATE TABLE counts ( + command VARCHAR NOT NULL PRIMARY KEY, + count integer NOT NULL +); +CREATE TABLE tags ( + guild_id VARCHAR(30) NOT NULL, + name text NOT NULL, + content text NOT NULL, + author VARCHAR(30) NOT NULL, + UNIQUE(guild_id, name) +); +`; + +const updates = [ + "", // reserved + "CREATE TABLE IF NOT EXISTS settings ( id smallint PRIMARY KEY, version integer NOT NULL, CHECK(id = 1) );\nALTER TABLE guilds ADD COLUMN accessed timestamp;", + "ALTER TABLE guilds DROP COLUMN accessed", + "ALTER TABLE settings ADD COLUMN IF NOT EXISTS broadcast text" +]; + +export async function setup() { + const existingCommands = (await sql`SELECT command FROM counts`).map(x => x.command); + const commandNames = [...commands.keys(), ...messageCommands.keys()]; + for (const command of existingCommands) { + if (!commandNames.includes(command)) { + await sql`DELETE FROM counts WHERE command = ${command}`; + } + } + for (const command of commandNames) { + if (!existingCommands.includes(command)) { + await sql`INSERT INTO counts ${sql({ command, count: 0 }, "command", "count")}`; + } + } +} + +export async function upgrade(logger) { + try { + await sql.begin(async (sql) => { + await sql.unsafe(settingsSchema); + let version; + const settingsrow = (await sql`SELECT version FROM settings WHERE id = 1`); + if (settingsrow.length == 0) { + version = 0; + } else { + version = settingsrow[0].version; + } + const latestVersion = updates.length - 1; + if (version === 0) { + logger.info("Initializing PostgreSQL database..."); + await sql.unsafe(schema); + } else if (version < latestVersion) { + logger.info(`Migrating PostgreSQL database, which is currently at version ${version}...`); + while (version < latestVersion) { + version++; + logger.info(`Running version ${version} update script...`); + await sql.unsafe(updates[version]); + } + } else if (version > latestVersion) { + throw new Error(`PostgreSQL database is at version ${version}, but this version of the bot only supports up to version ${latestVersion}.`); + } else { + return; + } + await sql`INSERT INTO settings ${sql({ id: 1, version: latestVersion })} ON CONFLICT (id) DO UPDATE SET version = ${latestVersion}`; + }); + } catch (e) { + logger.error(`PostgreSQL migration failed: ${e}`); + logger.error("Unable to start the bot, quitting now."); + return 1; + } +} + +export async function getGuild(query) { + let guild; + await sql.begin(async (sql) => { + guild = (await sql`SELECT * FROM guilds WHERE guild_id = ${query}`)[0]; + if (guild == undefined) { + guild = { guild_id: query, prefix: process.env.PREFIX, disabled: [], disabled_commands: [] }; + await sql`INSERT INTO guilds ${sql(guild)}`; + } + }); + return guild; +} + +export async function setPrefix(prefix, guild) { + await sql`UPDATE guilds SET prefix = ${prefix} WHERE guild_id = ${guild.id}`; + prefixCache.set(guild.id, prefix); +} + +export async function getTag(guild, tag) { + const tagResult = await sql`SELECT * FROM tags WHERE guild_id = ${guild} AND name = ${tag}`; + return tagResult[0] ? { content: tagResult[0].content, author: tagResult[0].author } : undefined; +} + +export async function getTags(guild) { + const tagArray = await sql`SELECT * FROM tags WHERE guild_id = ${guild}`; + const tags = {}; + for (const tag of tagArray) { + tags[tag.name] = { content: tag.content, author: tag.author }; + } + return tags; +} + +export async function setTag(name, content, guild) { + await sql`INSERT INTO tags ${sql({ guild_id: guild.id, name, content: content.content, author: content.author }, "guild_id", "name", "content", "author")}`; +} + +export async function editTag(name, content, guild) { + await sql`UPDATE tags SET content = ${content.content}, author = ${content.author} WHERE guild_id = ${guild.id} AND name = ${name}`; +} + +export async function removeTag(name, guild) { + await sql`DELETE FROM tags WHERE guild_id = ${guild.id} AND name = ${name}`; +} + +export async function setBroadcast(msg) { + await sql`UPDATE settings SET broadcast = ${msg} WHERE id = 1`; +} + +export async function getBroadcast() { + const result = await sql`SELECT broadcast FROM settings WHERE id = 1`; + return result[0].broadcast; +} + +export async function disableCommand(guild, command) { + const guildDB = await this.getGuild(guild); + await sql`UPDATE guilds SET disabled_commands = ${(guildDB.disabled_commands ? [...guildDB.disabled_commands, command] : [command]).filter((v) => !!v)} WHERE guild_id = ${guild}`; + disabledCmdCache.set(guild, guildDB.disabled_commands ? [...guildDB.disabled_commands, command] : [command].filter((v) => !!v)); +} + +export async function enableCommand(guild, command) { + const guildDB = await this.getGuild(guild); + const newDisabled = guildDB.disabled_commands ? guildDB.disabled_commands.filter(item => item !== command) : []; + await sql`UPDATE guilds SET disabled_commands = ${newDisabled} WHERE guild_id = ${guild}`; + disabledCmdCache.set(guild, newDisabled); +} + +export async function disableChannel(channel) { + const guildDB = await this.getGuild(channel.guildID); + await sql`UPDATE guilds SET disabled_commands = ${[...guildDB.disabled, channel.id]} WHERE guild_id = ${channel.guildID}`; + disabledCache.set(channel.guildID, [...guildDB.disabled, channel.id]); +} + +export async function enableChannel(channel) { + const guildDB = await this.getGuild(channel.guildID); + const newDisabled = guildDB.disabled.filter(item => item !== channel.id); + await sql`UPDATE guilds SET disabled_commands = ${newDisabled} WHERE guild_id = ${channel.guildID}`; + disabledCache.set(channel.guildID, newDisabled); +} + +export async function getCounts() { + const counts = await sql`SELECT * FROM counts`; + const countObject = {}; + for (const { command, count } of counts) { + countObject[command] = count; + } + return countObject; +} + +export async function addCount(command) { + await sql`INSERT INTO counts ${sql({ command, count: 1 }, "command", "count")} ON CONFLICT (command) DO UPDATE SET count = counts.count + 1 WHERE counts.command = ${command}`; +} + +export async function stop() { + await sql.end(); +} diff --git a/utils/database/sqlite.js b/utils/database/sqlite.js index 6d808c9..2b9d043 100644 --- a/utils/database/sqlite.js +++ b/utils/database/sqlite.js @@ -1,194 +1,194 @@ -import { commands, messageCommands, disabledCache, disabledCmdCache, prefixCache } from "../collections.js"; - -import sqlite3 from "better-sqlite3"; -const connection = sqlite3(process.env.DB.replace("sqlite://", "")); - -const schema = ` -CREATE TABLE guilds ( - guild_id VARCHAR(30) NOT NULL PRIMARY KEY, - prefix VARCHAR(15) NOT NULL, - disabled text NOT NULL, - disabled_commands text NOT NULL -); -CREATE TABLE counts ( - command VARCHAR NOT NULL PRIMARY KEY, - count integer NOT NULL -); -CREATE TABLE tags ( - guild_id VARCHAR(30) NOT NULL, - name text NOT NULL, - content text NOT NULL, - author VARCHAR(30) NOT NULL, - UNIQUE(guild_id, name) -); -CREATE TABLE settings ( - id smallint PRIMARY KEY, - broadcast VARCHAR, - CHECK(id = 1) -); -INSERT INTO settings (id) VALUES (1); -`; - -const updates = [ - "", // reserved - "ALTER TABLE guilds ADD COLUMN accessed int", - "ALTER TABLE guilds DROP COLUMN accessed", - `CREATE TABLE settings ( - id smallint PRIMARY KEY, - broadcast VARCHAR, - CHECK(id = 1) - ); - INSERT INTO settings (id) VALUES (1);`, -]; - -export async function setup() { - const existingCommands = connection.prepare("SELECT command FROM counts").all().map(x => x.command); - const commandNames = [...commands.keys(), ...messageCommands.keys()]; - for (const command of existingCommands) { - if (!commandNames.includes(command)) { - connection.prepare("DELETE FROM counts WHERE command = ?").run(command); - } - } - for (const command of commandNames) { - if (!existingCommands.includes(command)) { - connection.prepare("INSERT INTO counts (command, count) VALUES (?, ?)").run(command, 0); - } - } -} - -export async function stop() { - connection.close(); -} - -export async function upgrade(logger) { - connection.exec("BEGIN TRANSACTION"); - try { - let version = connection.pragma("user_version", { simple: true }); - const latestVersion = updates.length - 1; - if (version == 0) { - logger.info("Initializing SQLite database..."); - connection.exec(schema); - } else if (version < latestVersion) { - logger.info(`Migrating SQLite database at ${process.env.DB}, which is currently at version ${version}...`); - while (version < latestVersion) { - version++; - logger.info(`Running version ${version} update script...`); - connection.exec(updates[version]); - } - } else if (version > latestVersion) { - throw new Error(`SQLite database is at version ${version}, but this version of the bot only supports up to version ${latestVersion}.`); - } else { - return; - } - connection.pragma(`user_version = ${latestVersion}`); // prepared statements don't seem to work here - } catch (e) { - logger.error(`SQLite migration failed: ${e}`); - connection.exec("ROLLBACK"); - logger.error("Unable to start the bot, quitting now."); - return 1; - } - connection.exec("COMMIT"); -} - -export async function addCount(command) { - connection.prepare("UPDATE counts SET count = count + 1 WHERE command = ?").run(command); -} - -export async function getCounts() { - const counts = connection.prepare("SELECT * FROM counts").all(); - const countObject = {}; - for (const { command, count } of counts) { - countObject[command] = count; - } - return countObject; -} - -export async function disableCommand(guild, command) { - const guildDB = await this.getGuild(guild); - connection.prepare("UPDATE guilds SET disabled_commands = ? WHERE guild_id = ?").run(JSON.stringify((guildDB.disabledCommands ? [...JSON.parse(guildDB.disabledCommands), command] : [command]).filter((v) => !!v)), guild); - disabledCmdCache.set(guild, guildDB.disabled_commands ? [...JSON.parse(guildDB.disabledCommands), command] : [command].filter((v) => !!v)); -} - -export async function enableCommand(guild, command) { - const guildDB = await this.getGuild(guild); - const newDisabled = guildDB.disabledCommands ? JSON.parse(guildDB.disabledCommands).filter(item => item !== command) : []; - connection.prepare("UPDATE guilds SET disabled_commands = ? WHERE guild_id = ?").run(JSON.stringify(newDisabled), guild); - disabledCmdCache.set(guild, newDisabled); -} - -export async function disableChannel(channel) { - const guildDB = await this.getGuild(channel.guildID); - connection.prepare("UPDATE guilds SET disabled = ? WHERE guild_id = ?").run(JSON.stringify([...JSON.parse(guildDB.disabled), channel.id]), channel.guildID); - disabledCache.set(channel.guildID, [...JSON.parse(guildDB.disabled), channel.id]); -} - -export async function enableChannel(channel) { - const guildDB = await this.getGuild(channel.guildID); - const newDisabled = JSON.parse(guildDB.disabled).filter(item => item !== channel.id); - connection.prepare("UPDATE guilds SET disabled = ? WHERE guild_id = ?").run(JSON.stringify(newDisabled), channel.guildID); - disabledCache.set(channel.guildID, newDisabled); -} - -export async function getTag(guild, tag) { - const tagResult = connection.prepare("SELECT * FROM tags WHERE guild_id = ? AND name = ?").get(guild, tag); - return tagResult ? { content: tagResult.content, author: tagResult.author } : undefined; -} - -export async function getTags(guild) { - const tagArray = connection.prepare("SELECT * FROM tags WHERE guild_id = ?").all(guild); - const tags = {}; - if (!tagArray) return []; - for (const tag of tagArray) { - tags[tag.name] = { content: tag.content, author: tag.author }; - } - return tags; -} - -export async function setTag(name, content, guild) { - const tag = { - id: guild.id, - name: name, - content: content.content, - author: content.author - }; - connection.prepare("INSERT INTO tags (guild_id, name, content, author) VALUES (@id, @name, @content, @author)").run(tag); -} - -export async function removeTag(name, guild) { - connection.prepare("DELETE FROM tags WHERE guild_id = ? AND name = ?").run(guild.id, name); -} - -export async function editTag(name, content, guild) { - connection.prepare("UPDATE tags SET content = ?, author = ? WHERE guild_id = ? AND name = ?").run(content.content, content.author, guild.id, name); -} - -export async function setBroadcast(msg) { - connection.prepare("UPDATE settings SET broadcast = ? WHERE id = 1").run(msg); -} - -export async function getBroadcast() { - const result = connection.prepare("SELECT broadcast FROM settings WHERE id = 1").all(); - return result[0].broadcast; -} - -export async function setPrefix(prefix, guild) { - connection.prepare("UPDATE guilds SET prefix = ? WHERE guild_id = ?").run(prefix, guild.id); - prefixCache.set(guild.id, prefix); -} - -export async function getGuild(query) { - let guild; - connection.transaction(() => { - guild = connection.prepare("SELECT * FROM guilds WHERE guild_id = ?").get(query); - if (!guild) { - guild = { - id: query, - prefix: process.env.PREFIX, - disabled: "[]", - disabledCommands: "[]" - }; - connection.prepare("INSERT INTO guilds (guild_id, prefix, disabled, disabled_commands) VALUES (@id, @prefix, @disabled, @disabledCommands)").run(guild); - } - })(); - return guild; -} +import { commands, messageCommands, disabledCache, disabledCmdCache, prefixCache } from "../collections.js"; + +import sqlite3 from "better-sqlite3"; +const connection = sqlite3(process.env.DB.replace("sqlite://", "")); + +const schema = ` +CREATE TABLE guilds ( + guild_id VARCHAR(30) NOT NULL PRIMARY KEY, + prefix VARCHAR(15) NOT NULL, + disabled text NOT NULL, + disabled_commands text NOT NULL +); +CREATE TABLE counts ( + command VARCHAR NOT NULL PRIMARY KEY, + count integer NOT NULL +); +CREATE TABLE tags ( + guild_id VARCHAR(30) NOT NULL, + name text NOT NULL, + content text NOT NULL, + author VARCHAR(30) NOT NULL, + UNIQUE(guild_id, name) +); +CREATE TABLE settings ( + id smallint PRIMARY KEY, + broadcast VARCHAR, + CHECK(id = 1) +); +INSERT INTO settings (id) VALUES (1); +`; + +const updates = [ + "", // reserved + "ALTER TABLE guilds ADD COLUMN accessed int", + "ALTER TABLE guilds DROP COLUMN accessed", + `CREATE TABLE settings ( + id smallint PRIMARY KEY, + broadcast VARCHAR, + CHECK(id = 1) + ); + INSERT INTO settings (id) VALUES (1);`, +]; + +export async function setup() { + const existingCommands = connection.prepare("SELECT command FROM counts").all().map(x => x.command); + const commandNames = [...commands.keys(), ...messageCommands.keys()]; + for (const command of existingCommands) { + if (!commandNames.includes(command)) { + connection.prepare("DELETE FROM counts WHERE command = ?").run(command); + } + } + for (const command of commandNames) { + if (!existingCommands.includes(command)) { + connection.prepare("INSERT INTO counts (command, count) VALUES (?, ?)").run(command, 0); + } + } +} + +export async function stop() { + connection.close(); +} + +export async function upgrade(logger) { + connection.exec("BEGIN TRANSACTION"); + try { + let version = connection.pragma("user_version", { simple: true }); + const latestVersion = updates.length - 1; + if (version == 0) { + logger.info("Initializing SQLite database..."); + connection.exec(schema); + } else if (version < latestVersion) { + logger.info(`Migrating SQLite database at ${process.env.DB}, which is currently at version ${version}...`); + while (version < latestVersion) { + version++; + logger.info(`Running version ${version} update script...`); + connection.exec(updates[version]); + } + } else if (version > latestVersion) { + throw new Error(`SQLite database is at version ${version}, but this version of the bot only supports up to version ${latestVersion}.`); + } else { + return; + } + connection.pragma(`user_version = ${latestVersion}`); // prepared statements don't seem to work here + } catch (e) { + logger.error(`SQLite migration failed: ${e}`); + connection.exec("ROLLBACK"); + logger.error("Unable to start the bot, quitting now."); + return 1; + } + connection.exec("COMMIT"); +} + +export async function addCount(command) { + connection.prepare("UPDATE counts SET count = count + 1 WHERE command = ?").run(command); +} + +export async function getCounts() { + const counts = connection.prepare("SELECT * FROM counts").all(); + const countObject = {}; + for (const { command, count } of counts) { + countObject[command] = count; + } + return countObject; +} + +export async function disableCommand(guild, command) { + const guildDB = await this.getGuild(guild); + connection.prepare("UPDATE guilds SET disabled_commands = ? WHERE guild_id = ?").run(JSON.stringify((guildDB.disabledCommands ? [...JSON.parse(guildDB.disabledCommands), command] : [command]).filter((v) => !!v)), guild); + disabledCmdCache.set(guild, guildDB.disabled_commands ? [...JSON.parse(guildDB.disabledCommands), command] : [command].filter((v) => !!v)); +} + +export async function enableCommand(guild, command) { + const guildDB = await this.getGuild(guild); + const newDisabled = guildDB.disabledCommands ? JSON.parse(guildDB.disabledCommands).filter(item => item !== command) : []; + connection.prepare("UPDATE guilds SET disabled_commands = ? WHERE guild_id = ?").run(JSON.stringify(newDisabled), guild); + disabledCmdCache.set(guild, newDisabled); +} + +export async function disableChannel(channel) { + const guildDB = await this.getGuild(channel.guildID); + connection.prepare("UPDATE guilds SET disabled = ? WHERE guild_id = ?").run(JSON.stringify([...JSON.parse(guildDB.disabled), channel.id]), channel.guildID); + disabledCache.set(channel.guildID, [...JSON.parse(guildDB.disabled), channel.id]); +} + +export async function enableChannel(channel) { + const guildDB = await this.getGuild(channel.guildID); + const newDisabled = JSON.parse(guildDB.disabled).filter(item => item !== channel.id); + connection.prepare("UPDATE guilds SET disabled = ? WHERE guild_id = ?").run(JSON.stringify(newDisabled), channel.guildID); + disabledCache.set(channel.guildID, newDisabled); +} + +export async function getTag(guild, tag) { + const tagResult = connection.prepare("SELECT * FROM tags WHERE guild_id = ? AND name = ?").get(guild, tag); + return tagResult ? { content: tagResult.content, author: tagResult.author } : undefined; +} + +export async function getTags(guild) { + const tagArray = connection.prepare("SELECT * FROM tags WHERE guild_id = ?").all(guild); + const tags = {}; + if (!tagArray) return []; + for (const tag of tagArray) { + tags[tag.name] = { content: tag.content, author: tag.author }; + } + return tags; +} + +export async function setTag(name, content, guild) { + const tag = { + id: guild.id, + name: name, + content: content.content, + author: content.author + }; + connection.prepare("INSERT INTO tags (guild_id, name, content, author) VALUES (@id, @name, @content, @author)").run(tag); +} + +export async function removeTag(name, guild) { + connection.prepare("DELETE FROM tags WHERE guild_id = ? AND name = ?").run(guild.id, name); +} + +export async function editTag(name, content, guild) { + connection.prepare("UPDATE tags SET content = ?, author = ? WHERE guild_id = ? AND name = ?").run(content.content, content.author, guild.id, name); +} + +export async function setBroadcast(msg) { + connection.prepare("UPDATE settings SET broadcast = ? WHERE id = 1").run(msg); +} + +export async function getBroadcast() { + const result = connection.prepare("SELECT broadcast FROM settings WHERE id = 1").all(); + return result[0].broadcast; +} + +export async function setPrefix(prefix, guild) { + connection.prepare("UPDATE guilds SET prefix = ? WHERE guild_id = ?").run(prefix, guild.id); + prefixCache.set(guild.id, prefix); +} + +export async function getGuild(query) { + let guild; + connection.transaction(() => { + guild = connection.prepare("SELECT * FROM guilds WHERE guild_id = ?").get(query); + if (!guild) { + guild = { + id: query, + prefix: process.env.PREFIX, + disabled: "[]", + disabledCommands: "[]" + }; + connection.prepare("INSERT INTO guilds (guild_id, prefix, disabled, disabled_commands) VALUES (@id, @prefix, @disabled, @disabledCommands)").run(guild); + } + })(); + return guild; +} diff --git a/utils/handler.js b/utils/handler.js index 4cee615..d5daf7a 100644 --- a/utils/handler.js +++ b/utils/handler.js @@ -1,126 +1,126 @@ -import { paths, commands, messageCommands, info, sounds, categories, aliases as _aliases } from "./collections.js"; -import { log } from "./logger.js"; - -import { readFileSync } from "fs"; - -const { blacklist } = JSON.parse(readFileSync(new URL("../config/commands.json", import.meta.url))); - -let queryValue = 0; - -// load command into memory -export async function load(client, command, slashReload = false) { - const { default: props } = await import(`${command}?v=${queryValue}`); - queryValue++; - const commandArray = command.split("/"); - let commandName = commandArray[commandArray.length - 1].split(".")[0]; - const category = commandArray[commandArray.length - 2]; - - if (blacklist.includes(commandName)) { - log("warn", `Skipped loading blacklisted command ${command}...`); - return; - } - - if (category === "message") { - const nameStringArray = commandName.split("-"); - for (const index of nameStringArray.keys()) { - nameStringArray[index] = nameStringArray[index].charAt(0).toUpperCase() + nameStringArray[index].slice(1); - } - commandName = nameStringArray.join(" "); - } - - props.init(); - paths.set(commandName, command); - - const commandInfo = { - category: category, - description: props.description, - aliases: props.aliases, - params: props.arguments, - flags: props.flags, - slashAllowed: props.slashAllowed, - directAllowed: props.directAllowed, - adminOnly: props.adminOnly, - type: 1 - }; - - if (category === "message") { - messageCommands.set(commandName, props); - commandInfo.type = 3; - } else { - commands.set(commandName, props); - } - - if (slashReload && props.slashAllowed) { - await send(client); - } - - if (Object.getPrototypeOf(props).name === "SoundboardCommand") sounds.set(commandName, props.file); - - info.set(commandName, commandInfo); - - const categoryCommands = categories.get(category); - categories.set(category, categoryCommands ? [...categoryCommands, commandName] : [commandName]); - - if (props.aliases) { - for (const alias of props.aliases) { - _aliases.set(alias, commandName); - paths.set(alias, command); - } - } - return commandName; -} - -export function update() { - const commandArray = []; - const privateCommandArray = []; - const merged = new Map([...commands, ...messageCommands]); - for (const [name, command] of merged.entries()) { - let cmdInfo = info.get(name); - if (command.postInit) { - const cmd = command.postInit(); - cmdInfo = { - category: cmdInfo.category, - description: cmd.description, - aliases: cmd.aliases, - params: cmd.arguments, - flags: cmd.flags, - slashAllowed: cmd.slashAllowed, - directAllowed: cmd.directAllowed, - adminOnly: cmd.adminOnly, - type: cmdInfo.type - }; - info.set(name, cmdInfo); - } - if (cmdInfo?.type === 3) { - (cmdInfo.adminOnly ? privateCommandArray : commandArray).push({ - name: name, - type: cmdInfo.type, - dm_permission: cmdInfo.directAllowed - }); - } else if (cmdInfo?.slashAllowed) { - (cmdInfo.adminOnly ? privateCommandArray : commandArray).push({ - name, - type: cmdInfo.type, - description: cmdInfo.description, - options: cmdInfo.flags, - dm_permission: cmdInfo.directAllowed - }); - } - } - return { - main: commandArray, - private: privateCommandArray - }; -} - -export async function send(bot) { - const commandArray = update(); - log("info", "Sending application command data to Discord..."); - let cmdArray = commandArray.main; - if (process.env.ADMIN_SERVER && process.env.ADMIN_SERVER !== "") { - await bot.application.bulkEditGuildCommands(process.env.ADMIN_SERVER, commandArray.private); - } else { - cmdArray = [...commandArray.main, ...commandArray.private]; - } - await bot.application.bulkEditGlobalCommands(cmdArray); +import { paths, commands, messageCommands, info, sounds, categories, aliases as _aliases } from "./collections.js"; +import { log } from "./logger.js"; + +import { readFileSync } from "fs"; + +const { blacklist } = JSON.parse(readFileSync(new URL("../config/commands.json", import.meta.url))); + +let queryValue = 0; + +// load command into memory +export async function load(client, command, slashReload = false) { + const { default: props } = await import(`${command}?v=${queryValue}`); + queryValue++; + const commandArray = command.split("/"); + let commandName = commandArray[commandArray.length - 1].split(".")[0]; + const category = commandArray[commandArray.length - 2]; + + if (blacklist.includes(commandName)) { + log("warn", `Skipped loading blacklisted command ${command}...`); + return; + } + + if (category === "message") { + const nameStringArray = commandName.split("-"); + for (const index of nameStringArray.keys()) { + nameStringArray[index] = nameStringArray[index].charAt(0).toUpperCase() + nameStringArray[index].slice(1); + } + commandName = nameStringArray.join(" "); + } + + props.init(); + paths.set(commandName, command); + + const commandInfo = { + category: category, + description: props.description, + aliases: props.aliases, + params: props.arguments, + flags: props.flags, + slashAllowed: props.slashAllowed, + directAllowed: props.directAllowed, + adminOnly: props.adminOnly, + type: 1 + }; + + if (category === "message") { + messageCommands.set(commandName, props); + commandInfo.type = 3; + } else { + commands.set(commandName, props); + } + + if (slashReload && props.slashAllowed) { + await send(client); + } + + if (Object.getPrototypeOf(props).name === "SoundboardCommand") sounds.set(commandName, props.file); + + info.set(commandName, commandInfo); + + const categoryCommands = categories.get(category); + categories.set(category, categoryCommands ? [...categoryCommands, commandName] : [commandName]); + + if (props.aliases) { + for (const alias of props.aliases) { + _aliases.set(alias, commandName); + paths.set(alias, command); + } + } + return commandName; +} + +export function update() { + const commandArray = []; + const privateCommandArray = []; + const merged = new Map([...commands, ...messageCommands]); + for (const [name, command] of merged.entries()) { + let cmdInfo = info.get(name); + if (command.postInit) { + const cmd = command.postInit(); + cmdInfo = { + category: cmdInfo.category, + description: cmd.description, + aliases: cmd.aliases, + params: cmd.arguments, + flags: cmd.flags, + slashAllowed: cmd.slashAllowed, + directAllowed: cmd.directAllowed, + adminOnly: cmd.adminOnly, + type: cmdInfo.type + }; + info.set(name, cmdInfo); + } + if (cmdInfo?.type === 3) { + (cmdInfo.adminOnly ? privateCommandArray : commandArray).push({ + name: name, + type: cmdInfo.type, + dm_permission: cmdInfo.directAllowed + }); + } else if (cmdInfo?.slashAllowed) { + (cmdInfo.adminOnly ? privateCommandArray : commandArray).push({ + name, + type: cmdInfo.type, + description: cmdInfo.description, + options: cmdInfo.flags, + dm_permission: cmdInfo.directAllowed + }); + } + } + return { + main: commandArray, + private: privateCommandArray + }; +} + +export async function send(bot) { + const commandArray = update(); + log("info", "Sending application command data to Discord..."); + let cmdArray = commandArray.main; + if (process.env.ADMIN_SERVER && process.env.ADMIN_SERVER !== "") { + await bot.application.bulkEditGuildCommands(process.env.ADMIN_SERVER, commandArray.private); + } else { + cmdArray = [...commandArray.main, ...commandArray.private]; + } + await bot.application.bulkEditGlobalCommands(cmdArray); } \ No newline at end of file diff --git a/utils/help.js b/utils/help.js index 7c5729d..431073a 100644 --- a/utils/help.js +++ b/utils/help.js @@ -1,69 +1,69 @@ -import { commands, info } from "./collections.js"; -import { promises } from "fs"; - -export const categoryTemplate = { - general: [], - tags: ["> **Every command in this category is a subcommand of the tag command.**\n"], - "image-editing": ["> **These commands support the PNG, JPEG, WEBP (static), and GIF (animated or static) formats.**\n"] -}; -export let categories = categoryTemplate; - -export let generated = false; - -export function generateList() { - categories = categoryTemplate; - for (const [command] of commands) { - const category = info.get(command).category; - const description = info.get(command).description; - const params = info.get(command).params; - if (category === "tags") { - const subCommands = info.get(command).flags; - categories.tags.push(`**tags** ${params.default} - ${description}`); - for (const subCommand of subCommands) { - categories.tags.push(`**tags ${subCommand.name}**${params[subCommand.name] ? ` ${params[subCommand.name].join(" ")}` : ""} - ${subCommand.description}`); - } - } else { - if (!categories[category]) categories[category] = []; - categories[category].push(`**${command}**${params ? ` ${params.join(" ")}` : ""} - ${description}`); - } - } - generated = true; -} - -export async function createPage(output) { - let template = `# esmBot${process.env.NODE_ENV === "development" ? " Dev" : ""} Command List - -This page was last generated on \`${new Date().toString()}\`. - -\`[]\` means an argument is required, \`{}\` means an argument is optional. - -**Want to help support esmBot's development? Consider donating on Patreon!** https://patreon.com/TheEssem -`; - - template += "\n## Table of Contents\n"; - for (const category of Object.keys(categories)) { - const categoryStringArray = category.split("-"); - for (const index of categoryStringArray.keys()) { - categoryStringArray[index] = categoryStringArray[index].charAt(0).toUpperCase() + categoryStringArray[index].slice(1); - } - template += `+ [**${categoryStringArray.join(" ")}**](#${category})\n`; - } - - // hell - for (const category of Object.keys(categories)) { - const categoryStringArray = category.split("-"); - for (const index of categoryStringArray.keys()) { - categoryStringArray[index] = categoryStringArray[index].charAt(0).toUpperCase() + categoryStringArray[index].slice(1); - } - template += `\n## ${categoryStringArray.join(" ")}\n`; - for (const command of categories[category]) { - if (command.startsWith(">")) { - template += `${command}\n`; - } else { - template += `+ ${command}\n`; - } - } - } - - await promises.writeFile(output, template); +import { commands, info } from "./collections.js"; +import { promises } from "fs"; + +export const categoryTemplate = { + general: [], + tags: ["> **Every command in this category is a subcommand of the tag command.**\n"], + "image-editing": ["> **These commands support the PNG, JPEG, WEBP (static), and GIF (animated or static) formats.**\n"] +}; +export let categories = categoryTemplate; + +export let generated = false; + +export function generateList() { + categories = categoryTemplate; + for (const [command] of commands) { + const category = info.get(command).category; + const description = info.get(command).description; + const params = info.get(command).params; + if (category === "tags") { + const subCommands = info.get(command).flags; + categories.tags.push(`**tags** ${params.default} - ${description}`); + for (const subCommand of subCommands) { + categories.tags.push(`**tags ${subCommand.name}**${params[subCommand.name] ? ` ${params[subCommand.name].join(" ")}` : ""} - ${subCommand.description}`); + } + } else { + if (!categories[category]) categories[category] = []; + categories[category].push(`**${command}**${params ? ` ${params.join(" ")}` : ""} - ${description}`); + } + } + generated = true; +} + +export async function createPage(output) { + let template = `# esmBot${process.env.NODE_ENV === "development" ? " Dev" : ""} Command List + +This page was last generated on \`${new Date().toString()}\`. + +\`[]\` means an argument is required, \`{}\` means an argument is optional. + +**Want to help support esmBot's development? Consider donating on Patreon!** https://patreon.com/TheEssem +`; + + template += "\n## Table of Contents\n"; + for (const category of Object.keys(categories)) { + const categoryStringArray = category.split("-"); + for (const index of categoryStringArray.keys()) { + categoryStringArray[index] = categoryStringArray[index].charAt(0).toUpperCase() + categoryStringArray[index].slice(1); + } + template += `+ [**${categoryStringArray.join(" ")}**](#${category})\n`; + } + + // hell + for (const category of Object.keys(categories)) { + const categoryStringArray = category.split("-"); + for (const index of categoryStringArray.keys()) { + categoryStringArray[index] = categoryStringArray[index].charAt(0).toUpperCase() + categoryStringArray[index].slice(1); + } + template += `\n## ${categoryStringArray.join(" ")}\n`; + for (const command of categories[category]) { + if (command.startsWith(">")) { + template += `${command}\n`; + } else { + template += `+ ${command}\n`; + } + } + } + + await promises.writeFile(output, template); } \ No newline at end of file diff --git a/utils/image-runner.js b/utils/image-runner.js index 17948fd..3ca2ab2 100644 --- a/utils/image-runner.js +++ b/utils/image-runner.js @@ -1,73 +1,73 @@ -import { createRequire } from "module"; -import { isMainThread, parentPort, workerData } from "worker_threads"; -import { request } from "undici"; -import path from "path"; -import { fileURLToPath } from "url"; - -const nodeRequire = createRequire(import.meta.url); - -const relPath = `../build/${process.env.DEBUG && process.env.DEBUG === "true" ? "Debug" : "Release"}/image.node`; -const img = nodeRequire(relPath); - -const enumMap = { - "forget": 0, - "northwest": 1, - "north": 2, - "northeast": 3, - "west": 4, - "center": 5, - "east": 6, - "southwest": 7, - "south": 8, - "southeast": 9 -}; - -export default function run(object) { - return new Promise((resolve, reject) => { - // If the image has a path, it must also have a type - let promise = Promise.resolve(); - if (object.path) { - if (object.params.type !== "image/gif" && object.onlyGIF) resolve({ - buffer: Buffer.alloc(0), - fileExtension: "nogif" - }); - promise = request(object.path).then(res => res.body.arrayBuffer()).then(buf => Buffer.from(buf)); - } - // Convert from a MIME type (e.g. "image/png") to something the image processor understands (e.g. "png"). - // Don't set `type` directly on the object we are passed as it will be read afterwards. - // If no image type is given (say, the command generates its own image), make it a PNG. - const fileExtension = object.params.type ? object.params.type.split("/")[1] : "png"; - promise.then(buf => { - if (buf) object.params.data = buf; - const objectWithFixedType = Object.assign({}, object.params, { type: fileExtension }); - if (objectWithFixedType.gravity) { - if (isNaN(objectWithFixedType.gravity)) { - objectWithFixedType.gravity = enumMap[objectWithFixedType.gravity]; - } - } - objectWithFixedType.basePath = path.join(path.dirname(fileURLToPath(import.meta.url)), "../"); - try { - const result = img.image(object.cmd, objectWithFixedType); - const returnObject = { - buffer: result.data, - fileExtension: result.type - }; - resolve(returnObject); - } catch (e) { - reject(e); - } - }); - }); -} - -if (!isMainThread) { - run(workerData) - .then(returnObject => { - parentPort.postMessage(returnObject); - process.exit(); - }) - .catch(err => { - // turn promise rejection into normal error - throw err; - }); -} +import { createRequire } from "module"; +import { isMainThread, parentPort, workerData } from "worker_threads"; +import { request } from "undici"; +import path from "path"; +import { fileURLToPath } from "url"; + +const nodeRequire = createRequire(import.meta.url); + +const relPath = `../build/${process.env.DEBUG && process.env.DEBUG === "true" ? "Debug" : "Release"}/image.node`; +const img = nodeRequire(relPath); + +const enumMap = { + "forget": 0, + "northwest": 1, + "north": 2, + "northeast": 3, + "west": 4, + "center": 5, + "east": 6, + "southwest": 7, + "south": 8, + "southeast": 9 +}; + +export default function run(object) { + return new Promise((resolve, reject) => { + // If the image has a path, it must also have a type + let promise = Promise.resolve(); + if (object.path) { + if (object.params.type !== "image/gif" && object.onlyGIF) resolve({ + buffer: Buffer.alloc(0), + fileExtension: "nogif" + }); + promise = request(object.path).then(res => res.body.arrayBuffer()).then(buf => Buffer.from(buf)); + } + // Convert from a MIME type (e.g. "image/png") to something the image processor understands (e.g. "png"). + // Don't set `type` directly on the object we are passed as it will be read afterwards. + // If no image type is given (say, the command generates its own image), make it a PNG. + const fileExtension = object.params.type ? object.params.type.split("/")[1] : "png"; + promise.then(buf => { + if (buf) object.params.data = buf; + const objectWithFixedType = Object.assign({}, object.params, { type: fileExtension }); + if (objectWithFixedType.gravity) { + if (isNaN(objectWithFixedType.gravity)) { + objectWithFixedType.gravity = enumMap[objectWithFixedType.gravity]; + } + } + objectWithFixedType.basePath = path.join(path.dirname(fileURLToPath(import.meta.url)), "../"); + try { + const result = img.image(object.cmd, objectWithFixedType); + const returnObject = { + buffer: result.data, + fileExtension: result.type + }; + resolve(returnObject); + } catch (e) { + reject(e); + } + }); + }); +} + +if (!isMainThread) { + run(workerData) + .then(returnObject => { + parentPort.postMessage(returnObject); + process.exit(); + }) + .catch(err => { + // turn promise rejection into normal error + throw err; + }); +} diff --git a/utils/image.js b/utils/image.js index bf0f0b3..a4d2391 100644 --- a/utils/image.js +++ b/utils/image.js @@ -1,170 +1,170 @@ -import { request } from "undici"; -import fs from "fs"; -import path from "path"; -import { fileURLToPath } from "url"; -import { Worker } from "worker_threads"; -import { createRequire } from "module"; -import { fileTypeFromBuffer, fileTypeFromFile } from "file-type"; -import * as logger from "./logger.js"; -import ImageConnection from "./imageConnection.js"; - -// only requiring this to work around an issue regarding worker threads -const nodeRequire = createRequire(import.meta.url); -if (!process.env.API_TYPE || process.env.API_TYPE === "none") { - nodeRequire(`../build/${process.env.DEBUG && process.env.DEBUG === "true" ? "Debug" : "Release"}/image.node`); -} - -const formats = ["image/jpeg", "image/png", "image/webp", "image/gif", "video/mp4", "video/webm", "video/quicktime"]; -export const connections = new Map(); -export let servers = process.env.API_TYPE === "ws" ? JSON.parse(fs.readFileSync(new URL("../config/servers.json", import.meta.url), { encoding: "utf8" })).image : null; - -export async function getType(image, extraReturnTypes) { - if (!image.startsWith("http")) { - const imageType = await fileTypeFromFile(image); - if (imageType && formats.includes(imageType.mime)) { - return imageType.mime; - } - return undefined; - } - let type; - const controller = new AbortController(); - const timeout = setTimeout(() => { - controller.abort(); - }, 3000); - try { - const imageRequest = await request(image, { - signal: controller.signal, - method: "HEAD" - }); - clearTimeout(timeout); - const size = imageRequest.headers["content-range"] ? imageRequest.headers["content-range"].split("/")[1] : imageRequest.headers["content-length"]; - if (parseInt(size) > 26214400 && extraReturnTypes) { // 25 MB - type = "large"; - return type; - } - const typeHeader = imageRequest.headers["content-type"]; - if (typeHeader) { - type = typeHeader; - } else { - const timeout = setTimeout(() => { - controller.abort(); - }, 3000); - const bufRequest = await request(image, { - signal: controller.signal, - headers: { - range: "bytes=0-1023" - } - }); - clearTimeout(timeout); - const imageBuffer = await bufRequest.body.arrayBuffer(); - const imageType = await fileTypeFromBuffer(imageBuffer); - if (imageType && formats.includes(imageType.mime)) { - type = imageType.mime; - } - } - } catch (error) { - if (error.name === "AbortError") { - throw Error("Timed out"); - } else { - throw error; - } - } finally { - clearTimeout(timeout); - } - return type; -} - -function connect(server, auth) { - const connection = new ImageConnection(server, auth); - connections.set(server, connection); -} - -function disconnect() { - for (const connection of connections.values()) { - connection.close(); - } - connections.clear(); -} - -async function repopulate() { - const data = await fs.promises.readFile(new URL("../config/servers.json", import.meta.url), { encoding: "utf8" }); - servers = JSON.parse(data).image; -} - -export async function reloadImageConnections() { - disconnect(); - await repopulate(); - let amount = 0; - for (const server of servers) { - try { - connect(server.server, server.auth); - amount += 1; - } catch (e) { - logger.error(e); - } - } - return amount; -} - -function chooseServer(ideal) { - if (ideal.length === 0) throw "No available servers"; - const sorted = ideal.sort((a, b) => { - return a.load - b.load; - }); - return sorted[0]; -} - -async function getIdeal(object) { - const idealServers = []; - for (const [address, connection] of connections) { - if (connection.conn.readyState !== 0 && connection.conn.readyState !== 1) { - continue; - } - if (object.params.type && !connection.formats[object.cmd]?.includes(object.params.type)) continue; - idealServers.push({ - addr: address, - load: await connection.getCount() - }); - } - const server = chooseServer(idealServers); - return connections.get(server.addr); -} - -function waitForWorker(worker) { - return new Promise((resolve, reject) => { - worker.once("message", (data) => { - resolve({ - buffer: Buffer.from([...data.buffer]), - type: data.fileExtension - }); - }); - worker.once("error", reject); - }); -} - -export async function runImageJob(params) { - if (process.env.API_TYPE === "ws") { - for (let i = 0; i < 3; i++) { - const currentServer = await getIdeal(params); - try { - await currentServer.queue(BigInt(params.id), params); - await currentServer.wait(BigInt(params.id)); - const output = await currentServer.getOutput(params.id); - return output; - } catch (e) { - if (i < 2 && e === "Request ended prematurely due to a closed connection") { - continue; - } else { - if (e === "No available servers" && i >= 2) throw "Request ended prematurely due to a closed connection"; - throw e; - } - } - } - } else { - // Called from command (not using image API) - const worker = new Worker(path.join(path.dirname(fileURLToPath(import.meta.url)), "./image-runner.js"), { - workerData: params - }); - return await waitForWorker(worker); - } +import { request } from "undici"; +import fs from "fs"; +import path from "path"; +import { fileURLToPath } from "url"; +import { Worker } from "worker_threads"; +import { createRequire } from "module"; +import { fileTypeFromBuffer, fileTypeFromFile } from "file-type"; +import * as logger from "./logger.js"; +import ImageConnection from "./imageConnection.js"; + +// only requiring this to work around an issue regarding worker threads +const nodeRequire = createRequire(import.meta.url); +if (!process.env.API_TYPE || process.env.API_TYPE === "none") { + nodeRequire(`../build/${process.env.DEBUG && process.env.DEBUG === "true" ? "Debug" : "Release"}/image.node`); +} + +const formats = ["image/jpeg", "image/png", "image/webp", "image/gif", "video/mp4", "video/webm", "video/quicktime"]; +export const connections = new Map(); +export let servers = process.env.API_TYPE === "ws" ? JSON.parse(fs.readFileSync(new URL("../config/servers.json", import.meta.url), { encoding: "utf8" })).image : null; + +export async function getType(image, extraReturnTypes) { + if (!image.startsWith("http")) { + const imageType = await fileTypeFromFile(image); + if (imageType && formats.includes(imageType.mime)) { + return imageType.mime; + } + return undefined; + } + let type; + const controller = new AbortController(); + const timeout = setTimeout(() => { + controller.abort(); + }, 3000); + try { + const imageRequest = await request(image, { + signal: controller.signal, + method: "HEAD" + }); + clearTimeout(timeout); + const size = imageRequest.headers["content-range"] ? imageRequest.headers["content-range"].split("/")[1] : imageRequest.headers["content-length"]; + if (parseInt(size) > 26214400 && extraReturnTypes) { // 25 MB + type = "large"; + return type; + } + const typeHeader = imageRequest.headers["content-type"]; + if (typeHeader) { + type = typeHeader; + } else { + const timeout = setTimeout(() => { + controller.abort(); + }, 3000); + const bufRequest = await request(image, { + signal: controller.signal, + headers: { + range: "bytes=0-1023" + } + }); + clearTimeout(timeout); + const imageBuffer = await bufRequest.body.arrayBuffer(); + const imageType = await fileTypeFromBuffer(imageBuffer); + if (imageType && formats.includes(imageType.mime)) { + type = imageType.mime; + } + } + } catch (error) { + if (error.name === "AbortError") { + throw Error("Timed out"); + } else { + throw error; + } + } finally { + clearTimeout(timeout); + } + return type; +} + +function connect(server, auth) { + const connection = new ImageConnection(server, auth); + connections.set(server, connection); +} + +function disconnect() { + for (const connection of connections.values()) { + connection.close(); + } + connections.clear(); +} + +async function repopulate() { + const data = await fs.promises.readFile(new URL("../config/servers.json", import.meta.url), { encoding: "utf8" }); + servers = JSON.parse(data).image; +} + +export async function reloadImageConnections() { + disconnect(); + await repopulate(); + let amount = 0; + for (const server of servers) { + try { + connect(server.server, server.auth); + amount += 1; + } catch (e) { + logger.error(e); + } + } + return amount; +} + +function chooseServer(ideal) { + if (ideal.length === 0) throw "No available servers"; + const sorted = ideal.sort((a, b) => { + return a.load - b.load; + }); + return sorted[0]; +} + +async function getIdeal(object) { + const idealServers = []; + for (const [address, connection] of connections) { + if (connection.conn.readyState !== 0 && connection.conn.readyState !== 1) { + continue; + } + if (object.params.type && !connection.formats[object.cmd]?.includes(object.params.type)) continue; + idealServers.push({ + addr: address, + load: await connection.getCount() + }); + } + const server = chooseServer(idealServers); + return connections.get(server.addr); +} + +function waitForWorker(worker) { + return new Promise((resolve, reject) => { + worker.once("message", (data) => { + resolve({ + buffer: Buffer.from([...data.buffer]), + type: data.fileExtension + }); + }); + worker.once("error", reject); + }); +} + +export async function runImageJob(params) { + if (process.env.API_TYPE === "ws") { + for (let i = 0; i < 3; i++) { + const currentServer = await getIdeal(params); + try { + await currentServer.queue(BigInt(params.id), params); + await currentServer.wait(BigInt(params.id)); + const output = await currentServer.getOutput(params.id); + return output; + } catch (e) { + if (i < 2 && e === "Request ended prematurely due to a closed connection") { + continue; + } else { + if (e === "No available servers" && i >= 2) throw "Request ended prematurely due to a closed connection"; + throw e; + } + } + } + } else { + // Called from command (not using image API) + const worker = new Worker(path.join(path.dirname(fileURLToPath(import.meta.url)), "./image-runner.js"), { + workerData: params + }); + return await waitForWorker(worker); + } } \ No newline at end of file diff --git a/utils/imageConnection.js b/utils/imageConnection.js index 1488359..5a05551 100644 --- a/utils/imageConnection.js +++ b/utils/imageConnection.js @@ -1,171 +1,171 @@ -import { request } from "undici"; -import WebSocket from "ws"; -import * as logger from "./logger.js"; -import { setTimeout } from "timers/promises"; - -const Rerror = 0x01; -const Tqueue = 0x02; -const Rqueue = 0x03; -const Tcancel = 0x04; -const Rcancel = 0x05; -const Twait = 0x06; -const Rwait = 0x07; -const Rinit = 0x08; - -class ImageConnection { - constructor(host, auth, tls = false) { - this.requests = new Map(); - if (!host.includes(":")) { - host += ":3762"; - } - this.host = host; - this.auth = auth; - this.tag = 0; - this.disconnected = false; - this.formats = {}; - this.wsproto = null; - if (tls) { - this.wsproto = "wss"; - } else { - this.wsproto = "ws"; - } - this.sockurl = `${this.wsproto}://${host}/sock`; - const headers = {}; - if (auth) { - headers.Authentication = auth; - } - this.conn = new WebSocket(this.sockurl, { headers }); - let httpproto; - if (tls) { - httpproto = "https"; - } else { - httpproto = "http"; - } - this.httpurl = `${httpproto}://${host}`; - this.conn.on("message", (msg) => this.onMessage(msg)); - this.conn.once("error", (err) => this.onError(err)); - this.conn.once("close", () => this.onClose()); - } - - async onMessage(msg) { - const op = msg.readUint8(0); - if (op === Rinit) { - this.formats = JSON.parse(msg.toString("utf8", 7)); - return; - } - const tag = msg.readUint16LE(1); - const promise = this.requests.get(tag); - if (!promise) { - logger.error(`Received response for unknown request ${tag}`); - return; - } - this.requests.delete(tag); - if (op === Rerror) { - promise.reject(new Error(msg.slice(3, msg.length).toString())); - return; - } - promise.resolve(); - } - - onError(e) { - logger.error(e.toString()); - } - - async onClose() { - for (const [tag, obj] of this.requests.entries()) { - obj.reject("Request ended prematurely due to a closed connection"); - this.requests.delete(tag); - } - if (!this.disconnected) { - logger.warn(`Lost connection to ${this.host}, attempting to reconnect in 5 seconds...`); - await setTimeout(5000); - this.conn = new WebSocket(this.sockurl, { - headers: { - "Authentication": this.auth - } - }); - this.conn.on("message", (msg) => this.onMessage(msg)); - this.conn.once("error", (err) => this.onError(err)); - this.conn.once("close", () => this.onClose()); - } - this.disconnected = false; - } - - close() { - this.disconnected = true; - this.conn.close(); - } - - queue(jobid, jobobj) { - const str = JSON.stringify(jobobj); - const buf = Buffer.alloc(8); - buf.writeBigUint64LE(jobid); - return this.do(Tqueue, jobid, Buffer.concat([buf, Buffer.from(str)])); - } - - wait(jobid) { - const buf = Buffer.alloc(8); - buf.writeBigUint64LE(jobid); - return this.do(Twait, jobid, buf); - } - - cancel(jobid) { - const buf = Buffer.alloc(8); - buf.writeBigUint64LE(jobid); - return this.do(Tcancel, jobid, buf); - } - - async getOutput(jobid) { - const req = await request(`${this.httpurl}/image?id=${jobid}`, { - headers: { - authentication: this.auth || undefined - } - }); - const contentType = req.headers["content-type"]; - let type; - switch (contentType) { - case "image/gif": - type = "gif"; - break; - case "image/png": - type = "png"; - break; - case "image/jpeg": - type = "jpg"; - break; - case "image/webp": - type = "webp"; - break; - default: - type = contentType; - break; - } - return { buffer: Buffer.from(await req.body.arrayBuffer()), type }; - } - - async getCount() { - const req = await request(`${this.httpurl}/count`, { - headers: { - authentication: this.auth || undefined - } - }); - if (req.statusCode !== 200) return; - const res = parseInt(await req.body.text()); - return res; - } - - async do(op, id, data) { - const buf = Buffer.alloc(1 + 2); - let tag = this.tag++; - if (tag > 65535) tag = this.tag = 0; - buf.writeUint8(op); - buf.writeUint16LE(tag, 1); - this.conn.send(Buffer.concat([buf, data])); - const promise = new Promise((resolve, reject) => { - this.requests.set(tag, { resolve, reject, id, op }); - }); - return promise; - } -} - -export default ImageConnection; +import { request } from "undici"; +import WebSocket from "ws"; +import * as logger from "./logger.js"; +import { setTimeout } from "timers/promises"; + +const Rerror = 0x01; +const Tqueue = 0x02; +const Rqueue = 0x03; +const Tcancel = 0x04; +const Rcancel = 0x05; +const Twait = 0x06; +const Rwait = 0x07; +const Rinit = 0x08; + +class ImageConnection { + constructor(host, auth, tls = false) { + this.requests = new Map(); + if (!host.includes(":")) { + host += ":3762"; + } + this.host = host; + this.auth = auth; + this.tag = 0; + this.disconnected = false; + this.formats = {}; + this.wsproto = null; + if (tls) { + this.wsproto = "wss"; + } else { + this.wsproto = "ws"; + } + this.sockurl = `${this.wsproto}://${host}/sock`; + const headers = {}; + if (auth) { + headers.Authentication = auth; + } + this.conn = new WebSocket(this.sockurl, { headers }); + let httpproto; + if (tls) { + httpproto = "https"; + } else { + httpproto = "http"; + } + this.httpurl = `${httpproto}://${host}`; + this.conn.on("message", (msg) => this.onMessage(msg)); + this.conn.once("error", (err) => this.onError(err)); + this.conn.once("close", () => this.onClose()); + } + + async onMessage(msg) { + const op = msg.readUint8(0); + if (op === Rinit) { + this.formats = JSON.parse(msg.toString("utf8", 7)); + return; + } + const tag = msg.readUint16LE(1); + const promise = this.requests.get(tag); + if (!promise) { + logger.error(`Received response for unknown request ${tag}`); + return; + } + this.requests.delete(tag); + if (op === Rerror) { + promise.reject(new Error(msg.slice(3, msg.length).toString())); + return; + } + promise.resolve(); + } + + onError(e) { + logger.error(e.toString()); + } + + async onClose() { + for (const [tag, obj] of this.requests.entries()) { + obj.reject("Request ended prematurely due to a closed connection"); + this.requests.delete(tag); + } + if (!this.disconnected) { + logger.warn(`Lost connection to ${this.host}, attempting to reconnect in 5 seconds...`); + await setTimeout(5000); + this.conn = new WebSocket(this.sockurl, { + headers: { + "Authentication": this.auth + } + }); + this.conn.on("message", (msg) => this.onMessage(msg)); + this.conn.once("error", (err) => this.onError(err)); + this.conn.once("close", () => this.onClose()); + } + this.disconnected = false; + } + + close() { + this.disconnected = true; + this.conn.close(); + } + + queue(jobid, jobobj) { + const str = JSON.stringify(jobobj); + const buf = Buffer.alloc(8); + buf.writeBigUint64LE(jobid); + return this.do(Tqueue, jobid, Buffer.concat([buf, Buffer.from(str)])); + } + + wait(jobid) { + const buf = Buffer.alloc(8); + buf.writeBigUint64LE(jobid); + return this.do(Twait, jobid, buf); + } + + cancel(jobid) { + const buf = Buffer.alloc(8); + buf.writeBigUint64LE(jobid); + return this.do(Tcancel, jobid, buf); + } + + async getOutput(jobid) { + const req = await request(`${this.httpurl}/image?id=${jobid}`, { + headers: { + authentication: this.auth || undefined + } + }); + const contentType = req.headers["content-type"]; + let type; + switch (contentType) { + case "image/gif": + type = "gif"; + break; + case "image/png": + type = "png"; + break; + case "image/jpeg": + type = "jpg"; + break; + case "image/webp": + type = "webp"; + break; + default: + type = contentType; + break; + } + return { buffer: Buffer.from(await req.body.arrayBuffer()), type }; + } + + async getCount() { + const req = await request(`${this.httpurl}/count`, { + headers: { + authentication: this.auth || undefined + } + }); + if (req.statusCode !== 200) return; + const res = parseInt(await req.body.text()); + return res; + } + + async do(op, id, data) { + const buf = Buffer.alloc(1 + 2); + let tag = this.tag++; + if (tag > 65535) tag = this.tag = 0; + buf.writeUint8(op); + buf.writeUint16LE(tag, 1); + this.conn.send(Buffer.concat([buf, data])); + const promise = new Promise((resolve, reject) => { + this.requests.set(tag, { resolve, reject, id, op }); + }); + return promise; + } +} + +export default ImageConnection; diff --git a/utils/imagedetect.js b/utils/imagedetect.js index c04eaf4..9e537ee 100644 --- a/utils/imagedetect.js +++ b/utils/imagedetect.js @@ -1,189 +1,190 @@ -import { request } from "undici"; -import { getType } from "./image.js"; - -const tenorURLs = [ - "tenor.com", - "www.tenor.com" -]; -const giphyURLs = [ - "giphy.com", - "www.giphy.com", - "i.giphy.com" -]; -const giphyMediaURLs = [ // there could be more of these - "media.giphy.com", - "media0.giphy.com", - "media1.giphy.com", - "media2.giphy.com", - "media3.giphy.com", - "media4.giphy.com" -]; -const imgurURLs = [ - "imgur.com", - "www.imgur.com", - "i.imgur.com" -]; -const gfycatURLs = [ - "gfycat.com", - "www.gfycat.com", - "thumbs.gfycat.com", - "giant.gfycat.com" -]; - -const combined = [...tenorURLs, ...giphyURLs, ...giphyMediaURLs, ...imgurURLs, ...gfycatURLs]; - -const imageFormats = ["image/jpeg", "image/png", "image/webp", "image/gif", "large"]; -const videoFormats = ["video/mp4", "video/webm", "video/mov"]; - -// gets the proper image paths -const getImage = async (image, image2, video, extraReturnTypes, gifv = false, type = null, link = false) => { - try { - const fileNameSplit = new URL(image).pathname.split("/"); - const fileName = fileNameSplit[fileNameSplit.length - 1]; - const fileNameNoExtension = fileName.slice(0, fileName.lastIndexOf(".")); - const payload = { - url: image2, - path: image, - name: fileNameNoExtension - }; - const host = new URL(image2).host; - if (gifv || (link && combined.includes(host))) { - if (tenorURLs.includes(host)) { - // Tenor doesn't let us access a raw GIF without going through their API, - // so we use that if there's a key in the config - if (process.env.TENOR !== "") { - let id; - if (image2.includes("tenor.com/view/")) { - id = image2.split("-").pop(); - } else if (image2.endsWith(".gif")) { - const redirect = (await request(image2, { method: "HEAD" })).headers.location; - id = redirect.split("-").pop(); - } - const data = await request(`https://tenor.googleapis.com/v2/posts?ids=${id}&media_filter=gif&limit=1&client_key=esmBot%20${process.env.ESMBOT_VER}&key=${process.env.TENOR}`); - if (data.statusCode === 429) { - if (extraReturnTypes) { - payload.type = "tenorlimit"; - return payload; - } else { - return; - } - } - const json = await data.body.json(); - if (json.error) throw Error(json.error.message); - payload.path = json.results[0].media_formats.gif.url; - } - } else if (giphyURLs.includes(host)) { - // Can result in an HTML page instead of a GIF - payload.path = `https://media0.giphy.com/media/${image2.split("/")[4].split("-").pop()}/giphy.gif`; - } else if (giphyMediaURLs.includes(host)) { - payload.path = `https://media0.giphy.com/media/${image2.split("/")[4]}/giphy.gif`; - } else if (imgurURLs.includes(host)) { - // Seems that Imgur has a possibility of making GIFs static - payload.path = image.replace(".mp4", ".gif"); - } else if (gfycatURLs.includes(host)) { - // iirc Gfycat also seems to sometimes make GIFs static - if (link) { - const data = await request(`https://api.gfycat.com/v1/gfycats/${image.split("/").pop().split(".mp4")[0]}`); - const json = await data.body.json(); - if (json.errorMessage) throw Error(json.errorMessage); - payload.path = json.gfyItem.gifUrl; - } else { - payload.path = `https://thumbs.gfycat.com/${image.split("/").pop().split(".mp4")[0]}-size_restricted.gif`; - } - } - payload.type = "image/gif"; - } else if (video) { - payload.type = type ?? await getType(payload.path, extraReturnTypes); - if (!payload.type || (!videoFormats.includes(payload.type) && !imageFormats.includes(payload.type))) return; - } else { - payload.type = type ?? await getType(payload.path, extraReturnTypes); - if (!payload.type || !imageFormats.includes(payload.type)) return; - } - return payload; - } catch (error) { - if (error.name === "AbortError") { - throw Error("Timed out"); - } else { - throw error; - } - } -}; - -const checkImages = async (message, extraReturnTypes, video, sticker) => { - let type; - if (sticker && message.stickerItems) { - type = message.stickerItems[0]; - } else { - // first check the embeds - if (message.embeds.length !== 0) { - // embeds can vary in types, we check for tenor gifs first - if (message.embeds[0].type === "gifv") { - type = await getImage(message.embeds[0].video.url, message.embeds[0].url, video, extraReturnTypes, true); - // then we check for other image types - } else if ((message.embeds[0].type === "video" || message.embeds[0].type === "image") && message.embeds[0].thumbnail) { - type = await getImage(message.embeds[0].thumbnail.proxyURL, message.embeds[0].thumbnail.url, video, extraReturnTypes); - // finally we check both possible image fields for "generic" embeds - } else if (message.embeds[0].type === "rich" || message.embeds[0].type === "article") { - if (message.embeds[0].thumbnail) { - type = await getImage(message.embeds[0].thumbnail.proxyURL, message.embeds[0].thumbnail.url, video, extraReturnTypes); - } else if (message.embeds[0].image) { - type = await getImage(message.embeds[0].image.proxyURL, message.embeds[0].image.url, video, extraReturnTypes); - } - } - // then check the attachments - } else if (message.attachments.size !== 0 && message.attachments.first().width) { - type = await getImage(message.attachments.first().proxyURL, message.attachments.first().url, video); - } - } - // if the return value exists then return it - return type ?? false; -}; - -// this checks for the latest message containing an image and returns the url of the image -export default async (client, cmdMessage, interaction, options, extraReturnTypes = false, video = false, sticker = false, singleMessage = false) => { - // we start by determining whether or not we're dealing with an interaction or a message - if (interaction) { - // we can get a raw attachment or a URL in the interaction itself - if (options) { - if (options.image) { - const attachment = interaction.data.resolved.attachments.get(options.image); - const result = await getImage(attachment.proxyURL, attachment.url, video, attachment.contentType); - if (result !== false) return result; - } else if (options.link) { - const result = await getImage(options.link, options.link, video, extraReturnTypes, false, null, true); - if (result !== false) return result; - } - } - } - if (cmdMessage) { - // check if the message is a reply to another message - if (cmdMessage.messageReference && !singleMessage) { - const replyMessage = await client.rest.channels.getMessage(cmdMessage.messageReference.channelID, cmdMessage.messageReference.messageID).catch(() => undefined); - if (replyMessage) { - const replyResult = await checkImages(replyMessage, extraReturnTypes, video, sticker); - if (replyResult !== false) return replyResult; - } - } - // then we check the current message - const result = await checkImages(cmdMessage, extraReturnTypes, video, sticker); - if (result !== false) return result; - } - if (!singleMessage) { - // if there aren't any replies or interaction attachments then iterate over the last few messages in the channel - try { - const channel = (interaction ? interaction : cmdMessage).channel ?? await client.rest.channels.get((interaction ? interaction : cmdMessage).channelID); - const messages = await channel.getMessages(); - // iterate over each message - for (const message of messages) { - const result = await checkImages(message, extraReturnTypes, video, sticker); - if (result === false) { - continue; - } else { - return result; - } - } - } catch { - // no-op - } - } -}; +import { request } from "undici"; +import { getType } from "./image.js"; + +const tenorURLs = [ + "tenor.com", + "www.tenor.com" +]; +const giphyURLs = [ + "giphy.com", + "www.giphy.com", + "i.giphy.com" +]; +const giphyMediaURLs = [ // there could be more of these + "media.giphy.com", + "media0.giphy.com", + "media1.giphy.com", + "media2.giphy.com", + "media3.giphy.com", + "media4.giphy.com" +]; +const imgurURLs = [ + "imgur.com", + "www.imgur.com", + "i.imgur.com" +]; +const gfycatURLs = [ + "gfycat.com", + "www.gfycat.com", + "thumbs.gfycat.com", + "giant.gfycat.com" +]; + +const combined = [...tenorURLs, ...giphyURLs, ...giphyMediaURLs, ...imgurURLs, ...gfycatURLs]; + +const imageFormats = ["image/jpeg", "image/png", "image/webp", "image/gif", "large"]; +const videoFormats = ["video/mp4", "video/webm", "video/mov"]; + +// gets the proper image paths +const getImage = async (image, image2, video, extraReturnTypes, gifv = false, type = null, link = false) => { + try { + const fileNameSplit = new URL(image).pathname.split("/"); + const fileName = fileNameSplit[fileNameSplit.length - 1]; + const fileNameNoExtension = fileName.slice(0, fileName.lastIndexOf(".")); + const payload = { + url: image2, + path: image, + name: fileNameNoExtension + }; + const host = new URL(image2).host; + if (gifv || (link && combined.includes(host))) { + if (tenorURLs.includes(host)) { + // Tenor doesn't let us access a raw GIF without going through their API, + // so we use that if there's a key in the config + if (process.env.TENOR !== "") { + let id; + if (image2.includes("tenor.com/view/")) { + id = image2.split("-").pop(); + } else if (image2.endsWith(".gif")) { + const redirect = (await request(image2, { method: "HEAD" })).headers.location; + id = redirect.split("-").pop(); + } + const data = await request(`https://tenor.googleapis.com/v2/posts?ids=${id}&media_filter=gif&limit=1&client_key=esmBot%20${process.env.ESMBOT_VER}&key=${process.env.TENOR}`); + if (data.statusCode === 429) { + if (extraReturnTypes) { + payload.type = "tenorlimit"; + return payload; + } else { + return; + } + } + const json = await data.body.json(); + if (json.error) throw Error(json.error.message); + payload.path = json.results[0].media_formats.gif.url; + } + } else if (giphyURLs.includes(host)) { + // Can result in an HTML page instead of a GIF + payload.path = `https://media0.giphy.com/media/${image2.split("/")[4].split("-").pop()}/giphy.gif`; + } else if (giphyMediaURLs.includes(host)) { + payload.path = `https://media0.giphy.com/media/${image2.split("/")[4]}/giphy.gif`; + } else if (imgurURLs.includes(host)) { + // Seems that Imgur has a possibility of making GIFs static + payload.path = image.replace(".mp4", ".gif"); + } else if (gfycatURLs.includes(host)) { + // iirc Gfycat also seems to sometimes make GIFs static + if (link) { + const data = await request(`https://api.gfycat.com/v1/gfycats/${image.split("/").pop().split(".mp4")[0]}`); + const json = await data.body.json(); + if (json.errorMessage) throw Error(json.errorMessage); + payload.path = json.gfyItem.gifUrl; + } else { + payload.path = `https://thumbs.gfycat.com/${image.split("/").pop().split(".mp4")[0]}-size_restricted.gif`; + } + } + payload.type = "image/gif"; + } else if (video) { + payload.type = type ?? await getType(payload.path, extraReturnTypes); + if (!payload.type || (!videoFormats.includes(payload.type) && !imageFormats.includes(payload.type))) return; + } else { + payload.type = type ?? await getType(payload.path, extraReturnTypes); + if (!payload.type || !imageFormats.includes(payload.type)) return; + } + return payload; + } catch (error) { + if (error.name === "AbortError") { + throw Error("Timed out"); + } else { + throw error; + } + } +}; + +const urlFromMxc = async (mxcUri) => { + const stripped = mxcUri.replace("mxc://", "") + return process.env.MATRIX_BASEURL+"/_matrix/media/r0/download/"+stripped +} + +const checkImages = async (message, extraReturnTypes, video, sticker) => { + let type; + // console.log(message) + if (typeof message.content.info !== undefined) { + if (message.content.msgtype == "m.image") { + const url = await urlFromMxc(message.content.url) + const fileNameNoExtension = message.content.body.slice(0, message.content.body.lastIndexOf(".")); + type = {name: fileNameNoExtension, path: url, url: url, type: message.content.info.mimetype} + } + } + // // first check the embeds + // if (message.embeds.length !== 0) { + // // embeds can vary in types, we check for tenor gifs first + // if (message.embeds[0].type === "gifv") { + // type = await getImage(message.embeds[0].video.url, message.embeds[0].url, video, extraReturnTypes, true); + // // then we check for other image types + // } else if ((message.embeds[0].type === "video" || message.embeds[0].type === "image") && message.embeds[0].thumbnail) { + // type = await getImage(message.embeds[0].thumbnail.proxyURL, message.embeds[0].thumbnail.url, video, extraReturnTypes); + // // finally we check both possible image fields for "generic" embeds + // } else if (message.embeds[0].type === "rich" || message.embeds[0].type === "article") { + // if (message.embeds[0].thumbnail) { + // type = await getImage(message.embeds[0].thumbnail.proxyURL, message.embeds[0].thumbnail.url, video, extraReturnTypes); + // } else if (message.embeds[0].image) { + // type = await getImage(message.embeds[0].image.proxyURL, message.embeds[0].image.url, video, extraReturnTypes); + // } + // } + // // then check the attachments + // } else if (message.attachments.size !== 0 && message.attachments.first().width) { + // type = await getImage(message.attachments.first().proxyURL, message.attachments.first().url, video); + // } + // if the return value exists then return it + return type ?? false; +}; + +// this checks for the latest message containing an image and returns the url of the image +export default async (client, cmdMessage, interaction, options, extraReturnTypes = false, video = false, sticker = false, singleMessage = false) => { + // we start by determining whether or not we're dealing with an interaction or a message + if (cmdMessage) { + // console.log(cmdMessage) + // let channel = await client.getRoom(cmdMessage.room_id); + // console.log(channel) + // check if the message is a reply to another message + // console.log(cmdMessage.content['m.relates_to']) + if (cmdMessage.content['m.relates_to'] !== undefined) { + const replyMessage = await client.fetchRoomEvent(cmdMessage.room_id, cmdMessage.content['m.relates_to']['m.in_reply_to'].event_id) + // console.log(replyMessage) + if (replyMessage) { + const replyResult = await checkImages(replyMessage, extraReturnTypes, video, sticker); + if (replyResult !== false) return replyResult; + } + } + // then we check the current message + const result = await checkImages(cmdMessage, extraReturnTypes, video, sticker); + if (result !== false) return result; + } + // if (!singleMessage) { + // // if there aren't any replies or interaction attachments then iterate over the last few messages in the channel + // try { + // const channel = (interaction ? interaction : cmdMessage).channel ?? await client.rest.channels.get((interaction ? interaction : cmdMessage).channelID); + // const messages = await channel.getMessages(); + // // iterate over each message + // for (const message of messages) { + // const result = await checkImages(message, extraReturnTypes, video, sticker); + // if (result === false) { + // continue; + // } else { + // return result; + // } + // } + // } catch { + // // no-op + // } + // } +}; diff --git a/utils/logger.js b/utils/logger.js index 95dfed7..09f2d8a 100644 --- a/utils/logger.js +++ b/utils/logger.js @@ -1,44 +1,44 @@ -import winston from "winston"; -import "winston-daily-rotate-file"; - -export const logger = winston.createLogger({ - levels: { - error: 0, - warn: 1, - info: 2, - main: 3, - debug: 4 - }, - transports: [ - new winston.transports.Console({ format: winston.format.colorize({ all: true }), stderrLevels: ["error", "warn"] }), - new winston.transports.DailyRotateFile({ filename: "logs/error-%DATE%.log", level: "error", zippedArchive: true, maxSize: 4194304, maxFiles: 8 }), - new winston.transports.DailyRotateFile({ filename: "logs/main-%DATE%.log", zippedArchive: true, maxSize: 4194304, maxFiles: 8 }) - ], - level: process.env.DEBUG_LOG ? "debug" : "main", - format: winston.format.combine( - winston.format.timestamp({ format: "YYYY-MM-DD HH:mm:ss" }), - winston.format.printf((info) => { - const { - timestamp, level, message, ...args - } = info; - - return `[${timestamp}]: [${level.toUpperCase()}] - ${message} ${Object.keys(args).length ? JSON.stringify(args, null, 2) : ""}`; - }), - ) -}); - -winston.addColors({ - info: "green", - main: "gray", - debug: "magenta", - warn: "yellow", - error: "red" -}); - -export function log(type, content) { return content ? logger.log(type === "log" ? "main" : type, content) : logger.info(type); } - -export function error(...args) { return log("error", ...args); } - -export function warn(...args) { return log("warn", ...args); } - -export function debug(...args) { return log("debug", ...args); } +import winston from "winston"; +import "winston-daily-rotate-file"; + +export const logger = winston.createLogger({ + levels: { + error: 0, + warn: 1, + info: 2, + main: 3, + debug: 4 + }, + transports: [ + new winston.transports.Console({ format: winston.format.colorize({ all: true }), stderrLevels: ["error", "warn"] }), + new winston.transports.DailyRotateFile({ filename: "logs/error-%DATE%.log", level: "error", zippedArchive: true, maxSize: 4194304, maxFiles: 8 }), + new winston.transports.DailyRotateFile({ filename: "logs/main-%DATE%.log", zippedArchive: true, maxSize: 4194304, maxFiles: 8 }) + ], + level: process.env.DEBUG_LOG ? "debug" : "main", + format: winston.format.combine( + winston.format.timestamp({ format: "YYYY-MM-DD HH:mm:ss" }), + winston.format.printf((info) => { + const { + timestamp, level, message, ...args + } = info; + + return `[${timestamp}]: [${level.toUpperCase()}] - ${message} ${Object.keys(args).length ? JSON.stringify(args, null, 2) : ""}`; + }), + ) +}); + +winston.addColors({ + info: "green", + main: "gray", + debug: "magenta", + warn: "yellow", + error: "red" +}); + +export function log(type, content) { return content ? logger.log(type === "log" ? "main" : type, content) : logger.info(type); } + +export function error(...args) { return log("error", ...args); } + +export function warn(...args) { return log("warn", ...args); } + +export function debug(...args) { return log("debug", ...args); } diff --git a/utils/misc.js b/utils/misc.js index 6cf44f2..d2c40fb 100644 --- a/utils/misc.js +++ b/utils/misc.js @@ -1,163 +1,164 @@ -import util from "util"; -import fs from "fs"; -import pm2 from "pm2"; -import { config } from "dotenv"; -import db from "./database.js"; - -// playing messages -const { messages } = JSON.parse(fs.readFileSync(new URL("../config/messages.json", import.meta.url))); -const { types } = JSON.parse(fs.readFileSync(new URL("../config/commands.json", import.meta.url))); - -let broadcast = false; - -// random(array) to select a random entry in array -export function random(array) { - if (!array || array.length < 1) return null; - return array[Math.floor(Math.random() * array.length)]; -} - -const optionalReplace = (token) => { - return token === undefined || token === "" ? "" : (token === "true" || token === "false" ? token : ""); -}; - -// clean(text) to clean message of any private info or mentions -export function clean(text) { - if (typeof text !== "string") - text = util.inspect(text, { depth: 1 }); - - text = text - .replaceAll("`", `\`${String.fromCharCode(8203)}`) - .replaceAll("@", `@${String.fromCharCode(8203)}`); - - let { parsed } = config(); - if (!parsed) parsed = process.env; - const imageServers = JSON.parse(fs.readFileSync(new URL("../config/servers.json", import.meta.url), { encoding: "utf8" })).image; - - if (imageServers?.length !== 0) { - for (const { server, auth } of imageServers) { - text = text.replaceAll(server, optionalReplace(server)); - text = text.replaceAll(auth, optionalReplace(auth)); - } - } - - for (const env of Object.keys(parsed)) { - text = text.replaceAll(parsed[env], optionalReplace(parsed[env])); - } - - return text; -} - -// textEncode(string) to encode characters for image processing -export function textEncode(string) { - return string.replaceAll("&", "&").replaceAll(">", ">").replaceAll("<", "<").replaceAll("\"", """).replaceAll("'", "'").replaceAll("\\n", "\n").replaceAll("\\:", ":").replaceAll("\\,", ","); -} - -// set activity (a.k.a. the gamer code) -export async function activityChanger(bot) { - if (!broadcast) { - await bot.editStatus("dnd", [{ - type: 0, - name: random(messages) + (types.classic ? ` | @${bot.user.username} help` : "") - }]); - } - setTimeout(() => activityChanger(bot), 900000); -} - -export async function checkBroadcast(bot) { - if (!db) { - return; - } - const message = await db.getBroadcast(); - if (message) { - startBroadcast(bot, message); - } -} - -export function startBroadcast(bot, message) { - bot.editStatus("dnd", [{ - type: 0, - name: message + (types.classic ? ` | @${bot.user.username} help` : "") - }]); - broadcast = true; -} - -export function endBroadcast(bot) { - bot.editStatus("dnd", [{ - type: 0, - name: random(messages) + (types.classic ? ` | @${bot.user.username} help` : "") - }]); - broadcast = false; -} - -export function getServers(bot) { - return new Promise((resolve, reject) => { - if (process.env.PM2_USAGE) { - pm2.launchBus((err, pm2Bus) => { - const listener = (packet) => { - if (packet.data?.type === "countResponse") { - resolve(packet.data.serverCount); - pm2Bus.off("process:msg"); - } - }; - pm2Bus.on("process:msg", listener); - }); - pm2.list((err, list) => { - if (err) { - reject(err); - return; - } - const managerProc = list.filter((v) => v.name === "esmBot-manager")[0]; - pm2.sendDataToProcessId(managerProc.pm_id, { - id: managerProc.pm_id, - type: "process:msg", - data: { - type: "getCount" - }, - topic: true - }, (err) => { - if (err) reject(err); - }); - }); - } else { - resolve(bot.guilds.size); - } - }); -} - -// copied from eris -export function cleanMessage(message, content) { - let cleanContent = content && content.replace(//g, "$1") || ""; - - const author = message.author ?? message.member ?? message.user; - let authorName = author.username; - if (message.member?.nick) { - authorName = message.member.nick; - } - cleanContent = cleanContent.replace(new RegExp(`<@!?${author.id}>`, "g"), `@${authorName}`); - - if (message.mentions) { - for (const mention of message.mentions.members) { - if (mention.nick) { - cleanContent = cleanContent.replace(new RegExp(`<@!?${mention.id}>`, "g"), `@${mention.nick}`); - } - cleanContent = cleanContent.replace(new RegExp(`<@!?${mention.id}>`, "g"), `@${mention.username}`); - } - - if (message.guildID && message.mentions.roles) { - for (const roleID of message.mentions.roles) { - const role = message.guild.roles.get(roleID); - const roleName = role ? role.name : "deleted-role"; - cleanContent = cleanContent.replace(new RegExp(`<@&${roleID}>`, "g"), `@${roleName}`); - } - } - - for (const id of message.mentions.channels) { - const channel = message.client.getChannel(id); - if (channel && channel.name && channel.mention) { - cleanContent = cleanContent.replace(channel.mention, `#${channel.name}`); - } - } - } - - return textEncode(cleanContent); +import util from "util"; +import fs from "fs"; +import pm2 from "pm2"; +import { config } from "dotenv"; +import db from "./database.js"; + +// playing messages +const { messages } = JSON.parse(fs.readFileSync(new URL("../config/messages.json", import.meta.url))); +const { types } = JSON.parse(fs.readFileSync(new URL("../config/commands.json", import.meta.url))); + +let broadcast = false; + +// random(array) to select a random entry in array +export function random(array) { + if (!array || array.length < 1) return null; + return array[Math.floor(Math.random() * array.length)]; +} + +const optionalReplace = (token) => { + return token === undefined || token === "" ? "" : (token === "true" || token === "false" ? token : ""); +}; + +// clean(text) to clean message of any private info or mentions +export function clean(text) { + if (typeof text !== "string") + text = util.inspect(text, { depth: 1 }); + + text = text + .replaceAll("`", `\`${String.fromCharCode(8203)}`) + .replaceAll("@", `@${String.fromCharCode(8203)}`); + + let { parsed } = config(); + if (!parsed) parsed = process.env; + const imageServers = JSON.parse(fs.readFileSync(new URL("../config/servers.json", import.meta.url), { encoding: "utf8" })).image; + + if (imageServers?.length !== 0) { + for (const { server, auth } of imageServers) { + text = text.replaceAll(server, optionalReplace(server)); + text = text.replaceAll(auth, optionalReplace(auth)); + } + } + + for (const env of Object.keys(parsed)) { + text = text.replaceAll(parsed[env], optionalReplace(parsed[env])); + } + + return text; +} + +// textEncode(string) to encode characters for image processing +export function textEncode(string) { + return string.replaceAll("&", "&").replaceAll(">", ">").replaceAll("<", "<").replaceAll("\"", """).replaceAll("'", "'").replaceAll("\\n", "\n").replaceAll("\\:", ":").replaceAll("\\,", ","); +} + +// set activity (a.k.a. the gamer code) +export async function activityChanger(bot) { + if (!broadcast) { + await bot.editStatus("dnd", [{ + type: 0, + name: random(messages) + (types.classic ? ` | @${bot.user.username} help` : "") + }]); + } + setTimeout(() => activityChanger(bot), 900000); +} + +export async function checkBroadcast(bot) { + if (!db) { + return; + } + const message = await db.getBroadcast(); + if (message) { + startBroadcast(bot, message); + } +} + +export function startBroadcast(bot, message) { + bot.editStatus("dnd", [{ + type: 0, + name: message + (types.classic ? ` | @${bot.user.username} help` : "") + }]); + broadcast = true; +} + +export function endBroadcast(bot) { + bot.editStatus("dnd", [{ + type: 0, + name: random(messages) + (types.classic ? ` | @${bot.user.username} help` : "") + }]); + broadcast = false; +} + +export function getServers(bot) { + return new Promise((resolve, reject) => { + if (process.env.PM2_USAGE) { + pm2.launchBus((err, pm2Bus) => { + const listener = (packet) => { + if (packet.data?.type === "countResponse") { + resolve(packet.data.serverCount); + pm2Bus.off("process:msg"); + } + }; + pm2Bus.on("process:msg", listener); + }); + pm2.list((err, list) => { + if (err) { + reject(err); + return; + } + const managerProc = list.filter((v) => v.name === "esmBot-manager")[0]; + pm2.sendDataToProcessId(managerProc.pm_id, { + id: managerProc.pm_id, + type: "process:msg", + data: { + type: "getCount" + }, + topic: true + }, (err) => { + if (err) reject(err); + }); + }); + } else { + resolve(bot.guilds.size); + } + }); +} + +// copied from eris +export function cleanMessage(message, content) { + let cleanContent = content && content.replace(//g, "$1") || ""; + // TODO: see if I need to fuck with this + + // const author = message.author ?? message.member ?? message.user; + // let authorName = author.username; + // if (message.member?.nick) { + // authorName = message.member.nick; + // } + // cleanContent = cleanContent.replace(new RegExp(`<@!?${author.id}>`, "g"), `@${authorName}`); + + // if (message.mentions) { + // for (const mention of message.mentions.members) { + // if (mention.nick) { + // cleanContent = cleanContent.replace(new RegExp(`<@!?${mention.id}>`, "g"), `@${mention.nick}`); + // } + // cleanContent = cleanContent.replace(new RegExp(`<@!?${mention.id}>`, "g"), `@${mention.username}`); + // } + + // if (message.guildID && message.mentions.roles) { + // for (const roleID of message.mentions.roles) { + // const role = message.guild.roles.get(roleID); + // const roleName = role ? role.name : "deleted-role"; + // cleanContent = cleanContent.replace(new RegExp(`<@&${roleID}>`, "g"), `@${roleName}`); + // } + // } + + // for (const id of message.mentions.channels) { + // const channel = message.client.getChannel(id); + // if (channel && channel.name && channel.mention) { + // cleanContent = cleanContent.replace(channel.mention, `#${channel.name}`); + // } + // } + // } + + return textEncode(cleanContent); } \ No newline at end of file diff --git a/utils/pagination/awaitinteractions.js b/utils/pagination/awaitinteractions.js index b416dfb..f21aeba 100644 --- a/utils/pagination/awaitinteractions.js +++ b/utils/pagination/awaitinteractions.js @@ -1,39 +1,39 @@ -// oceanic doesn't come with a method to wait for interactions by default, so we make our own -import { EventEmitter } from "events"; - -class InteractionCollector extends EventEmitter { - constructor(client, message, type, timeout = 120000) { - super(); - this.message = message; - this.type = type; - this.ended = false; - this.bot = client; - this.timeout = timeout; - this.listener = async (interaction) => { - await this.verify(interaction); - }; - this.bot.on("interactionCreate", this.listener); - this.end = setTimeout(() => this.stop("time"), timeout); - } - - async verify(interaction) { - if (!(interaction instanceof this.type)) return false; - if (this.message.id !== interaction.message.id) return false; - this.emit("interaction", interaction); - return true; - } - - extend() { - clearTimeout(this.end); - this.end = setTimeout(() => this.stop("time"), this.timeout); - } - - stop(reason) { - if (this.ended) return; - this.ended = true; - this.bot.removeListener("interactionCreate", this.listener); - this.emit("end", this.collected, reason); - } -} - -export default InteractionCollector; +// oceanic doesn't come with a method to wait for interactions by default, so we make our own +import { EventEmitter } from "events"; + +class InteractionCollector extends EventEmitter { + constructor(client, message, type, timeout = 120000) { + super(); + this.message = message; + this.type = type; + this.ended = false; + this.bot = client; + this.timeout = timeout; + this.listener = async (interaction) => { + await this.verify(interaction); + }; + this.bot.on("interactionCreate", this.listener); + this.end = setTimeout(() => this.stop("time"), timeout); + } + + async verify(interaction) { + if (!(interaction instanceof this.type)) return false; + if (this.message.id !== interaction.message.id) return false; + this.emit("interaction", interaction); + return true; + } + + extend() { + clearTimeout(this.end); + this.end = setTimeout(() => this.stop("time"), this.timeout); + } + + stop(reason) { + if (this.ended) return; + this.ended = true; + this.bot.removeListener("interactionCreate", this.listener); + this.emit("end", this.collected, reason); + } +} + +export default InteractionCollector; diff --git a/utils/pagination/pagination.js b/utils/pagination/pagination.js index 4b2ba7f..774ae2c 100644 --- a/utils/pagination/pagination.js +++ b/utils/pagination/pagination.js @@ -1,187 +1,187 @@ -import InteractionCollector from "./awaitinteractions.js"; -import { ComponentInteraction } from "oceanic.js"; - -export default async (client, info, pages, timeout = 120000) => { - const options = info.type === "classic" ? { - messageReference: { - channelID: info.message.channelID, - messageID: info.message.id, - guildID: info.message.guildID, - failIfNotExists: false - }, - allowedMentions: { - repliedUser: false - } - } : {}; - let page = 0; - const components = { - components: [{ - type: 1, - components: [ - { - type: 2, - label: "Back", - emoji: { - id: null, - name: "◀" - }, - style: 1, - customID: "back" - }, - { - type: 2, - label: "Forward", - emoji: { - id: null, - name: "â–ļ" - }, - style: 1, - customID: "forward" - }, - { - type: 2, - label: "Jump", - emoji: { - id: null, - name: "đŸ”ĸ" - }, - style: 1, - customID: "jump" - }, - { - type: 2, - label: "Delete", - emoji: { - id: null, - name: "🗑" - }, - style: 4, - customID: "delete" - } - ] - }] - }; - let currentPage; - if (info.type === "classic") { - currentPage = await client.rest.channels.createMessage(info.message.channelID, Object.assign(pages[page], options, pages.length > 1 ? components : {})); - } else { - currentPage = await info.interaction[info.interaction.acknowledged ? "editOriginal" : "createMessage"](Object.assign(pages[page], pages.length > 1 ? components : {})); - if (!currentPage) currentPage = await info.interaction.getOriginal(); - } - - if (pages.length > 1) { - const interactionCollector = new InteractionCollector(client, currentPage, ComponentInteraction, timeout); - interactionCollector.on("interaction", async (interaction) => { - if ((interaction.member ?? interaction.user).id === info.author.id) { - switch (interaction.data.customID) { - case "back": - await interaction.deferUpdate(); - page = page > 0 ? --page : pages.length - 1; - currentPage = await currentPage.edit(Object.assign(pages[page], options)); - interactionCollector.extend(); - break; - case "forward": - await interaction.deferUpdate(); - page = page + 1 < pages.length ? ++page : 0; - currentPage = await currentPage.edit(Object.assign(pages[page], options)); - interactionCollector.extend(); - break; - case "jump": - await interaction.deferUpdate(); - var newComponents = JSON.parse(JSON.stringify(components)); - for (const index of newComponents.components[0].components.keys()) { - newComponents.components[0].components[index].disabled = true; - } - currentPage = await currentPage.edit(newComponents); - interactionCollector.extend(); - var jumpComponents = { - components: [{ - type: 1, - components: [{ - type: 3, - customID: "seekDropdown", - placeholder: "Page Number", - options: [] - }] - }] - }; - for (let i = 0; i < pages.length && i < 25; i++) { - const payload = { - label: i + 1, - value: i - }; - jumpComponents.components[0].components[0].options[i] = payload; - } - var promise; - if (info.type === "classic") { - promise = client.rest.channels.createMessage(info.message.channelID, Object.assign({ content: "What page do you want to jump to?" }, { - messageReference: { - channelID: currentPage.channelID, - messageID: currentPage.id, - guildID: currentPage.guildID, - failIfNotExists: false - }, - allowedMentions: { - repliedUser: false - } - }, jumpComponents)); - } else { - promise = info.interaction.createFollowup(Object.assign({ content: "What page do you want to jump to?" }, jumpComponents)); - } - promise.then(askMessage => { - const dropdownCollector = new InteractionCollector(client, askMessage, ComponentInteraction, timeout); - let ended = false; - dropdownCollector.on("interaction", async (response) => { - if (response.data.customID !== "seekDropdown") return; - try { - await askMessage.delete(); - } catch { - // no-op - } - page = Number(response.data.values.raw[0]); - currentPage = await currentPage.edit(Object.assign(pages[page], options, components)); - ended = true; - dropdownCollector.stop(); - }); - dropdownCollector.once("end", async () => { - if (ended) return; - try { - await askMessage.delete(); - } catch { - // no-op - } - currentPage = await currentPage.edit(Object.assign(pages[page], options, components)); - }); - }).catch(error => { - throw error; - }); - break; - case "delete": - await interaction.deferUpdate(); - interactionCollector.emit("end", true); - try { - await currentPage.delete(); - } catch { - // no-op - } - return; - default: - break; - } - } - }); - interactionCollector.once("end", async (deleted = false) => { - interactionCollector.removeAllListeners("interaction"); - if (!deleted) { - for (const index of components.components[0].components.keys()) { - components.components[0].components[index].disabled = true; - } - try { - await currentPage.edit(components); - } catch { - // no-op - } - } - }); - } -}; +import InteractionCollector from "./awaitinteractions.js"; +import { ComponentInteraction } from "oceanic.js"; + +export default async (client, info, pages, timeout = 120000) => { + const options = info.type === "classic" ? { + messageReference: { + channelID: info.message.channelID, + messageID: info.message.id, + guildID: info.message.guildID, + failIfNotExists: false + }, + allowedMentions: { + repliedUser: false + } + } : {}; + let page = 0; + const components = { + components: [{ + type: 1, + components: [ + { + type: 2, + label: "Back", + emoji: { + id: null, + name: "◀" + }, + style: 1, + customID: "back" + }, + { + type: 2, + label: "Forward", + emoji: { + id: null, + name: "â–ļ" + }, + style: 1, + customID: "forward" + }, + { + type: 2, + label: "Jump", + emoji: { + id: null, + name: "đŸ”ĸ" + }, + style: 1, + customID: "jump" + }, + { + type: 2, + label: "Delete", + emoji: { + id: null, + name: "🗑" + }, + style: 4, + customID: "delete" + } + ] + }] + }; + let currentPage; + if (info.type === "classic") { + currentPage = await client.rest.channels.createMessage(info.message.channelID, Object.assign(pages[page], options, pages.length > 1 ? components : {})); + } else { + currentPage = await info.interaction[info.interaction.acknowledged ? "editOriginal" : "createMessage"](Object.assign(pages[page], pages.length > 1 ? components : {})); + if (!currentPage) currentPage = await info.interaction.getOriginal(); + } + + if (pages.length > 1) { + const interactionCollector = new InteractionCollector(client, currentPage, ComponentInteraction, timeout); + interactionCollector.on("interaction", async (interaction) => { + if ((interaction.member ?? interaction.user).id === info.author.id) { + switch (interaction.data.customID) { + case "back": + await interaction.deferUpdate(); + page = page > 0 ? --page : pages.length - 1; + currentPage = await currentPage.edit(Object.assign(pages[page], options)); + interactionCollector.extend(); + break; + case "forward": + await interaction.deferUpdate(); + page = page + 1 < pages.length ? ++page : 0; + currentPage = await currentPage.edit(Object.assign(pages[page], options)); + interactionCollector.extend(); + break; + case "jump": + await interaction.deferUpdate(); + var newComponents = JSON.parse(JSON.stringify(components)); + for (const index of newComponents.components[0].components.keys()) { + newComponents.components[0].components[index].disabled = true; + } + currentPage = await currentPage.edit(newComponents); + interactionCollector.extend(); + var jumpComponents = { + components: [{ + type: 1, + components: [{ + type: 3, + customID: "seekDropdown", + placeholder: "Page Number", + options: [] + }] + }] + }; + for (let i = 0; i < pages.length && i < 25; i++) { + const payload = { + label: i + 1, + value: i + }; + jumpComponents.components[0].components[0].options[i] = payload; + } + var promise; + if (info.type === "classic") { + promise = client.rest.channels.createMessage(info.message.channelID, Object.assign({ content: "What page do you want to jump to?" }, { + messageReference: { + channelID: currentPage.channelID, + messageID: currentPage.id, + guildID: currentPage.guildID, + failIfNotExists: false + }, + allowedMentions: { + repliedUser: false + } + }, jumpComponents)); + } else { + promise = info.interaction.createFollowup(Object.assign({ content: "What page do you want to jump to?" }, jumpComponents)); + } + promise.then(askMessage => { + const dropdownCollector = new InteractionCollector(client, askMessage, ComponentInteraction, timeout); + let ended = false; + dropdownCollector.on("interaction", async (response) => { + if (response.data.customID !== "seekDropdown") return; + try { + await askMessage.delete(); + } catch { + // no-op + } + page = Number(response.data.values.raw[0]); + currentPage = await currentPage.edit(Object.assign(pages[page], options, components)); + ended = true; + dropdownCollector.stop(); + }); + dropdownCollector.once("end", async () => { + if (ended) return; + try { + await askMessage.delete(); + } catch { + // no-op + } + currentPage = await currentPage.edit(Object.assign(pages[page], options, components)); + }); + }).catch(error => { + throw error; + }); + break; + case "delete": + await interaction.deferUpdate(); + interactionCollector.emit("end", true); + try { + await currentPage.delete(); + } catch { + // no-op + } + return; + default: + break; + } + } + }); + interactionCollector.once("end", async (deleted = false) => { + interactionCollector.removeAllListeners("interaction"); + if (!deleted) { + for (const index of components.components[0].components.keys()) { + components.components[0].components[index].disabled = true; + } + try { + await currentPage.edit(components); + } catch { + // no-op + } + } + }); + } +}; diff --git a/utils/parseCommand.js b/utils/parseCommand.js index 5ec1d64..4d4c3bb 100644 --- a/utils/parseCommand.js +++ b/utils/parseCommand.js @@ -1,112 +1,112 @@ -export default (input) => { - if (typeof input === "string") input = input.split(/\s+/g); - const args = { _: [] }; - let curr = null; - let concated = ""; - for (let i = 0; i < input.length; i++) { - const a = input[i]; - if ((a.startsWith("--") || a.startsWith("—")) && !curr) { - if (a.includes("=")) { - const [arg, value] = (a.startsWith("--") ? a.slice(2).split("=") : a.slice(1).split("=")); - let ended = true; - if (arg !== "_") { - if (value.startsWith("\"")) { - if (value.endsWith("\"")) { - args[arg] = value.slice(1).slice(0, -1); - } else { - args[arg] = `${value.slice(1)} `; - ended = false; - } - } else if (value.endsWith("\"")) { - args[arg] += a.slice(0, -1); - } else if (value !== "") { - args[arg] = value; - } else { - args[arg] = true; - } - if (args[arg] === "true") { - args[arg] = true; - } else if (args[arg] === "false") { - args[arg] = false; - } - if (!ended) curr = arg; - } - } else { - args[a.slice(2)] = true; - } - } else if (curr) { - if (a.endsWith("\"")) { - args[curr] += a.slice(0, -1); - curr = null; - } else { - args[curr] += `${a} `; - } - } else { - if (concated !== "") { - concated += `${a} `; - } else { - args._.push(a); - } - } - } - - if (curr && args[curr] == "") { - args[curr] = true; - } - - return args; -}; - -// /* -// Format: -// [{name: "verbose", type: "bool"}, {name: "username", type: "string"}] -// */ -// export default (input, format) => { -// let results = {}; -// let text = input.split(' ').slice(1).join(' '); -// format.forEach(element => { -// if(element.pos !== undefined) return; -// switch (element.type) { -// case "bool": -// res = text.match(`--${element.name}[ |=](.*?)($| )`); -// if(res) { -// text = text.replace(res[0], ""); -// results[element.name] = (res[1].toLowerCase() == "true"); -// } else { -// res = text.match(`--${element.name}`); -// if(res) text = text.replace(res[0], ""); -// results[element.name] = (res != null); -// } -// break; -// case "string": -// res = text.match(`--${element.name}[ |=](.*?)($| )`); -// if(res) text = text.replace(res[0], ""); -// results[element.name] = (res ? res[1].replace('\\','') : null); -// break; -// case "int": -// res = text.match(`--${element.name}[ |=](.*?)($| )`); -// if(res) text = text.replace(res[0], ""); -// results[element.name] = (res ? parseInt(res[1]) : null); -// break; -// case "float": -// res = text.match(`--${element.name}[ |=](.*?)($| )`); -// if(res) text = text.replace(res[0], ""); -// results[element.name] = (res ? parseFloat(res[1]) : null); -// break; -// default: -// throw Error("unknown type"); -// break; -// } -// }); -// let s = text.split(' '); -// results._ = text; -// format.forEach(element => { -// if(element.pos === undefined) return; -// if(element.pos <= s.length) { -// results[element.name] = s[element.pos]; -// } else { -// results[element.name] = null; -// } -// }) -// return results; -// } +export default (input) => { + if (typeof input === "string") input = input.split(/\s+/g); + const args = { _: [] }; + let curr = null; + let concated = ""; + for (let i = 0; i < input.length; i++) { + const a = input[i]; + if ((a.startsWith("--") || a.startsWith("—")) && !curr) { + if (a.includes("=")) { + const [arg, value] = (a.startsWith("--") ? a.slice(2).split("=") : a.slice(1).split("=")); + let ended = true; + if (arg !== "_") { + if (value.startsWith("\"")) { + if (value.endsWith("\"")) { + args[arg] = value.slice(1).slice(0, -1); + } else { + args[arg] = `${value.slice(1)} `; + ended = false; + } + } else if (value.endsWith("\"")) { + args[arg] += a.slice(0, -1); + } else if (value !== "") { + args[arg] = value; + } else { + args[arg] = true; + } + if (args[arg] === "true") { + args[arg] = true; + } else if (args[arg] === "false") { + args[arg] = false; + } + if (!ended) curr = arg; + } + } else { + args[a.slice(2)] = true; + } + } else if (curr) { + if (a.endsWith("\"")) { + args[curr] += a.slice(0, -1); + curr = null; + } else { + args[curr] += `${a} `; + } + } else { + if (concated !== "") { + concated += `${a} `; + } else { + args._.push(a); + } + } + } + + if (curr && args[curr] == "") { + args[curr] = true; + } + + return args; +}; + +// /* +// Format: +// [{name: "verbose", type: "bool"}, {name: "username", type: "string"}] +// */ +// export default (input, format) => { +// let results = {}; +// let text = input.split(' ').slice(1).join(' '); +// format.forEach(element => { +// if(element.pos !== undefined) return; +// switch (element.type) { +// case "bool": +// res = text.match(`--${element.name}[ |=](.*?)($| )`); +// if(res) { +// text = text.replace(res[0], ""); +// results[element.name] = (res[1].toLowerCase() == "true"); +// } else { +// res = text.match(`--${element.name}`); +// if(res) text = text.replace(res[0], ""); +// results[element.name] = (res != null); +// } +// break; +// case "string": +// res = text.match(`--${element.name}[ |=](.*?)($| )`); +// if(res) text = text.replace(res[0], ""); +// results[element.name] = (res ? res[1].replace('\\','') : null); +// break; +// case "int": +// res = text.match(`--${element.name}[ |=](.*?)($| )`); +// if(res) text = text.replace(res[0], ""); +// results[element.name] = (res ? parseInt(res[1]) : null); +// break; +// case "float": +// res = text.match(`--${element.name}[ |=](.*?)($| )`); +// if(res) text = text.replace(res[0], ""); +// results[element.name] = (res ? parseFloat(res[1]) : null); +// break; +// default: +// throw Error("unknown type"); +// break; +// } +// }); +// let s = text.split(' '); +// results._ = text; +// format.forEach(element => { +// if(element.pos === undefined) return; +// if(element.pos <= s.length) { +// results[element.name] = s[element.pos]; +// } else { +// results[element.name] = null; +// } +// }) +// return results; +// } diff --git a/utils/pm2/ext.js b/utils/pm2/ext.js index fe073c7..e7d39aa 100644 --- a/utils/pm2/ext.js +++ b/utils/pm2/ext.js @@ -1,240 +1,240 @@ -import pm2 from "pm2"; -import winston from "winston"; - -// load config from .env file -import { resolve, dirname } from "path"; -import { fileURLToPath } from "url"; -import { readFileSync } from "fs"; -import { createServer } from "http"; -import { config } from "dotenv"; -config({ path: resolve(dirname(fileURLToPath(import.meta.url)), "../../.env") }); - -// oceanic client used for getting shard counts -import { Client } from "oceanic.js"; - -import database from "../database.js"; -import { cpus } from "os"; - -const logger = winston.createLogger({ - levels: { - error: 0, - warn: 1, - info: 2, - main: 3, - debug: 4 - }, - transports: [ - new winston.transports.Console({ format: winston.format.colorize({ all: true }), stderrLevels: ["error", "warn"] }) - ], - level: process.env.DEBUG_LOG ? "debug" : "main", - format: winston.format.combine( - winston.format.timestamp({ format: "YYYY-MM-DD HH:mm:ss" }), - winston.format.printf((info) => { - const { - timestamp, level, message, ...args - } = info; - - return `[${timestamp}]: [${level.toUpperCase()}] - ${message} ${Object.keys(args).length ? JSON.stringify(args, null, 2) : ""}`; - }), - ) -}); - -winston.addColors({ - info: "green", - main: "gray", - debug: "magenta", - warn: "yellow", - error: "red" -}); - -let serverCount = 0; -let shardCount = 0; -let clusterCount = 0; -let responseCount = 0; - -let timeout; - -process.on("message", (packet) => { - if (packet.data?.type === "getCount") { - process.send({ - type: "process:msg", - data: { - type: "countResponse", - serverCount - } - }); - } -}); - -function updateStats() { - serverCount = 0; - shardCount = 0; - clusterCount = 0; - responseCount = 0; - return new Promise((resolve, reject) => { - pm2.list((err, list) => { - if (err) reject(err); - const clusters = list.filter((v) => v.name.includes("esmBot-proc")); - clusterCount = clusters.length; - const listener = (packet) => { - if (packet.data?.type === "serverCounts") { - clearTimeout(timeout); - serverCount += packet.data.guilds; - shardCount += packet.data.shards; - responseCount += 1; - if (responseCount >= clusterCount) { - resolve(); - process.removeListener("message", listener); - } else { - timeout = setTimeout(() => { - reject(); - process.removeListener("message", listener); - }, 5000); - } - } - }; - timeout = setTimeout(() => { - reject(); - process.removeListener("message", listener); - }, 5000); - process.on("message", listener); - process.send({ - type: "process:msg", - data: { - type: "serverCounts" - } - }); - }); - }); -} - -if (process.env.METRICS && process.env.METRICS !== "") { - const servers = []; - if (process.env.API_TYPE === "ws") { - const imageHosts = JSON.parse(readFileSync(new URL("../../config/servers.json", import.meta.url), { encoding: "utf8" })).image; - for (let { server } of imageHosts) { - if (!server.includes(":")) { - server += ":3762"; - } - servers.push(server); - } - } - const httpServer = createServer(async (req, res) => { - if (req.method !== "GET") { - res.statusCode = 405; - return res.end("GET only"); - } - res.write(`# HELP esmbot_command_count Number of times a command has been run -# TYPE esmbot_command_count counter -# HELP esmbot_server_count Number of servers/guilds the bot is in -# TYPE esmbot_server_count gauge -# HELP esmbot_shard_count Number of shards the bot has -# TYPE esmbot_shard_count gauge -`); - if (database) { - const counts = await database.getCounts(); - for (const [i, w] of Object.entries(counts)) { - res.write(`esmbot_command_count{command="${i}"} ${w}\n`); - } - } - - res.write(`esmbot_server_count ${serverCount}\n`); - res.write(`esmbot_shard_count ${shardCount}\n`); - res.end(); - }); - httpServer.listen(process.env.METRICS, () => { - logger.log("info", `Serving metrics at ${process.env.METRICS}`); - }); -} - -setInterval(updateStats, 300000); - -setTimeout(updateStats, 10000); - -logger.info("Started esmBot management process."); - -// from eris-fleet -function calcShards(shards, procs) { - if (procs < 2) return [shards]; - - const length = shards.length; - const r = []; - let i = 0; - let size; - - if (length % procs === 0) { - size = Math.floor(length / procs); - while (i < length) { - r.push(shards.slice(i, (i += size))); - } - } else { - while (i < length) { - size = Math.ceil((length - i) / procs--); - r.push(shards.slice(i, (i += size))); - } - } - - return r; -} - -(async function init() { - logger.main("Getting gateway connection data..."); - const client = new Client({ - auth: `Bot ${process.env.TOKEN}`, - gateway: { - concurrency: "auto", - maxShards: "auto", - presence: { - status: "idle", - activities: [{ - type: 0, - name: "Starting esmBot..." - }] - }, - intents: [] - } - }); - - const connectionData = await client.rest.getBotGateway(); - const cpuAmount = cpus().length; - const procAmount = Math.min(connectionData.shards, cpuAmount); - logger.main(`Obtained data, connecting with ${connectionData.shards} shard(s) across ${procAmount} process(es)...`); - - const lastShard = connectionData.shards - 1; - const shardArray = []; - for (let i = 0; i <= lastShard; i++) { - shardArray.push(i); - } - const shardArrays = calcShards(shardArray, procAmount); - - for (let i = 0; i < shardArrays.length; i++) { - await awaitStart(i, shardArrays); - } -})(); - -function awaitStart(i, shardArrays) { - return new Promise((resolve) => { - pm2.start({ - name: `esmBot-proc${i}`, - script: "app.js", - autorestart: true, - exp_backoff_restart_delay: 1000, - wait_ready: true, - listen_timeout: 60000, - watch: false, - exec_mode: "cluster", - instances: 1, - env: { - "SHARDS": JSON.stringify(shardArrays) - } - }, (err) => { - if (err) { - logger.error(`Failed to start esmBot process ${i}: ${err}`); - process.exit(0); - } else { - logger.info(`Started esmBot process ${i}.`); - resolve(); - } - }); - }); +import pm2 from "pm2"; +import winston from "winston"; + +// load config from .env file +import { resolve, dirname } from "path"; +import { fileURLToPath } from "url"; +import { readFileSync } from "fs"; +import { createServer } from "http"; +import { config } from "dotenv"; +config({ path: resolve(dirname(fileURLToPath(import.meta.url)), "../../.env") }); + +// oceanic client used for getting shard counts +import { Client } from "oceanic.js"; + +import database from "../database.js"; +import { cpus } from "os"; + +const logger = winston.createLogger({ + levels: { + error: 0, + warn: 1, + info: 2, + main: 3, + debug: 4 + }, + transports: [ + new winston.transports.Console({ format: winston.format.colorize({ all: true }), stderrLevels: ["error", "warn"] }) + ], + level: process.env.DEBUG_LOG ? "debug" : "main", + format: winston.format.combine( + winston.format.timestamp({ format: "YYYY-MM-DD HH:mm:ss" }), + winston.format.printf((info) => { + const { + timestamp, level, message, ...args + } = info; + + return `[${timestamp}]: [${level.toUpperCase()}] - ${message} ${Object.keys(args).length ? JSON.stringify(args, null, 2) : ""}`; + }), + ) +}); + +winston.addColors({ + info: "green", + main: "gray", + debug: "magenta", + warn: "yellow", + error: "red" +}); + +let serverCount = 0; +let shardCount = 0; +let clusterCount = 0; +let responseCount = 0; + +let timeout; + +process.on("message", (packet) => { + if (packet.data?.type === "getCount") { + process.send({ + type: "process:msg", + data: { + type: "countResponse", + serverCount + } + }); + } +}); + +function updateStats() { + serverCount = 0; + shardCount = 0; + clusterCount = 0; + responseCount = 0; + return new Promise((resolve, reject) => { + pm2.list((err, list) => { + if (err) reject(err); + const clusters = list.filter((v) => v.name.includes("esmBot-proc")); + clusterCount = clusters.length; + const listener = (packet) => { + if (packet.data?.type === "serverCounts") { + clearTimeout(timeout); + serverCount += packet.data.guilds; + shardCount += packet.data.shards; + responseCount += 1; + if (responseCount >= clusterCount) { + resolve(); + process.removeListener("message", listener); + } else { + timeout = setTimeout(() => { + reject(); + process.removeListener("message", listener); + }, 5000); + } + } + }; + timeout = setTimeout(() => { + reject(); + process.removeListener("message", listener); + }, 5000); + process.on("message", listener); + process.send({ + type: "process:msg", + data: { + type: "serverCounts" + } + }); + }); + }); +} + +if (process.env.METRICS && process.env.METRICS !== "") { + const servers = []; + if (process.env.API_TYPE === "ws") { + const imageHosts = JSON.parse(readFileSync(new URL("../../config/servers.json", import.meta.url), { encoding: "utf8" })).image; + for (let { server } of imageHosts) { + if (!server.includes(":")) { + server += ":3762"; + } + servers.push(server); + } + } + const httpServer = createServer(async (req, res) => { + if (req.method !== "GET") { + res.statusCode = 405; + return res.end("GET only"); + } + res.write(`# HELP esmbot_command_count Number of times a command has been run +# TYPE esmbot_command_count counter +# HELP esmbot_server_count Number of servers/guilds the bot is in +# TYPE esmbot_server_count gauge +# HELP esmbot_shard_count Number of shards the bot has +# TYPE esmbot_shard_count gauge +`); + if (database) { + const counts = await database.getCounts(); + for (const [i, w] of Object.entries(counts)) { + res.write(`esmbot_command_count{command="${i}"} ${w}\n`); + } + } + + res.write(`esmbot_server_count ${serverCount}\n`); + res.write(`esmbot_shard_count ${shardCount}\n`); + res.end(); + }); + httpServer.listen(process.env.METRICS, () => { + logger.log("info", `Serving metrics at ${process.env.METRICS}`); + }); +} + +setInterval(updateStats, 300000); + +setTimeout(updateStats, 10000); + +logger.info("Started esmBot management process."); + +// from eris-fleet +function calcShards(shards, procs) { + if (procs < 2) return [shards]; + + const length = shards.length; + const r = []; + let i = 0; + let size; + + if (length % procs === 0) { + size = Math.floor(length / procs); + while (i < length) { + r.push(shards.slice(i, (i += size))); + } + } else { + while (i < length) { + size = Math.ceil((length - i) / procs--); + r.push(shards.slice(i, (i += size))); + } + } + + return r; +} + +(async function init() { + logger.main("Getting gateway connection data..."); + const client = new Client({ + auth: `Bot ${process.env.TOKEN}`, + gateway: { + concurrency: "auto", + maxShards: "auto", + presence: { + status: "idle", + activities: [{ + type: 0, + name: "Starting esmBot..." + }] + }, + intents: [] + } + }); + + const connectionData = await client.rest.getBotGateway(); + const cpuAmount = cpus().length; + const procAmount = Math.min(connectionData.shards, cpuAmount); + logger.main(`Obtained data, connecting with ${connectionData.shards} shard(s) across ${procAmount} process(es)...`); + + const lastShard = connectionData.shards - 1; + const shardArray = []; + for (let i = 0; i <= lastShard; i++) { + shardArray.push(i); + } + const shardArrays = calcShards(shardArray, procAmount); + + for (let i = 0; i < shardArrays.length; i++) { + await awaitStart(i, shardArrays); + } +})(); + +function awaitStart(i, shardArrays) { + return new Promise((resolve) => { + pm2.start({ + name: `esmBot-proc${i}`, + script: "app.js", + autorestart: true, + exp_backoff_restart_delay: 1000, + wait_ready: true, + listen_timeout: 60000, + watch: false, + exec_mode: "cluster", + instances: 1, + env: { + "SHARDS": JSON.stringify(shardArrays) + } + }, (err) => { + if (err) { + logger.error(`Failed to start esmBot process ${i}: ${err}`); + process.exit(0); + } else { + logger.info(`Started esmBot process ${i}.`); + resolve(); + } + }); + }); } \ No newline at end of file diff --git a/utils/soundplayer.js b/utils/soundplayer.js index 9147c34..c157241 100644 --- a/utils/soundplayer.js +++ b/utils/soundplayer.js @@ -1,270 +1,270 @@ -import * as logger from "./logger.js"; -import fs from "fs"; -import format from "format-duration"; -import { Shoukaku, Connectors } from "shoukaku"; -import { setTimeout } from "timers/promises"; - -export const players = new Map(); -export const queues = new Map(); -export const skipVotes = new Map(); - -export let manager; -export let nodes = JSON.parse(fs.readFileSync(new URL("../config/servers.json", import.meta.url), { encoding: "utf8" })).lava; -export let connected = false; - -export function connect(client) { - manager = new Shoukaku(new Connectors.OceanicJS(client), nodes, { moveOnDisconnect: true, resume: true, reconnectInterval: 500, reconnectTries: 1 }); - manager.on("error", (node, error) => { - logger.error(`An error occurred on Lavalink node ${node}: ${error}`); - }); - manager.on("debug", (node, info) => { - logger.debug(`Debug event from Lavalink node ${node}: ${info}`); - }); - manager.once("ready", () => { - logger.log(`Successfully connected to ${manager.nodes.size} Lavalink node(s).`); - connected = true; - }); -} - -export async function reload(client) { - if (!manager) connect(client); - const activeNodes = manager.nodes; - const json = await fs.promises.readFile(new URL("../config/servers.json", import.meta.url), { encoding: "utf8" }); - nodes = JSON.parse(json).lava; - const names = nodes.map((a) => a.name); - for (const name in activeNodes) { - if (!names.includes(name)) { - manager.removeNode(name); - } - } - for (const node of nodes) { - if (!activeNodes.has(node.name)) { - manager.addNode(node); - } - } - if (!manager.nodes.size) connected = false; - return manager.nodes.size; -} - -export async function play(client, sound, options, music = false) { - if (!connected) return { content: "I'm not connected to any audio servers!", flags: 64 }; - if (!manager) return { content: "The sound commands are still starting up!", flags: 64 }; - if (!options.channel.guild) return { content: "This command only works in servers!", flags: 64 }; - if (!options.member.voiceState) return { content: "You need to be in a voice channel first!", flags: 64 }; - if (!options.channel.guild.permissionsOf(client.user.id.toString()).has("CONNECT")) return { content: "I can't join this voice channel!", flags: 64 }; - const voiceChannel = options.channel.guild.channels.get(options.member.voiceState.channelID) ?? await client.rest.channels.get(options.member.voiceState.channelID); - if (!voiceChannel.permissionsOf(client.user.id.toString()).has("CONNECT")) return { content: "I don't have permission to join this voice channel!", flags: 64 }; - if (!music && manager.players.has(options.channel.guildID)) return { content: "I can't play a sound effect while other audio is playing!", flags: 64 }; - const node = manager.getNode(); - if (!music && !nodes.filter(obj => obj.name === node.name)[0].local) { - sound = sound.replace(/\.\//, "https://raw.githubusercontent.com/esmBot/esmBot/master/"); - } - let response; - try { - response = await node.rest.resolve(sound); - if (!response) return { content: "🔊 I couldn't get a response from the audio server.", flags: 64 }; - if (response.loadType === "NO_MATCHES" || response.loadType === "LOAD_FAILED") return { content: "I couldn't find that song!", flags: 64 }; - } catch (e) { - logger.error(e); - return { content: "🔊 Hmmm, seems that all of the audio servers are down. Try again in a bit.", flags: 64 }; - } - const oldQueue = queues.get(voiceChannel.guildID); - if (!response.tracks || response.tracks.length === 0) return { content: "I couldn't find that song!", flags: 64 }; - if (process.env.YT_DISABLED === "true" && response.tracks[0].info.sourceName === "youtube") return "YouTube playback is disabled on this instance."; - if (music) { - const sortedTracks = response.tracks.map((val) => { return val.track; }); - const playlistTracks = response.playlistInfo.selectedTrack ? sortedTracks : [sortedTracks[0]]; - queues.set(voiceChannel.guildID, oldQueue ? [...oldQueue, ...playlistTracks] : playlistTracks); - } - const playerMeta = players.get(options.channel.guildID); - let player; - if (node.players.has(voiceChannel.guildID)) { - player = node.players.get(voiceChannel.guildID); - } else if (playerMeta?.player) { - const storedState = playerMeta?.player?.connection.state; - if (storedState && storedState === 1) { - player = playerMeta?.player; - } - } - const connection = player ?? await node.joinChannel({ - guildId: voiceChannel.guildID, - channelId: voiceChannel.id, - shardId: voiceChannel.guild.shard.id, - deaf: true - }); - - if (oldQueue?.length && music) { - return `Your ${response.playlistInfo.name ? "playlist" : "tune"} \`${response.playlistInfo.name ? response.playlistInfo.name.trim() : (response.tracks[0].info.title !== "" ? response.tracks[0].info.title.trim() : "(blank)")}\` has been added to the queue!`; - } else { - nextSong(client, options, connection, response.tracks[0].track, response.tracks[0].info, music, voiceChannel, playerMeta?.host ?? options.member.id, playerMeta?.loop ?? false, playerMeta?.shuffle ?? false); - return; - } -} - -export async function nextSong(client, options, connection, track, info, music, voiceChannel, host, loop = false, shuffle = false, lastTrack = null) { - skipVotes.delete(voiceChannel.guildID); - const parts = Math.floor((0 / info.length) * 10); - let playingMessage; - if (music && lastTrack === track && players.has(voiceChannel.guildID)) { - playingMessage = players.get(voiceChannel.guildID).playMessage; - } else { - try { - const content = !music ? { content: "🔊 Playing sound..." } : { - embeds: [{ - color: 16711680, - author: { - name: "Now Playing", - iconURL: client.user.avatarURL() - }, - fields: [{ - name: "ℹī¸ Title", - value: info.title?.trim() !== "" ? info.title : "(blank)" - }, - { - name: "🎤 Artist", - value: info.author?.trim() !== "" ? info.author : "(blank)" - }, - { - name: "đŸ’Ŧ Channel", - value: voiceChannel.name - }, - { - name: "🌐 Node", - value: connection.node?.name ?? "Unknown" - }, - { - name: `${"â–Ŧ".repeat(parts)}🔘${"â–Ŧ".repeat(10 - parts)}`, - value: `0:00/${info.isStream ? "∞" : format(info.length)}` - }] - }] - }; - if (options.type === "classic") { - playingMessage = await client.rest.channels.createMessage(options.channel.id, content); - } else { - if ((Date.now() - options.interaction.createdAt) >= 900000) { // discord interactions are only valid for 15 minutes - playingMessage = await client.rest.channels.createMessage(options.channel.id, content); - } else if (lastTrack && lastTrack !== track) { - playingMessage = await options.interaction.createFollowup(content); - } else { - playingMessage = await options.interaction[options.interaction.acknowledged ? "editOriginal" : "createMessage"](content); - if (!playingMessage) playingMessage = await options.interaction.getOriginal(); - } - } - } catch { - // no-op - } - } - connection.removeAllListeners("exception"); - connection.removeAllListeners("stuck"); - connection.removeAllListeners("end"); - connection.setVolume(0.70); - connection.playTrack({ track }); - players.set(voiceChannel.guildID, { player: connection, type: music ? "music" : "sound", host, voiceChannel, originalChannel: options.channel, loop, shuffle, playMessage: playingMessage }); - connection.once("exception", (exception) => errHandle(exception, client, connection, playingMessage, voiceChannel, options)); - connection.on("stuck", () => { - const nodeName = manager.getNode().name; - connection.move(nodeName); - connection.resume(); - }); - connection.on("end", async (data) => { - if (data.reason === "REPLACED") return; - let queue = queues.get(voiceChannel.guildID); - const player = players.get(voiceChannel.guildID); - if (player && process.env.STAYVC === "true") { - player.type = "idle"; - players.set(voiceChannel.guildID, player); - } - let newQueue; - if (player?.shuffle) { - if (player.loop) { - queue.push(queue.shift()); - } else { - queue = queue.slice(1); - } - queue.unshift(queue.splice(Math.floor(Math.random() * queue.length), 1)[0]); - newQueue = queue; - } else if (player?.loop) { - queue.push(queue.shift()); - newQueue = queue; - } else { - newQueue = queue ? queue.slice(1) : []; - } - queues.set(voiceChannel.guildID, newQueue); - if (newQueue.length !== 0) { - const newTrack = await connection.node.rest.decode(newQueue[0]); - nextSong(client, options, connection, newQueue[0], newTrack, music, voiceChannel, host, player.loop, player.shuffle, track); - try { - if (options.type === "classic") { - if (newQueue[0] !== track && playingMessage.channel.messages.has(playingMessage.id)) await playingMessage.delete(); - if (newQueue[0] !== track && player.playMessage.channel.messages.has(player.playMessage.id)) await player.playMessage.delete(); - } - } catch { - // no-op - } - } else if (process.env.STAYVC !== "true") { - await setTimeout(400); - connection.node.leaveChannel(voiceChannel.guildID); - players.delete(voiceChannel.guildID); - queues.delete(voiceChannel.guildID); - skipVotes.delete(voiceChannel.guildID); - try { - const content = `🔊 The voice channel session in \`${voiceChannel.name}\` has ended.`; - if (options.type === "classic") { - await client.rest.channels.createMessage(options.channel.id, { content }); - } else { - if ((Date.now() - options.interaction.createdAt) >= 900000) { - await client.rest.channels.createMessage(options.channel.id, { content }); - } else { - await options.interaction.createFollowup({ content }); - } - } - } catch { - // no-op - } - } - if (options.type === "classic") { - try { - if (playingMessage.channel.messages.has(playingMessage.id)) await playingMessage.delete(); - if (player?.playMessage.channel.messages.has(player.playMessage.id)) await player.playMessage.delete(); - } catch { - // no-op - } - } - }); -} - -export async function errHandle(exception, client, connection, playingMessage, voiceChannel, options, closed) { - try { - if (playingMessage.channel.messages.has(playingMessage.id)) await playingMessage.delete(); - const playMessage = players.get(voiceChannel.guildID).playMessage; - if (playMessage.channel.messages.has(playMessage.id)) await playMessage.delete(); - } catch { - // no-op - } - players.delete(voiceChannel.guildID); - queues.delete(voiceChannel.guildID); - skipVotes.delete(voiceChannel.guildID); - logger.error(exception); - try { - connection.node.leaveChannel(voiceChannel.guildID); - } catch { - // no-op - } - connection.removeAllListeners("exception"); - connection.removeAllListeners("stuck"); - connection.removeAllListeners("end"); - try { - const content = closed ? `🔊 I got disconnected by Discord and tried to reconnect; however, I got this error instead:\n\`\`\`${exception}\`\`\`` : `🔊 Looks like there was an error regarding sound playback:\n\`\`\`${exception.type}: ${exception.error}\`\`\``; - if (options.type === "classic") { - await client.rest.channels.createMessage(playingMessage.channel.id, { content }); - } else { - if ((Date.now() - options.interaction.createdAt) >= 900000) { - await client.rest.channels.createMessage(options.channel.id, { content }); - } else { - await options.interaction.createFollowup({ content }); - } - } - } catch { - // no-op - } +import * as logger from "./logger.js"; +import fs from "fs"; +import format from "format-duration"; +import { Shoukaku, Connectors } from "shoukaku"; +import { setTimeout } from "timers/promises"; + +export const players = new Map(); +export const queues = new Map(); +export const skipVotes = new Map(); + +export let manager; +export let nodes = JSON.parse(fs.readFileSync(new URL("../config/servers.json", import.meta.url), { encoding: "utf8" })).lava; +export let connected = false; + +export function connect(client) { + manager = new Shoukaku(new Connectors.OceanicJS(client), nodes, { moveOnDisconnect: true, resume: true, reconnectInterval: 500, reconnectTries: 1 }); + manager.on("error", (node, error) => { + logger.error(`An error occurred on Lavalink node ${node}: ${error}`); + }); + manager.on("debug", (node, info) => { + logger.debug(`Debug event from Lavalink node ${node}: ${info}`); + }); + manager.once("ready", () => { + logger.log(`Successfully connected to ${manager.nodes.size} Lavalink node(s).`); + connected = true; + }); +} + +export async function reload(client) { + if (!manager) connect(client); + const activeNodes = manager.nodes; + const json = await fs.promises.readFile(new URL("../config/servers.json", import.meta.url), { encoding: "utf8" }); + nodes = JSON.parse(json).lava; + const names = nodes.map((a) => a.name); + for (const name in activeNodes) { + if (!names.includes(name)) { + manager.removeNode(name); + } + } + for (const node of nodes) { + if (!activeNodes.has(node.name)) { + manager.addNode(node); + } + } + if (!manager.nodes.size) connected = false; + return manager.nodes.size; +} + +export async function play(client, sound, options, music = false) { + if (!connected) return { content: "I'm not connected to any audio servers!", flags: 64 }; + if (!manager) return { content: "The sound commands are still starting up!", flags: 64 }; + if (!options.channel.guild) return { content: "This command only works in servers!", flags: 64 }; + if (!options.member.voiceState) return { content: "You need to be in a voice channel first!", flags: 64 }; + if (!options.channel.guild.permissionsOf(client.user.id.toString()).has("CONNECT")) return { content: "I can't join this voice channel!", flags: 64 }; + const voiceChannel = options.channel.guild.channels.get(options.member.voiceState.channelID) ?? await client.rest.channels.get(options.member.voiceState.channelID); + if (!voiceChannel.permissionsOf(client.user.id.toString()).has("CONNECT")) return { content: "I don't have permission to join this voice channel!", flags: 64 }; + if (!music && manager.players.has(options.channel.guildID)) return { content: "I can't play a sound effect while other audio is playing!", flags: 64 }; + const node = manager.getNode(); + if (!music && !nodes.filter(obj => obj.name === node.name)[0].local) { + sound = sound.replace(/\.\//, "https://raw.githubusercontent.com/esmBot/esmBot/master/"); + } + let response; + try { + response = await node.rest.resolve(sound); + if (!response) return { content: "🔊 I couldn't get a response from the audio server.", flags: 64 }; + if (response.loadType === "NO_MATCHES" || response.loadType === "LOAD_FAILED") return { content: "I couldn't find that song!", flags: 64 }; + } catch (e) { + logger.error(e); + return { content: "🔊 Hmmm, seems that all of the audio servers are down. Try again in a bit.", flags: 64 }; + } + const oldQueue = queues.get(voiceChannel.guildID); + if (!response.tracks || response.tracks.length === 0) return { content: "I couldn't find that song!", flags: 64 }; + if (process.env.YT_DISABLED === "true" && response.tracks[0].info.sourceName === "youtube") return "YouTube playback is disabled on this instance."; + if (music) { + const sortedTracks = response.tracks.map((val) => { return val.track; }); + const playlistTracks = response.playlistInfo.selectedTrack ? sortedTracks : [sortedTracks[0]]; + queues.set(voiceChannel.guildID, oldQueue ? [...oldQueue, ...playlistTracks] : playlistTracks); + } + const playerMeta = players.get(options.channel.guildID); + let player; + if (node.players.has(voiceChannel.guildID)) { + player = node.players.get(voiceChannel.guildID); + } else if (playerMeta?.player) { + const storedState = playerMeta?.player?.connection.state; + if (storedState && storedState === 1) { + player = playerMeta?.player; + } + } + const connection = player ?? await node.joinChannel({ + guildId: voiceChannel.guildID, + channelId: voiceChannel.id, + shardId: voiceChannel.guild.shard.id, + deaf: true + }); + + if (oldQueue?.length && music) { + return `Your ${response.playlistInfo.name ? "playlist" : "tune"} \`${response.playlistInfo.name ? response.playlistInfo.name.trim() : (response.tracks[0].info.title !== "" ? response.tracks[0].info.title.trim() : "(blank)")}\` has been added to the queue!`; + } else { + nextSong(client, options, connection, response.tracks[0].track, response.tracks[0].info, music, voiceChannel, playerMeta?.host ?? options.member.id, playerMeta?.loop ?? false, playerMeta?.shuffle ?? false); + return; + } +} + +export async function nextSong(client, options, connection, track, info, music, voiceChannel, host, loop = false, shuffle = false, lastTrack = null) { + skipVotes.delete(voiceChannel.guildID); + const parts = Math.floor((0 / info.length) * 10); + let playingMessage; + if (music && lastTrack === track && players.has(voiceChannel.guildID)) { + playingMessage = players.get(voiceChannel.guildID).playMessage; + } else { + try { + const content = !music ? { content: "🔊 Playing sound..." } : { + embeds: [{ + color: 16711680, + author: { + name: "Now Playing", + iconURL: client.user.avatarURL() + }, + fields: [{ + name: "ℹī¸ Title", + value: info.title?.trim() !== "" ? info.title : "(blank)" + }, + { + name: "🎤 Artist", + value: info.author?.trim() !== "" ? info.author : "(blank)" + }, + { + name: "đŸ’Ŧ Channel", + value: voiceChannel.name + }, + { + name: "🌐 Node", + value: connection.node?.name ?? "Unknown" + }, + { + name: `${"â–Ŧ".repeat(parts)}🔘${"â–Ŧ".repeat(10 - parts)}`, + value: `0:00/${info.isStream ? "∞" : format(info.length)}` + }] + }] + }; + if (options.type === "classic") { + playingMessage = await client.rest.channels.createMessage(options.channel.id, content); + } else { + if ((Date.now() - options.interaction.createdAt) >= 900000) { // discord interactions are only valid for 15 minutes + playingMessage = await client.rest.channels.createMessage(options.channel.id, content); + } else if (lastTrack && lastTrack !== track) { + playingMessage = await options.interaction.createFollowup(content); + } else { + playingMessage = await options.interaction[options.interaction.acknowledged ? "editOriginal" : "createMessage"](content); + if (!playingMessage) playingMessage = await options.interaction.getOriginal(); + } + } + } catch { + // no-op + } + } + connection.removeAllListeners("exception"); + connection.removeAllListeners("stuck"); + connection.removeAllListeners("end"); + connection.setVolume(0.70); + connection.playTrack({ track }); + players.set(voiceChannel.guildID, { player: connection, type: music ? "music" : "sound", host, voiceChannel, originalChannel: options.channel, loop, shuffle, playMessage: playingMessage }); + connection.once("exception", (exception) => errHandle(exception, client, connection, playingMessage, voiceChannel, options)); + connection.on("stuck", () => { + const nodeName = manager.getNode().name; + connection.move(nodeName); + connection.resume(); + }); + connection.on("end", async (data) => { + if (data.reason === "REPLACED") return; + let queue = queues.get(voiceChannel.guildID); + const player = players.get(voiceChannel.guildID); + if (player && process.env.STAYVC === "true") { + player.type = "idle"; + players.set(voiceChannel.guildID, player); + } + let newQueue; + if (player?.shuffle) { + if (player.loop) { + queue.push(queue.shift()); + } else { + queue = queue.slice(1); + } + queue.unshift(queue.splice(Math.floor(Math.random() * queue.length), 1)[0]); + newQueue = queue; + } else if (player?.loop) { + queue.push(queue.shift()); + newQueue = queue; + } else { + newQueue = queue ? queue.slice(1) : []; + } + queues.set(voiceChannel.guildID, newQueue); + if (newQueue.length !== 0) { + const newTrack = await connection.node.rest.decode(newQueue[0]); + nextSong(client, options, connection, newQueue[0], newTrack, music, voiceChannel, host, player.loop, player.shuffle, track); + try { + if (options.type === "classic") { + if (newQueue[0] !== track && playingMessage.channel.messages.has(playingMessage.id)) await playingMessage.delete(); + if (newQueue[0] !== track && player.playMessage.channel.messages.has(player.playMessage.id)) await player.playMessage.delete(); + } + } catch { + // no-op + } + } else if (process.env.STAYVC !== "true") { + await setTimeout(400); + connection.node.leaveChannel(voiceChannel.guildID); + players.delete(voiceChannel.guildID); + queues.delete(voiceChannel.guildID); + skipVotes.delete(voiceChannel.guildID); + try { + const content = `🔊 The voice channel session in \`${voiceChannel.name}\` has ended.`; + if (options.type === "classic") { + await client.rest.channels.createMessage(options.channel.id, { content }); + } else { + if ((Date.now() - options.interaction.createdAt) >= 900000) { + await client.rest.channels.createMessage(options.channel.id, { content }); + } else { + await options.interaction.createFollowup({ content }); + } + } + } catch { + // no-op + } + } + if (options.type === "classic") { + try { + if (playingMessage.channel.messages.has(playingMessage.id)) await playingMessage.delete(); + if (player?.playMessage.channel.messages.has(player.playMessage.id)) await player.playMessage.delete(); + } catch { + // no-op + } + } + }); +} + +export async function errHandle(exception, client, connection, playingMessage, voiceChannel, options, closed) { + try { + if (playingMessage.channel.messages.has(playingMessage.id)) await playingMessage.delete(); + const playMessage = players.get(voiceChannel.guildID).playMessage; + if (playMessage.channel.messages.has(playMessage.id)) await playMessage.delete(); + } catch { + // no-op + } + players.delete(voiceChannel.guildID); + queues.delete(voiceChannel.guildID); + skipVotes.delete(voiceChannel.guildID); + logger.error(exception); + try { + connection.node.leaveChannel(voiceChannel.guildID); + } catch { + // no-op + } + connection.removeAllListeners("exception"); + connection.removeAllListeners("stuck"); + connection.removeAllListeners("end"); + try { + const content = closed ? `🔊 I got disconnected by Discord and tried to reconnect; however, I got this error instead:\n\`\`\`${exception}\`\`\`` : `🔊 Looks like there was an error regarding sound playback:\n\`\`\`${exception.type}: ${exception.error}\`\`\``; + if (options.type === "classic") { + await client.rest.channels.createMessage(playingMessage.channel.id, { content }); + } else { + if ((Date.now() - options.interaction.createdAt) >= 900000) { + await client.rest.channels.createMessage(options.channel.id, { content }); + } else { + await options.interaction.createFollowup({ content }); + } + } + } catch { + // no-op + } } \ No newline at end of file diff --git a/utils/tempimages.js b/utils/tempimages.js index 0753ff8..2b03edc 100644 --- a/utils/tempimages.js +++ b/utils/tempimages.js @@ -1,98 +1,98 @@ -import * as logger from "../utils/logger.js"; -import { readdir, lstat, rm, writeFile, stat } from "fs/promises"; - -let dirSizeCache; - -export async function upload(client, result, context, interaction = false) { - const filename = `${Math.random().toString(36).substring(2, 15)}.${result.name.split(".")[1]}`; - await writeFile(`${process.env.TEMPDIR}/${filename}`, result.contents); - const imageURL = `${process.env.TMP_DOMAIN || "https://tmp.esmbot.net"}/${filename}`; - const payload = { - embeds: [{ - color: 16711680, - title: "Here's your image!", - url: imageURL, - image: { - url: imageURL - }, - footer: { - text: "The result image was more than 8MB in size, so it was uploaded to an external site instead." - }, - }] - }; - if (interaction) { - await context[context.acknowledged ? "editOriginal" : "createMessage"](payload); - } else { - await client.rest.channels.createMessage(context.channelID, Object.assign(payload, { - messageReference: { - channelID: context.channelID, - messageID: context.id, - guildID: context.guildID ?? undefined, - failIfNotExists: false - }, - allowedMentions: { - repliedUser: false - } - })); - } - if (process.env.THRESHOLD) { - const size = dirSizeCache + result.contents.length; - dirSizeCache = size; - await removeOldImages(size); - } -} - -async function removeOldImages(size) { - if (size > process.env.THRESHOLD) { - const files = (await readdir(process.env.TEMPDIR)).map((file) => { - return lstat(`${process.env.TEMPDIR}/${file}`).then((stats) => { - if (stats.isSymbolicLink()) return; - return { - name: file, - size: stats.size, - ctime: stats.ctime - }; - }); - }); - - const resolvedFiles = await Promise.all(files); - const oldestFiles = resolvedFiles.filter(Boolean).sort((a, b) => a.ctime - b.ctime); - - do { - if (!oldestFiles[0]) break; - await rm(`${process.env.TEMPDIR}/${oldestFiles[0].name}`); - logger.log(`Removed oldest image file: ${oldestFiles[0].name}`); - size -= oldestFiles[0].size; - oldestFiles.shift(); - } while (size > process.env.THRESHOLD); - - const newSize = oldestFiles.reduce((a, b) => { - return a + b.size; - }, 0); - dirSizeCache = newSize; - } -} - -export async function parseThreshold() { - const matched = process.env.THRESHOLD.match(/(\d+)([KMGT])/); - const sizes = { - K: 1024, - M: 1048576, - G: 1073741824, - T: 1099511627776 - }; - if (matched && matched[1] && matched[2]) { - process.env.THRESHOLD = matched[1] * sizes[matched[2]]; - } else { - logger.error("Invalid THRESHOLD config."); - process.env.THRESHOLD = undefined; - } - const dirstat = (await readdir(process.env.TEMPDIR)).map((file) => { - return stat(`${process.env.TEMPDIR}/${file}`).then((stats) => stats.size); - }); - const size = await Promise.all(dirstat); - const reduced = size.reduce((a, b) => { - return a + b; - }, 0); - dirSizeCache = reduced; +import * as logger from "../utils/logger.js"; +import { readdir, lstat, rm, writeFile, stat } from "fs/promises"; + +let dirSizeCache; + +export async function upload(client, result, context, interaction = false) { + const filename = `${Math.random().toString(36).substring(2, 15)}.${result.name.split(".")[1]}`; + await writeFile(`${process.env.TEMPDIR}/${filename}`, result.contents); + const imageURL = `${process.env.TMP_DOMAIN || "https://tmp.esmbot.net"}/${filename}`; + const payload = { + embeds: [{ + color: 16711680, + title: "Here's your image!", + url: imageURL, + image: { + url: imageURL + }, + footer: { + text: "The result image was more than 8MB in size, so it was uploaded to an external site instead." + }, + }] + }; + if (interaction) { + await context[context.acknowledged ? "editOriginal" : "createMessage"](payload); + } else { + await client.rest.channels.createMessage(context.channelID, Object.assign(payload, { + messageReference: { + channelID: context.channelID, + messageID: context.id, + guildID: context.guildID ?? undefined, + failIfNotExists: false + }, + allowedMentions: { + repliedUser: false + } + })); + } + if (process.env.THRESHOLD) { + const size = dirSizeCache + result.contents.length; + dirSizeCache = size; + await removeOldImages(size); + } +} + +async function removeOldImages(size) { + if (size > process.env.THRESHOLD) { + const files = (await readdir(process.env.TEMPDIR)).map((file) => { + return lstat(`${process.env.TEMPDIR}/${file}`).then((stats) => { + if (stats.isSymbolicLink()) return; + return { + name: file, + size: stats.size, + ctime: stats.ctime + }; + }); + }); + + const resolvedFiles = await Promise.all(files); + const oldestFiles = resolvedFiles.filter(Boolean).sort((a, b) => a.ctime - b.ctime); + + do { + if (!oldestFiles[0]) break; + await rm(`${process.env.TEMPDIR}/${oldestFiles[0].name}`); + logger.log(`Removed oldest image file: ${oldestFiles[0].name}`); + size -= oldestFiles[0].size; + oldestFiles.shift(); + } while (size > process.env.THRESHOLD); + + const newSize = oldestFiles.reduce((a, b) => { + return a + b.size; + }, 0); + dirSizeCache = newSize; + } +} + +export async function parseThreshold() { + const matched = process.env.THRESHOLD.match(/(\d+)([KMGT])/); + const sizes = { + K: 1024, + M: 1048576, + G: 1073741824, + T: 1099511627776 + }; + if (matched && matched[1] && matched[2]) { + process.env.THRESHOLD = matched[1] * sizes[matched[2]]; + } else { + logger.error("Invalid THRESHOLD config."); + process.env.THRESHOLD = undefined; + } + const dirstat = (await readdir(process.env.TEMPDIR)).map((file) => { + return stat(`${process.env.TEMPDIR}/${file}`).then((stats) => stats.size); + }); + const size = await Promise.all(dirstat); + const reduced = size.reduce((a, b) => { + return a + b; + }, 0); + dirSizeCache = reduced; } \ No newline at end of file diff --git a/utils/urlcheck.js b/utils/urlcheck.js index 10aef72..13c3359 100644 --- a/utils/urlcheck.js +++ b/utils/urlcheck.js @@ -1,16 +1,16 @@ -export default (string) => { - const protocolAndDomainRE = /^(?:\w+:)?\/\/(\S+)$/; - const domainRE = /^[^\s.]+\.\S{2,}$/; - const match = string.match(protocolAndDomainRE); - if (!match) { - return false; - } - const everythingAfterProtocol = match[1]; - if (!everythingAfterProtocol) { - return false; - } - if (domainRE.test(everythingAfterProtocol)) { - return true; - } - return false; -}; +export default (string) => { + const protocolAndDomainRE = /^(?:\w+:)?\/\/(\S+)$/; + const domainRE = /^[^\s.]+\.\S{2,}$/; + const match = string.match(protocolAndDomainRE); + if (!match) { + return false; + } + const everythingAfterProtocol = match[1]; + if (!everythingAfterProtocol) { + return false; + } + if (domainRE.test(everythingAfterProtocol)) { + return true; + } + return false; +}; diff --git a/utils/wrap.js b/utils/wrap.js index a0f4485..ea95f71 100644 --- a/utils/wrap.js +++ b/utils/wrap.js @@ -1,12 +1,12 @@ -export default (str) => { - var regexString = ".{1,15}([\\s\u200B]+|$)|[^\\s\u200B]+?([\\s\u200B]+|$)"; - var re = new RegExp(regexString, "g"); - var lines = str.match(re) || []; - var result = lines.map((line) => { - if (line.slice(-1) === "\n") { - line = line.slice(0, line.length - 1); - } - return line; - }).join("\n"); - return result; +export default (str) => { + var regexString = ".{1,15}([\\s\u200B]+|$)|[^\\s\u200B]+?([\\s\u200B]+|$)"; + var re = new RegExp(regexString, "g"); + var lines = str.match(re) || []; + var result = lines.map((line) => { + if (line.slice(-1) === "\n") { + line = line.slice(0, line.length - 1); + } + return line; + }).join("\n"); + return result; }; \ No newline at end of file