diff --git a/.dockerignore b/.dockerignore index 9f5e8187..b864b7fb 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,16 +1,166 @@ -node_modules/ -.github/ -.vscode/ -data/ -dist/ -.husky/ - -.travis.yml -**.md -!README.md -docker-compose.yml -renovate.json -*.lock +# User-specific stuff +.idea/ + +# CMake +cmake-build-*/ + +# File-based project format +*.iws + +# IntelliJ +out/ + +# mpeltonen/sbt-idea plugin +.idea_modules/ + +# JIRA plugin +atlassian-ide-plugin.xm + +# Crashlytics plugin (for Android Studio and IntelliJ) +com_crashlytics_export_strings.xml +crashlytics.properties +crashlytics-build.properties +fabric.properties + +### Intellij Patch ### +# Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721 + +# *.iml +# modules.xml +# .idea/misc.xml +# *.ipr + +# Sonarlint plugin +# https://plugins.jetbrains.com/plugin/7973-sonarlint +.idea/**/sonarlint/ + +# SonarQube Plugin +# https://plugins.jetbrains.com/plugin/7238-sonarqube-community-plugin +.idea/**/sonarIssues.xml + +# Markdown Navigator plugin +# https://plugins.jetbrains.com/plugin/7896-markdown-navigator-enhanced +.idea/**/markdown-navigator.xml +.idea/**/markdown-navigator-enh.xml +.idea/**/markdown-navigator/ + +# Cache file creation bug +# See https://youtrack.jetbrains.com/issue/JBR-2257 +.idea/$CACHE_FILE$ + +# CodeStream plugin +# https://plugins.jetbrains.com/plugin/12206-codestream +.idea/codestream.xml + +### JetBrains+all ### +# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider +# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 + +# User-specific stuff + +# Generated files + +# Sensitive or high-churn files + +# Gradle + +# Gradle and Maven with auto-import +# When using Gradle or Maven with auto-import, you should exclude module files, +# since they will be recreated, and may cause churn. Uncomment if using +# auto-import. +# .idea/artifacts +# .idea/compiler.xml +# .idea/jarRepositories.xml +# .idea/modules.xml +# .idea/*.iml +# .idea/modules +# *.iml +# *.ipr + +# CMake + +# Mongo Explorer plugin + +# File-based project format + +# IntelliJ + +# mpeltonen/sbt-idea plugin + +# JIRA plugin + +# Cursive Clojure plugin + +# Crashlytics plugin (for Android Studio and IntelliJ) + +# Editor-based Rest Client + +# Android studio 3.1+ serialized cache file + +### JetBrains+all Patch ### +# Ignores the whole .idea folder and all .iml files +# See https://github.com/joeblau/gitignore.io/issues/186 and https://github.com/joeblau/gitignore.io/issues/360 + +.idea/ + +# Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-249601023 + +*.iml +modules.xml +.idea/misc.xml +*.ipr + +# Sonarlint plugin +.idea/sonarlint + +### Kotlin ### +# Compiled class file +*.class + +# Log file *.log -application.yml + +# BlueJ files +*.ctxt + +# Mobile Tools for Java (J2ME) +.mtj.tmp/ + +# Package Files # +*.jar +*.war +*.nar +*.ear +*.zip +*.tar.gz +*.rar + +# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml +hs_err_pid* + +### Gradle ### +.gradle +build/ + +# Ignore Gradle GUI config +gradle-app.setting + +# Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored) +!gradle-wrapper.jar + +# Cache of project +.gradletasknamecache + +# # Work around https://youtrack.jetbrains.com/issue/IDEA-116898 +# gradle/wrapper/gradle-wrapper.properties + +### Gradle Patch ### +**/build/ + +# other stuff config.yml +.github +LICENSE +README.md +renovate.json +docker-compose.yml diff --git a/.eslintignore b/.eslintignore deleted file mode 100644 index b38db2f2..00000000 --- a/.eslintignore +++ /dev/null @@ -1,2 +0,0 @@ -node_modules/ -build/ diff --git a/.eslintrc.json b/.eslintrc.json deleted file mode 100644 index fe1fda3e..00000000 --- a/.eslintrc.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "extends": ["prettier", "@augu/eslint-config/ts.js"], - "plugins": ["prettier"], - "rules": { - "@typescript-eslint/indent": "off", - "quotes": ["error", "single", { "avoidEscape": true, "allowTemplateLiterals": true }] - } -} diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 00000000..64023115 --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1 @@ +* @auguwu @IceeMC diff --git a/.github/workflows/ESLint.yml b/.github/workflows/ESLint.yml deleted file mode 100644 index b7114582..00000000 --- a/.github/workflows/ESLint.yml +++ /dev/null @@ -1,66 +0,0 @@ -name: ESLint Workflow -on: - push: - branches: - - 'feature/**' - - 'issue/gh-**' - - master - - edge - - paths-ignore: - - '.github/**' - - '.husky/**' - - '.vscode/**' - - 'assets/**' - - 'locales/**' - - 'docker/**' - - '.dockerignore' - - '.eslintignore' - - '.gitignore' - - '**.md' - - 'LICENSE' - - 'renovate.json' - - pull_request: - branches: - - 'feature/**' - - 'issue/gh-**' - - master - - edge - - paths-ignore: - - '.github/**' - - '.husky/**' - - '.vscode/**' - - 'assets/**' - - 'locales/**' - - 'docker/**' - - '.dockerignore' - - '.eslintignore' - - '.gitignore' - - '**.md' - - 'LICENSE' - - 'renovate.json' -jobs: - lint: - runs-on: ubuntu-latest - steps: - - name: 'Checkout repository' - uses: actions/checkout@v2 - - - name: Use Node.js v16 - uses: actions/setup-node@v2 - with: - node-version: 16.x - - - name: Installs all global packages - run: npm install -g eslint typescript - - - name: Installs local packages - run: yarn - - - name: Lints the repository and checks for linting errors - run: eslint src --ext .ts - - - name: Compiles the project to check for any build errors - run: tsc --noEmit diff --git a/.github/workflows/prod.yml b/.github/workflows/Production.yml similarity index 100% rename from .github/workflows/prod.yml rename to .github/workflows/Production.yml diff --git a/.github/workflows/Sentry.yml b/.github/workflows/Sentry.yml new file mode 100644 index 00000000..3157e40f --- /dev/null +++ b/.github/workflows/Sentry.yml @@ -0,0 +1,21 @@ +name: Update Sentry release on sentry.floof.gay +on: + release: + types: + - created +jobs: + sentry-release: + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v2 + + - name: Create Sentry release + uses: getsentry/action-release@v1 + env: + SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} + SENTRY_ORG: noelware + SENTRY_PROJECT: nino + SENTRY_URL: https://sentry.floof.gay + with: + environment: production diff --git a/.github/workflows/Shortlinks.yml b/.github/workflows/Shortlinks.yml new file mode 100644 index 00000000..89b259cb --- /dev/null +++ b/.github/workflows/Shortlinks.yml @@ -0,0 +1,38 @@ +name: ESLint Workflow +on: + schedule: + - cron: '0 0 * * *' +jobs: + shortlinks: + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v2 + + - name: Update shortlinks + uses: NinoDiscord/actions@master + with: + command: shortlinks + + - name: Setup Git configuration + run: | + git config --global user.name 'Noel[bot]' + git config --global user.email 'ohlookitsaugust@gmail.com' + git config --global committer.email 'cutie@floofy.dev' + git config --global committer.name 'Noel' + + - name: Check if git status is dirty + id: git_status + run: | + if [ -n "$(git status --porcelain)" ]; then + echo '::set-output name=STATUS_DIRTY::true' + else + echo '::set-output name=STATUS_DIRTY::false' + fi + + - name: Commit changes (if dirty) + if: contains(steps.git_status.outputs.STATUS_DIRTY, 'true') + run: | + git add . + git commit -m "chore(automate): update shortlinks.json file" + git push -u origin $(git rev-parse --abbrev-ref HEAD) diff --git a/.github/workflows/edge.yml b/.github/workflows/Staging.yml similarity index 100% rename from .github/workflows/edge.yml rename to .github/workflows/Staging.yml diff --git a/.github/workflows/ktlint.yml b/.github/workflows/ktlint.yml new file mode 100644 index 00000000..c65880b9 --- /dev/null +++ b/.github/workflows/ktlint.yml @@ -0,0 +1,82 @@ +name: ktlint +on: + push: + branches: + - 'feature/**' + - 'issue/gh-**' + - master + - edge + + paths-ignore: + - '.github/**' + - '.husky/**' + - '.vscode/**' + - 'assets/**' + - 'locales/**' + - 'docker/**' + - '.dockerignore' + - '.eslintignore' + - '.gitignore' + - '**.md' + - 'LICENSE' + - 'renovate.json' + + pull_request: + branches: + - 'feature/**' + - 'issue/gh-**' + - master + - edge + + paths-ignore: + - '.github/**' + - '.husky/**' + - '.vscode/**' + - 'assets/**' + - 'locales/**' + - 'docker/**' + - '.dockerignore' + - '.eslintignore' + - '.gitignore' + - '**.md' + - 'LICENSE' + - 'renovate.json' +jobs: + ktlint: + runs-on: ubuntu-latest + services: + redis: + image: bitnami/redis:latest + ports: + - 6379:6379 + timeouts: + image: ghcr.io/ninodiscord/timeouts/timeouts:latest + env: + AUTH: owodauwu + REDIS_HOST: redis + REDIS_PORT: 6379 + ports: + - 4025:4025 + steps: + - name: Checks out the repository + uses: actions/checkout@v2 + + - name: Sets up Java 17 + uses: actions/setup-java@v2 + with: + distribution: temurin # Eclipse Temurin is <3 + java-version: 17 + - name: Setup Gradle + uses: gradle/gradle-build-action@v2 + + - name: Lets ./gradlew be executable + run: chmod +x ./gradlew + + - name: Lints the repository for any code errors + run: ./gradlew spotlessCheck + + - name: Builds the project for any errors + run: ./gradlew compileKotlin + + - name: Unit test project + run: ./gradlew kotest diff --git a/.gitignore b/.gitignore index 1626ce53..49219d17 100644 --- a/.gitignore +++ b/.gitignore @@ -1,25 +1,569 @@ -# Folders -node_modules/ -old_locales/ -.husky/_/ -build/ +# User-specific stuff +.idea/**/workspace.xml +.idea/**/tasks.xml +.idea/**/usage.statistics.xml +.idea/**/dictionaries +.idea/**/shelf -# Jest -coverage/ +# AWS User-specific +.idea/**/aws.xml -# Files -application.yml -config.yml +# Generated files +.idea/**/contentModel.xml + +# Sensitive or high-churn files +.idea/**/dataSources/ +.idea/**/dataSources.ids +.idea/**/dataSources.local.xml +.idea/**/sqlDataSources.xml +.idea/**/dynamic.xml +.idea/**/uiDesigner.xml +.idea/**/dbnavigator.xml + +# Gradle +.idea/**/gradle.xml +.idea/**/libraries + +# Gradle and Maven with auto-import +# When using Gradle or Maven with auto-import, you should exclude module files, +# since they will be recreated, and may cause churn. Uncomment if using +# auto-import. +# .idea/artifacts +# .idea/compiler.xml +# .idea/jarRepositories.xml +# .idea/modules.xml +# .idea/*.iml +# .idea/modules +# *.iml +# *.ipr + +# CMake +cmake-build-*/ + +# Mongo Explorer plugin +.idea/**/mongoSettings.xml + +# File-based project format +*.iws + +# IntelliJ +out/ + +# mpeltonen/sbt-idea plugin +.idea_modules/ + +# JIRA plugin +atlassian-ide-plugin.xml + +# Cursive Clojure plugin +.idea/replstate.xml + +# SonarLint plugin +.idea/sonarlint/ + +# Crashlytics plugin (for Android Studio and IntelliJ) +com_crashlytics_export_strings.xml +crashlytics.properties +crashlytics-build.properties +fabric.properties + +# Editor-based Rest Client +.idea/httpRequests + +# Android studio 3.1+ serialized cache file +.idea/caches/build_file_checksums.ser + +### JetBrains+all Patch ### +# Ignores the whole .idea folder and all .iml files +# See https://github.com/joeblau/gitignore.io/issues/186 and https://github.com/joeblau/gitignore.io/issues/360 + +.idea/* + +# Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-249601023 + +*.iml +modules.xml +.idea/misc.xml +*.ipr + +# Sonarlint plugin +.idea/sonarlint + +### Kotlin ### +# Compiled class file +*.class + +# Log file *.log -.env -# Redis dumps, I run redis-server in the working directory :3 -*.rdb -launch.json +# BlueJ files +*.ctxt + +# Mobile Tools for Java (J2ME) +.mtj.tmp/ + +# Package Files # +*.jar +*.war +*.nar +*.ear +*.zip +*.tar.gz +*.rar + +# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml +hs_err_pid* +replay_pid* + +### Rust ### +# Generated by Cargo +# will have compiled files and executables +debug/ +target/ + +# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries +# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html +Cargo.lock + +# These are backup files generated by rustfmt +**/*.rs.bk + +# MSVC Windows builds of rustc generate these, which store debugging information +*.pdb + +### VisualStudioCode ### +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json +!.vscode/*.code-snippets + +# Local History for Visual Studio Code +.history/ + +# Built Visual Studio Code Extensions +*.vsix + +### VisualStudioCode Patch ### +# Ignore all local history of files +.history +.ionide + +# Support for Project snippet scope + +### Gradle ### +.gradle +build/ + +# Ignore Gradle GUI config +gradle-app.setting + +# Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored) +!gradle-wrapper.jar + +# Cache of project +.gradletasknamecache + +# # Work around https://youtrack.jetbrains.com/issue/IDEA-116898 +# gradle/wrapper/gradle-wrapper.properties + +### Gradle Patch ### +**/build/ + +### VisualStudio ### +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. +## +## Get latest from https://github.com/github/gitignore/blob/main/VisualStudio.gitignore + +# User-specific files +*.rsuser +*.suo +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Mono auto generated files +mono_crash.* + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +[Ww][Ii][Nn]32/ +[Aa][Rr][Mm]/ +[Aa][Rr][Mm]64/ +bld/ +[Bb]in/ +[Oo]bj/ +[Ll]og/ +[Ll]ogs/ + +# Visual Studio 2015/2017 cache/options directory +.vs/ +# Uncomment if you have tasks that create the project's static files in wwwroot +#wwwroot/ + +# Visual Studio 2017 auto generated files +Generated\ Files/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUnit +*.VisualState.xml +TestResult.xml +nunit-*.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# Benchmark Results +BenchmarkDotNet.Artifacts/ + +# .NET Core +project.lock.json +project.fragment.lock.json +artifacts/ + +# ASP.NET Scaffolding +ScaffoldingReadMe.txt + +# StyleCop +StyleCopReport.xml + +# Files built by Visual Studio +*_i.c +*_p.c +*_h.h +*.ilk +*.meta +*.obj +*.iobj +*.pch +*.ipdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*_wpftmp.csproj +*.tlog +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile +*.VC.db +*.VC.VC.opendb + +# Visual Studio profiler +*.psess +*.vsp +*.vspx +*.sap + +# Visual Studio Trace Files +*.e2e -# Ignore user-config -.vscode/ -.idea/ +# TFS 2012 Local Workspace +$tf/ -# v1 export -.nino +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# AxoCover is a Code Coverage Tool +.axoCover/* +!.axoCover/settings.json + +# Coverlet is a free, cross platform Code Coverage Tool +coverage*.json +coverage*.xml +coverage*.info + +# Visual Studio code coverage results +*.coverage +*.coveragexml + +# NCrunch +_NCrunch_* +.*crunch*.local.xml +nCrunchTemp_* + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +# Note: Comment the next line if you want to checkin your web deploy settings, +# but database connection strings (with potential passwords) will be unencrypted +*.pubxml +*.publishproj + +# Microsoft Azure Web App publish settings. Comment the next line if you want to +# checkin your Azure Web App publish settings, but sensitive information contained +# in these scripts will be unencrypted +PublishScripts/ + +# NuGet Packages +*.nupkg +# NuGet Symbol Packages +*.snupkg +# The packages folder can be ignored because of Package Restore +**/[Pp]ackages/* +# except build/, which is used as an MSBuild target. +!**/[Pp]ackages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/[Pp]ackages/repositories.config +# NuGet v3's project.json files produces more ignorable files +*.nuget.props +*.nuget.targets + +# Microsoft Azure Build Output +csx/ +*.build.csdef + +# Microsoft Azure Emulator +ecf/ +rcf/ + +# Windows Store app package directories and files +AppPackages/ +BundleArtifacts/ +Package.StoreAssociation.xml +_pkginfo.txt +*.appx +*.appxbundle +*.appxupload + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!?*.[Cc]ache/ + +# Others +ClientBin/ +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.jfm +*.pfx +*.publishsettings +orleans.codegen.cs + +# Including strong name files can present a security risk +# (https://github.com/github/gitignore/pull/2483#issue-259490424) +#*.snk + +# Since there are multiple workflows, uncomment next line to ignore bower_components +# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) +#bower_components/ + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm +ServiceFabricBackup/ +*.rptproj.bak + +# SQL Server files +*.mdf +*.ldf +*.ndf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings +*.rptproj.rsuser +*- [Bb]ackup.rdl +*- [Bb]ackup ([0-9]).rdl +*- [Bb]ackup ([0-9][0-9]).rdl + +# Microsoft Fakes +FakesAssemblies/ + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat +node_modules/ + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio 6 auto-generated workspace file (contains which files were open etc.) +*.vbw + +# Visual Studio 6 auto-generated project file (contains which files were open etc.) +*.vbp + +# Visual Studio 6 workspace and project file (working project files containing files to include in project) +*.dsw +*.dsp + +# Visual Studio 6 technical files + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + +# Paket dependency manager +.paket/paket.exe +paket-files/ + +# FAKE - F# Make +.fake/ + +# CodeRush personal settings +.cr/personal + +# Python Tools for Visual Studio (PTVS) +__pycache__/ +*.pyc + +# Cake - Uncomment if you are using it +# tools/** +# !tools/packages.config + +# Tabs Studio +*.tss + +# Telerik's JustMock configuration file +*.jmconfig + +# BizTalk build output +*.btp.cs +*.btm.cs +*.odx.cs +*.xsd.cs + +# OpenCover UI analysis results +OpenCover/ + +# Azure Stream Analytics local run output +ASALocalRun/ + +# MSBuild Binary and Structured Log +*.binlog + +# NVidia Nsight GPU debugger configuration file +*.nvuser + +# MFractors (Xamarin productivity tool) working folder +.mfractor/ + +# Local History for Visual Studio +.localhistory/ + +# Visual Studio History (VSHistory) files +.vshistory/ + +# BeatPulse healthcheck temp database +healthchecksdb + +# Backup folder for Package Reference Convert tool in Visual Studio 2017 +MigrationBackup/ + +# Ionide (cross platform F# VS Code tools) working folder +.ionide/ + +# Fody - auto-generated XML schema +FodyWeavers.xsd + +# VS Code files for those working on multiple tools +*.code-workspace + +# Local History for Visual Studio Code + +# Windows Installer files from build outputs +*.cab +*.msi +*.msix +*.msm +*.msp + +# JetBrains Rider +*.sln.iml + +# other stuff +config.yml +docker/cluster-operator/config.json +logs/*.log +bot/src/main/resources/config/logging.properties +!bot/src/main/resources/config/logging.example.properties diff --git a/.husky/pre-commit b/.husky/pre-commit deleted file mode 100644 index c00ddd06..00000000 --- a/.husky/pre-commit +++ /dev/null @@ -1,10 +0,0 @@ -#!/bin/sh -. "$(dirname "$0")/_/husky.sh" - -echo 'nino - ❓ lint ~ ❓ project - checking eslint for errors' -eslint src --ext .ts # `--fix` would normally be here but it should only print and not fix - -echo 'nino - ✔ lint ~ ❓ project - compiling project for errors' -tsc --noEmit - -echo 'nino - ✔ lint ~ ✔ project - we are done here' diff --git a/.prettierignore b/.prettierignore deleted file mode 100644 index c20b01f2..00000000 --- a/.prettierignore +++ /dev/null @@ -1,16 +0,0 @@ -.github -.husky -.idea -assets -build - -.dockerignore -.env -.eslintignore -.gitignore -LICENSE -package-*.json -package.json -README.md -renovate.json -tsconfig.json diff --git a/.prettierrc.json b/.prettierrc.json deleted file mode 100644 index 6f57c5ce..00000000 --- a/.prettierrc.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "semi": true, - "tabWidth": 2, - "singleQuote": true, - "endOfLine": "lf", - "printWidth": 120, - "trailingComma": "es5", - "bracketSpacing": true, - "jsxBracketSameLine": false -} diff --git a/.vscode/settings.json b/.vscode/settings.json deleted file mode 100644 index 44aeb406..00000000 --- a/.vscode/settings.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "typescript.tsdk": "node_modules\\typescript\\lib" -} diff --git a/Dockerfile b/Dockerfile index 0b37a573..43cda0da 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,17 +1,17 @@ -FROM node:17-alpine +FROM eclipse-temurin:17-alpine AS builder -LABEL MAINTAINER="Nino " RUN apk update && apk add git ca-certificates - -WORKDIR /opt/Nino +WORKDIR / COPY . . -RUN apk add --no-cache git -RUN npm i -g typescript eslint typeorm -RUN yarn -RUN yarn build:no-lint -RUN yarn cache clean +RUN chmod +x gradlew && ./gradlew :bot:installDist --stacktrace + +FROM eclipse-temurin:17-alpine AS builder -# Give it executable permissions -RUN chmod +x ./scripts/run-docker.sh +WORKDIR /app/noelware/nino +COPY --from=builder /docker/run.sh /app/noelware/nino/run.sh +COPY --from=builder /bot/build/install/bot /app/noelware/nino/bot +COPY --from=builder /docker/scripts/liblog.sh /app/noelware/nino/scripts/liblog.sh +COPY --from=builder /docker/docker-entrypoint.sh /app/noelware/nino/docker-entrypoint.sh -ENTRYPOINT [ "sh", "./scripts/run-docker.sh" ] +ENTRYPOINT [ "/app/noelware/nino/docker-entrypoint.sh" ] +CMD [ "/app/noelware/nino/run.sh" ] diff --git a/LICENSE b/LICENSE index b4c59589..baa7da17 100644 --- a/LICENSE +++ b/LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2019-2021 Nino +Copyright (c) 2019-2022 Nino Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index e60d980b..1120e0cc 100644 --- a/README.md +++ b/README.md @@ -1,130 +1,307 @@ -
-

🔨 Nino

-
Cute, advanced discord moderation bot made in Eris. Make your server cute and automated with utilities for you and your server moderators *:・゚✧*:・゚✧
-
+# [Nino](https://nino.sh) • GitHub Workflow Status GitHub License Noelware Server -
- GitHub Workflow Status - GitHub License - Noelware Server -
- -
+> :hammer: **Cute, advanced discord moderation bot made in Kord. Make your server cute and automated with utilities for you and your server moderators! ☆ ~('▽^人)** ## Features -- Auto Moderation: **Prevents raids, spam, ads, and much more!** -- Advanced warning system and automated punishments: **Automically punish who commit offenses!** -- Simplicity: **Simplicity is key to any discord bot, and Nino makes sure of it! All commands are tailored to be simple yet powerful.** +- 🔨 **Auto Moderation** - Prevent raids, spam, ads, and much more! + - Target users who post invites to your server, spam in a certain interval, and much more! + - Thresholds, messages, prevent roles/members from automod is all customizable! + +- ⚙️ **Advanced Configuration** - Configure Nino to feel like Nino is a part of your server~ + - Anything to Nino (mod-log messages, automod threshold/messages/prevention, logging) is all customizable! + - Implement guild policies to implement custom events Nino will react to! + +- ✨ **Simplicity** - Simplicity is key to any discord bot, and Nino is sure of it! + - Commands are tailored to be powerful but simple to the end user. + - Retrieve help on a command using `x!help `, `x! --help/-h`, or `x! help`. + - Stuck on what command usage is like? You can run `x!help usage` on how arguments are executed. ...and much more! ## Support -Need support related to Nino or any microservices under the organization? Join in the **Noelware** Discord server in #support under the **Nino** category: +Need any help with Nino or any microservices under the **@NinoDiscord** organization? You can join the **Noelware** Discord server +under [**#support**](https://discord.com/channels/824066105102303232/824071651486335036): -[![discord embed owo](https://discord.com/api/v9/guilds/824066105102303232/widget.png?style=banner3)](https://discord.gg/ATmjFH9kMH) +![Noelware Discord Server](https://invidget.switchblade.xyz/ATmjFH9kMH) ## Contributing -View our [contributing guidelines](https://github.com/NinoDiscord/Nino/blob/master/.github/CONTRIBUTING.md) and [code of conduct](https://github.com/NinoDiscord/Nino/blob/master/.github/CODE_OF_CONDUCT.md) before contributing. +Before you get started to contribute to Nino, view our [contributing guidelines](https://github.com/NinoDiscord/Nino/blob/master/.github/CONTRIBUTING.md) and our [code of conduct](https://github.com/NinoDiscord/Nino/blob/master/.github/CODE_OF_CONDUCT.md) before contributing. ## Self-hosting -Before attempting to self-host Nino, we didn't plan for users to be able to self-host their own instance of Nino. Most builds are usually buggy and untested as of late, we do have a "stable" branch but it can be buggy sometimes! If you want to use cutting edge features that are most likely not finished, view the [edge](https://github.com/NinoDiscord/Nino/tree/edge) branch for more details. The "stable" branch is master, so anything that should be stable will be added to the upstream. - -We will not provide support on how to self-host Nino, use at your own risk! If you do not want to bother hosting it, you can always invite the [public instance](https://discord.com/oauth2/authorize?client_id=531613242473054229&scope=bot) which will be the same experience if you hosted it or not. +Before to self-host Nino, ***we will not give you support on how to run your own Nino!*** The source code is available for +education purposes, and is not meant to run on small instances. Also, we have not planned for people to self-host their own +instance of Nino, since the source code and the main bots ARE identical from the source code. And, most builds pushed +to this repository have NOT been tested in production environments, so bewarn before running! If you do encouter bugs +with the bot or running it, please report it in our [issue tracker](https://github.com/NinoDiscord/Nino/issues) with the **bug** +label! ### Prerequisites -Before running your own instance of Nino, you will need the following tools: +Before running your own instance of Nino, you will need the following required tools: + +- [Timeouts Microservice](https://github.com/NinoDiscord/timeouts) **~** Used for mutes, bans, and more. This will not make Nino operate successfully. +- [PostgreSQL](https://postgresql.org) **~** Main database for holding user or guild data. Recommended version is 10 or higher. +- [Redis](https://redis.io) **~** Open source in-memory database storage to hold entities for quick retrieval. Recommended version is 5 or higher. +- [Java](https://java.com) **~** Language compiler for Gradle and Kotlin. Required version is JDK 16 or higher. + +If you're moving from **v0.x** -> **v2.x**, you will need to have your MongoDB instance and our utility for converting documents +into JSON, [Rei](https://github.com/NinoDiscord/Rei) before contiuning. + +#### Extra Tooling +There is tools that are *optional* but are mostly not-recommended in most cases: -- [Timeouts Service](https://github.com/NinoDiscord/timeouts) (Used for mutes and such or it'll not work!) -- [Node.js](https://nodejs.org) (Latest is always used in development, but LTS is recommended) -- [PostgreSQL](https://postgresql.org) (12 is used in development but anything above 10 should fairly work!) -- [Redis](https://redis.io) (6.2 is used in development but above v5 should work) +- [cluster-operator](https://github.com/MikaBot/cluster-operator) **~** Easily manages discord clustering between multiple nodes +- [Docker](https://docker.com) **~** Containerisation tool for isolation between the host and the underlying container. +- [Sentry](https://sentry.io) **~** Open-source application monitoring, with a focus on error reporting. -If you're moving from v0 to v1, you will need your MongoDB instance before to port the database and [Rei](https://github.com/NinoDiscord/Rei) installed on your system. +### Setup +You're almost there on how to run your instance! Before you frantically clone the repository and such, there is two options +on how to use Nino: -There is tools that are optional but are mostly recommended in some cases: +- Normally: You can spin up a **PM2** process to run Nino, which will run fine! +- Docker: Uses the [Dockerfile](./Dockerfile) to run Nino in a isolated container seperated from your machine and the host. -- [Sentry](https://sentry.io) - Useful to find out where errors are in a pretty UI -- [Docker](https://docker.com) - If you're a masochist and want to run a private instance with Docker -- [Git](https://git-scm.com) - Useful for fetching new updates easily. +#### Docker Setup +> ️️️️️️️⚠️ **BEFORE YOU CLONE, MAKE SURE DOCKER IS INSTALLED!** -### Setting up -There are 2 ways to setup Nino: using Docker or just doing shit yourself. Doing it yourself can very tedious -of how much Nino uses from v0 to v1 since Nino utilizes microservices! **☆♬○♩●♪✧♩((ヽ( ᐛ )ノ))♩✧♪●♩○♬☆** +1. **Clone the repository** using the `git clone` command: -### Docker ```sh -# 1. Clone the repository -$ git clone https://github.com/NinoDiscord/Nino.git && cd Nino +$ git clone https://github.com/NinoDiscord/Nino [-b edge] # If you want to use cutting edge features, +# add the `-b edge` flag! +``` + +2. **Change the directory** to `Nino` or however you named it, and create a image: -# 2. Create a image +```sh +$ cd Nino $ docker build -t nino:latest --no-cache . +``` + +3. **Run the image** to start the bot! + +```sh +# A volume is required for `.env` and `config.yml` so it can properly +# load your configuration! +$ docker run -d -v './config.yml:/app/Nino/config.yml:ro' -v './.env:/app/Nino/.env' nino:latest +``` -# 3. Run the image -$ docker run -d \ - --volume './config.yml:/opt/Nino/config.yml:ro' \ # read-only - nino:latest +4. **[OPTIONAL]** Use `docker-compose` to start all services. -# OPTIONAL: Use docker-compose.yml to run the services +```sh +# We provide a `docker-compose.yml` file so you can spin up the required +# services Nino requires without setting it up yourself. $ docker-compose up -d ``` -### Normal +#### Normal Setup +> ✏️ **Make sure you have a service to run Nino like `systemd` or `pm2`.** + +1. **Clone the repository** using the `git clone` command: + ```sh -# 1. Clone the repository -$ git clone https://github.com/NinoDiscord/Nino.git && cd Nino +$ git clone https://github.com/NinoDiscord/Nino [-b edge] # If you want to use cutting edge features, +# add the `-b edge` flag! +``` + +2. **Install import dependencies** -# 2. Install the dependencies -$ npm install +```sh +$ ./gradlew tasks +``` -# 3. Build the project -$ npm run build +3. **Build and compile** the Kotlin code -# 4. Run the project -$ npm start +```sh +$ ./gradlew :bot:build ``` -### Migrating from v0.x -> v1.x -If you used v0.x in the past, this is the process on how to migrate: +4. **Runs the project** -- Run `rei convert ...` to convert the documents into JSON, this process should take a while if there is a lot of cases or warnings. -- Run `node scripts/migrate.js `, where `` is the directory Rei converted your database to. +```sh +$ java -jar ./bot/build/libs/Nino.jar +``` -## Example `config.yml` file -- Replace `` with your Discord bot's token -- Replace `` with your PostgreSQL database username -- Replace `` with your PostgreSQL database password -- Replace `` (under `database`) with your PostgreSQL database host, if running locally, just use `localhost` or `database` if on Docker -- Replace `` with your PostgreSQL database port it's running, if running locally, set it to `5432` -- Replace `` (under `redis`) with your Redis connection host, if running locally, just use `localhost` or `redis` if on Docker -- Replace `` with the authenication token you set in the [timeouts](https://github.com/NinoDiscord/timeouts) relay service. +## Migrations +There has many changes towards the database when it comes to Nino, since all major releases have some-what a database revision once a while. -```yml -environment: development -token: +### v0.x -> v2.x +If you used **v0.x** in the past and you want to use the **v2.x** version, you can run the following commands: + +```sh +# Convert your MongoDB database into JSON file that the migrator script can read. +$ rei convert ... +# Runs the migrator script, if you're using Windows, +# use PowerShell: `./scripts/migrate.ps1` +$ ./scripts/migrate 0.x ./data +``` + +### v1.x -> v2.x +If you wish to migrate from **v1.x** towards **v2.x**, you can run the following commands: + +```sh +# Runs the migrator script +# *NIX: +$ ./scripts/migrate 1.x --password=... --user=... --database=... --host=... --port=... + +# PowerShell: +$ ./scripts/migrate.ps1 1.x -Password ... -User ... -Database ... -Host ... -Port ... +``` + +## Configuration +Nino's configurations are made up in a simple **config.yml** file located in the `root directory` of the project. The following keys +must be replaced: + +- **Replace `` with your [Discord bot's](https://discord.com/developers/applications) token.** +- **Replace `` with your Redis host** + - **If you are using Docker Compose, replace `` with "redis" since Compose will link the host with the container.** + - **If you're running it locally or the config key is not present, it'll infer as `localhost`** +- **Replace `` with your Redis network port** + - **If you are using Docker Compose, you can omit this config key since Compose will infer it to the redis container.** + - **If you're running it locally or the config key is not present, it'll infer as `6379`** +- **Replace `` with your PostgreSQL host.** + - **If you are using Docker Compose, replace `` with "postgres" since Compose will link the host with the container.** + - **If you're running it locally or the config key is not present, it'll infer as `localhost`** +- **Replace `` with your Redis network port** + - **If you are using Docker Compose, you can omit this config key since Compose will infer it to the redis container.** + - **If you're running it locally or the config key is not present, it'll infer as `5432`** + +```yml +# Returns the default locale Nino will use to send messages with. Our locales are managed +# under our GitHub repository for now, but this will change. +# +# Default: "en_US" +defaultLocale: "en_US" or "fr_FR" or "pt_BR" + +# Sets the environment for logging and such, `development` will give you debug logs +# in which you can report bugs and `production` will omit debug logs without +# any clutter. +# +# Default: "development" +environment: "development" or "production" + +# Sets the DSN url for configuring Sentry, this is not recommended on smaller instances! +# +# Default: Not present. +sentryDsn: ... + +# Returns the owners of the bot that can execute system admin commands like +# `eval`, `sh`, etc. +# +# Default: [empty array] +owners: + - owner1 + - owner2 + - ... + +# Yields your token to authenticate with Discord. This is REQUIRED +# and must be a valid token or it will not connect. +token: ... + +# Returns the token for `ravy.org` API, you cannot retrieve a key +# this is only for the public instances. +ravy: ... + +# Returns a list of prefixes to use when executing text-based commands prefixes: - - ! + - owo! + - uwu? + - pwp. -database: - url: postgres://:@:/ +# Returns the configuration for the botlists task. +# This is not recommended for smaller instances since using Nino and adding it +# to a public botlist will be deleted from it. +botlists: + # Returns the token for posting to Discord Services - https://discordservices.net + dservices: ... + + # Returns the token for posting to Discord Boats - https://discord.boats + dboats: ... + + # Returns the token for posting to Discord Bots - https://discord.bots.gg + dbots: ... + + # Returns the token for posting to top.gg - https://top.gg + topgg: ... + + # Returns the token for posting to Delly (Discord Extreme List) - https://del.rip + delly: ... + # Returns the token for posting to https://discords.com + discords: ... + +# Configuration for Redis for caching entities for quick retrival. +# Read our Privacy Policy on how we collect minimal data: https://nino.sh/privacy redis: - host: - port: 6379 + # Returns the password for authenticating to your Redis database. + password: ... + + # Returns an array of sentinels mapped to "host:port", + # this isn't required on smaller instances. + # Read more: https://redis.io/topics/sentinel + sentinels: + - host:port + - host2:port2 + + # Returns the master password for authenticating using the Sentinel + # approach. This is not required on smaller instances. + # Read more: https://redis.io/topics/sentinel + master: ... + + # Returns the index for Nino so it doesn't collide with any other + # Redis databases. + db: 1-16 + # Returns the redis host for connecting + host: ... + + # Returns the port for connecting + port: ... + +# Timeouts configuration timeouts: + # Returns the port for connecting to the WebSocket server. port: 4025 - auth: + + # Returns the authentication string for authorizing. + auth: ... + +# Instatus configuration for displaying the Gateway Ping +instatus: + # Metric component ID + # Use the instatus cli to retrieve: https://github.com/auguwu/instatus-cli + metricId: ... + + # The statuspage component ID + # Use the instatus cli to retrieve: https://github.com/auguwu/instatus-cli + component: ... + + # Instatus API key, fetch it here: + key: ... + +# Database configuration. Required! +database: + username: postgres + password: postgres + schema: public + host: + port: + name: nino ``` ## Maintainers -* Ice#4710 (DevOps, Developer) ([GitHub](https://github.com/IceeMC)) -* Rodentman87#8787 (Frontend Developer) ([GitHub](https://github.com/Rodentman87)) -* August#5820 (Project Lead, Developer) ([GitHub](https://github.com/auguwu)) +- [**Maisy ~ Rodentman87#8787**](https://likesdinosaurs.com) - Web Developer ([GitHub](https://githubc.om/Rodentman87)) +- [**Noel ~ August#5820**](https://floofy.dev) - Project Lead ([GitHub](https://github.com/auguwu)) +- [**Ice ~ Ice#4710**](https://winterfox.tech) - DevOps ([GitHub](https://github.com/IceeMC)) ## Hackweek Participants -* Chris ([GitHub](https://github.com/auguwu)) -* dondish ([GitHub](https://github.com/dondish)) -* Kyle ([GitHub](https://github.com/scrap)) -* Wessel ([GitHub](https://github.com/Wessel)) -* David ([GitHub](https://github.com/davidjcralph)) +> Since Nino was a submission towards [Discord's Hackweek](https://blog.discord.com/discord-community-hack-week-build-and-create-alongside-us-6b2a7b7bba33), this is a list of the participants that contributed to the project during June 23rd, 2019 - June 28th, 2019. + +- [**davidjcralph#9721**](https://davidjcralph.com) - ([GitHub](https://github.com/davidjcralph)) +- [**August#5820**](https://floofy.dev) - ([GitHub](https://github.com/auguwu)) +- [**dondish#8072**](https://odedshapira.me/) - ([GitHub](https://github.com/dondish)) +- [**Wessel#0498**](https://wessel.meek.moe) - ([GitHub](https://github.com/Wessel)) +- **Kyle** - ([GitHub](https://github.com/scrap)) + +## License +**Nino** is released under the **MIT License**, read [here](/LICENSE) for more information! 💖 diff --git a/assets/HEADING b/assets/HEADING new file mode 100644 index 00000000..9d0b8e84 --- /dev/null +++ b/assets/HEADING @@ -0,0 +1,22 @@ +/* + * Copyright (c) 2019-2022 Nino + * + * 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/assets/banner.txt b/assets/banner.txt new file mode 100644 index 00000000..14525490 --- /dev/null +++ b/assets/banner.txt @@ -0,0 +1,19 @@ + ##### # ## + ###### /# #### / # + /# / / ## ###/ ### +/ / / ## # # # + / / ## # + ## ## ## # ### ### /### /### + ## ## ## # ### ###/ #### / / ### / + ## ## ## # ## ## ###/ / ###/ + ## ## ## # ## ## ## ## ## + ## ## ## # ## ## ## ## ## + # ## ### ## ## ## ## ## + / ### ## ## ## ## ## + /##/ ## ## ## ## ## ## + / ##### ### / ### ### ###### +/ ## ##/ ### ### #### +# + ## + +~ Version: v{{.Version}} ~ Commit SHA: {{.CommitSha}} @ {{.BuildDate}} ~ diff --git a/bot/README.md b/bot/README.md new file mode 100644 index 00000000..9ff89c33 --- /dev/null +++ b/bot/README.md @@ -0,0 +1,15 @@ +# bot submodule +This is a collection of modules that keeps the Discord bot running together. + +## Modules +- [api](./api) - The API that is used for the dashboard and the slash commands implementation. +- [automod](./automod) - Collection of automod that Nino executes. +- [commands](./commands) - Text-based commands implementation. +- [core](./core) - Core components + modules. +- [database](./database) - Database models and utilities. +- [markup](./markup) - Soon:tm: markup language for customizing modlogs and logging outputs. +- [metrics](./metrics) - Prometheus metric registry. +- [punishments](./punishments) - Core punishments module to punish users based off an action. +- [slash-commands](./slash-commands) - Slash commands implementation. +- [src](./src) - The main application that you run with `java -jar` or with Docker! +- [timeouts](./timeouts) - Kotlin client for Nino's [timeouts microservice](https://github.com/NinoDiscord/timeouts) diff --git a/bot/api/build.gradle.kts b/bot/api/build.gradle.kts new file mode 100644 index 00000000..9727107b --- /dev/null +++ b/bot/api/build.gradle.kts @@ -0,0 +1,33 @@ +/** + * Copyright (c) 2019-2022 Nino + * + * 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. + */ + +plugins { + `nino-module` +} + +dependencies { + implementation("io.prometheus:simpleclient_common:0.14.1") + implementation("io.ktor:ktor-server-netty") + implementation(project(":bot:database")) + implementation(project(":bot:metrics")) + implementation(project(":bot:core")) +} diff --git a/bot/api/src/main/kotlin/sh/nino/discord/api/ApiServer.kt b/bot/api/src/main/kotlin/sh/nino/discord/api/ApiServer.kt new file mode 100644 index 00000000..7ae4743e --- /dev/null +++ b/bot/api/src/main/kotlin/sh/nino/discord/api/ApiServer.kt @@ -0,0 +1,121 @@ +/* + * Copyright (c) 2019-2022 Nino + * + * 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. + */ + +package sh.nino.discord.api + +import gay.floof.utils.slf4j.logging +import io.ktor.application.* +import io.ktor.features.* +import io.ktor.routing.* +import io.ktor.serialization.* +import io.ktor.server.engine.* +import io.ktor.server.netty.* +import org.koin.core.context.GlobalContext +import org.slf4j.LoggerFactory +import sh.nino.discord.api.middleware.ErrorHandling +import sh.nino.discord.api.middleware.Logging +import sh.nino.discord.api.middleware.ratelimiting.Ratelimiting +import sh.nino.discord.common.DEDI_NODE +import sh.nino.discord.common.NinoInfo +import sh.nino.discord.common.data.Config +import sh.nino.discord.common.data.Environment +import sh.nino.discord.common.extensions.retrieve +import sh.nino.discord.common.extensions.retrieveAll +import java.util.concurrent.TimeUnit + +class ApiServer { + private lateinit var server: NettyApplicationEngine + private val log by logging() + + suspend fun launch() { + log.info("Launching API server...") + + val config = GlobalContext.retrieve() + val environment = applicationEngineEnvironment { + this.developmentMode = config.environment == Environment.Development + this.log = LoggerFactory.getLogger("sh.nino.discord.api.ktor.Application") + + connector { + host = config.api!!.host + port = config.api!!.port + } + + module { + install(ErrorHandling) + install(Ratelimiting) + install(Logging) + + install(ContentNegotiation) { + json(GlobalContext.retrieve()) + } + + install(CORS) { + header("X-Forwarded-Proto") + anyHost() + } + + install(DefaultHeaders) { + header("X-Powered-By", "Nino/DiscordBot (+https://github.com/NinoDiscord/Nino; ${NinoInfo.VERSION})") + header("Server", "Noelware${if (DEDI_NODE != "none") "/$DEDI_NODE" else ""}") + } + + val endpoints = GlobalContext.retrieveAll() + log.info("Found ${endpoints.size} endpoints to register.") + + for (endpoint in endpoints) { + log.info("|- Found ${endpoint.routes.size} routes to implement.") + routing { + for (route in endpoint.routes) { + log.info(" \\- ${route.method.value} ${route.path}") + + route(route.path, route.method) { + handle { + try { + return@handle route.execute(this.call) + } catch (e: Exception) { + log.error("Unable to handle request \"${route.method.value} ${route.path}\":", e) + } + } + } + } + } + } + } + } + + server = embeddedServer(Netty, environment) + server.start(wait = true) + } + + suspend fun shutdown() { + if (!::server.isInitialized) { + log.warn("Server was never initialized, skipping") + return + } + + val ratelimiter = server.application.featureOrNull(Ratelimiting) + ratelimiter?.ratelimiter?.close() + + log.info("Dying off connections...") + server.stop(1, 5, TimeUnit.SECONDS) + } +} diff --git a/src/entities/LoggingEntity.ts b/bot/api/src/main/kotlin/sh/nino/discord/api/Endpoint.kt similarity index 50% rename from src/entities/LoggingEntity.ts rename to bot/api/src/main/kotlin/sh/nino/discord/api/Endpoint.kt index 101fc1b2..06735c1e 100644 --- a/src/entities/LoggingEntity.ts +++ b/bot/api/src/main/kotlin/sh/nino/discord/api/Endpoint.kt @@ -1,5 +1,5 @@ -/** - * Copyright (c) 2019-2021 Nino +/* + * Copyright (c) 2019-2022 Nino * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -20,35 +20,34 @@ * SOFTWARE. */ -import { Entity, Column, PrimaryColumn } from 'typeorm'; +package sh.nino.discord.api -export enum LoggingEvents { - VoiceMemberDeafened = 'voice_member_deafened', - VoiceChannelSwitch = 'voice_channel_switch', - VoiceMemberMuted = 'voice_member_muted', - VoiceChannelLeft = 'voice_channel_left', - VoiceChannelJoin = 'voice_channel_join', - MessageDeleted = 'message_delete', - MessageUpdated = 'message_update', -} - -@Entity({ name: 'logging' }) -export default class LoggingEntity { - @Column({ default: '{}', array: true, type: 'text', name: 'ignore_channels' }) - public ignoreChannels!: string[]; +import io.ktor.application.* +import io.ktor.http.* +import kotlin.reflect.KCallable +import kotlin.reflect.full.callSuspend +import kotlin.reflect.full.findAnnotation +import kotlin.reflect.full.hasAnnotation +import sh.nino.discord.api.annotations.Route as RouteMeta - @Column({ default: '{}', array: true, type: 'text', name: 'ignore_users' }) - public ignoreUsers!: string[]; - - @Column({ name: 'channel_id', nullable: true }) - public channelID?: string; +class Route(val path: String, val method: HttpMethod, private val callable: KCallable<*>, private val thiz: Any) { + suspend fun execute(call: ApplicationCall) { + callable.callSuspend(thiz, call) + } +} - @Column({ default: false }) - public enabled!: boolean; +open class Endpoint(val prefix: String) { + companion object { + fun merge(prefix: String, other: String): String { + if (other == "/") return prefix - @Column({ type: 'enum', array: true, enum: LoggingEvents, default: '{}' }) - public events!: LoggingEvents[]; + return "${if (prefix == "/") "" else prefix}$other" + } + } - @PrimaryColumn({ name: 'guild_id' }) - public guildID!: string; + val routes: List + get() = this::class.members.filter { it.hasAnnotation() }.map { + val meta = it.findAnnotation()!! + Route(merge(this.prefix, meta.path), HttpMethod.parse(meta.method.uppercase()), it, this) + } } diff --git a/bot/api/src/main/kotlin/sh/nino/discord/api/_Module.kt b/bot/api/src/main/kotlin/sh/nino/discord/api/_Module.kt new file mode 100644 index 00000000..4ea158b3 --- /dev/null +++ b/bot/api/src/main/kotlin/sh/nino/discord/api/_Module.kt @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2019-2022 Nino + * + * 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. + */ + +package sh.nino.discord.api + +import org.koin.dsl.module +import sh.nino.discord.api.routes.endpointModule + +val apiModule = endpointModule + module { + single { + ApiServer() + } +} diff --git a/bot/api/src/main/kotlin/sh/nino/discord/api/annotations/Route.kt b/bot/api/src/main/kotlin/sh/nino/discord/api/annotations/Route.kt new file mode 100644 index 00000000..23f5e259 --- /dev/null +++ b/bot/api/src/main/kotlin/sh/nino/discord/api/annotations/Route.kt @@ -0,0 +1,28 @@ +/* + * Copyright (c) 2019-2022 Nino + * + * 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. + */ + +package sh.nino.discord.api.annotations + +annotation class Route( + val path: String, + val method: String +) diff --git a/bot/api/src/main/kotlin/sh/nino/discord/api/annotations/SlashCommand.kt b/bot/api/src/main/kotlin/sh/nino/discord/api/annotations/SlashCommand.kt new file mode 100644 index 00000000..f4769fb9 --- /dev/null +++ b/bot/api/src/main/kotlin/sh/nino/discord/api/annotations/SlashCommand.kt @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2019-2022 Nino + * + * 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. + */ + +package sh.nino.discord.api.annotations + +/** + * Represents the declaration of a slash command with some metadata. + * @param name The name of the slash command, must be 1-32 characters. + * @param description The description of the slash command, must be 1-100 characters. + * @param onlyIn Guild IDs where this slash command will be registered in. + * @param userPermissions Bitwise values of the required permissions for the executor. + * @param botPermissions Bitwise values of the required permissions for the bot. + */ +annotation class SlashCommand( + val name: String, + val description: String, + val onlyIn: LongArray = [], + val userPermissions: LongArray = [], + val botPermissions: LongArray = [] +) diff --git a/bot/api/src/main/kotlin/sh/nino/discord/api/middleware/ErrorHandling.kt b/bot/api/src/main/kotlin/sh/nino/discord/api/middleware/ErrorHandling.kt new file mode 100644 index 00000000..28df1d53 --- /dev/null +++ b/bot/api/src/main/kotlin/sh/nino/discord/api/middleware/ErrorHandling.kt @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2019-2022 Nino + * + * 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. + */ + +package sh.nino.discord.api.middleware + +import io.ktor.application.* +import io.ktor.util.* +import io.sentry.Sentry + +class ErrorHandling { + companion object: ApplicationFeature { + override val key: AttributeKey = AttributeKey("ErrorHandling") + override fun install(pipeline: ApplicationCallPipeline, configure: Unit.() -> Unit): ErrorHandling { + pipeline.intercept(ApplicationCallPipeline.Call) { + try { + proceed() + } catch (e: Exception) { + if (Sentry.isEnabled()) Sentry.captureException(e) + + throw e + } + } + + return ErrorHandling() + } + } +} diff --git a/bot/api/src/main/kotlin/sh/nino/discord/api/middleware/Logging.kt b/bot/api/src/main/kotlin/sh/nino/discord/api/middleware/Logging.kt new file mode 100644 index 00000000..74c1f42c --- /dev/null +++ b/bot/api/src/main/kotlin/sh/nino/discord/api/middleware/Logging.kt @@ -0,0 +1,81 @@ +/* + * Copyright (c) 2019-2022 Nino + * + * 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. + */ + +package sh.nino.discord.api.middleware + +import gay.floof.utils.slf4j.logging +import io.ktor.application.* +import io.ktor.http.* +import io.ktor.request.* +import io.ktor.util.* +import io.ktor.util.pipeline.* +import io.prometheus.client.Histogram +import org.koin.core.context.GlobalContext +import sh.nino.discord.common.extensions.retrieve +import sh.nino.discord.metrics.MetricsRegistry + +class Logging { + private val log by logging() + private val startTimePhase = PipelinePhase("StartTimePhase") + private val logResponsePhase = PipelinePhase("LogResponsePhase") + private val prometheusObserver = AttributeKey("PrometheusObserver") + private val startTimeKey = AttributeKey("StartTimeKey") + + private fun install(pipeline: Application) { + pipeline.environment.monitor.subscribe(ApplicationStopped) { + log.warn("API has completely halted.") + } + + pipeline.addPhase(startTimePhase) + pipeline.intercept(startTimePhase) { + call.attributes.put(startTimeKey, System.currentTimeMillis()) + } + + pipeline.addPhase(logResponsePhase) + pipeline.intercept(logResponsePhase) { + logResponse(call) + } + + pipeline.intercept(ApplicationCallPipeline.Setup) { + val metrics = GlobalContext.retrieve() + if (metrics.enabled) { + val timer = metrics.apiRequestLatency!!.startTimer() + call.attributes.put(prometheusObserver, timer) + } + } + } + + private suspend fun logResponse(call: ApplicationCall) { + val time = System.currentTimeMillis() - call.attributes[startTimeKey] + val status = call.response.status()!! + val body = call.receive() + val timer = call.attributes.getOrNull(prometheusObserver) + + timer?.observeDuration() + log.info("${status.value} ${status.description} - ${call.request.httpMethod.value} ${call.request.path()} (${body.size} bytes written) [${time}ms]") + } + + companion object: ApplicationFeature { + override val key: AttributeKey = AttributeKey("Logging") + override fun install(pipeline: Application, configure: Unit.() -> Unit): Logging = Logging().apply { install(pipeline) } + } +} diff --git a/bot/api/src/main/kotlin/sh/nino/discord/api/middleware/ratelimiting/Ratelimiter.kt b/bot/api/src/main/kotlin/sh/nino/discord/api/middleware/ratelimiting/Ratelimiter.kt new file mode 100644 index 00000000..cb60d5b9 --- /dev/null +++ b/bot/api/src/main/kotlin/sh/nino/discord/api/middleware/ratelimiting/Ratelimiter.kt @@ -0,0 +1,206 @@ +/* + * Copyright (c) 2019-2022 Nino + * + * 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. + */ + +package sh.nino.discord.api.middleware.ratelimiting + +import gay.floof.utils.slf4j.logging +import io.ktor.application.* +import io.ktor.features.* +import io.ktor.http.* +import kotlinx.coroutines.future.await +import kotlinx.coroutines.launch +import kotlinx.coroutines.sync.Mutex +import kotlinx.datetime.Clock +import kotlinx.datetime.Instant +import kotlinx.serialization.Serializable +import kotlinx.serialization.json.Json +import org.apache.commons.lang3.time.StopWatch +import sh.nino.discord.common.extensions.inject +import sh.nino.discord.core.NinoScope +import sh.nino.discord.core.redis.RedisManager +import java.util.* +import java.util.concurrent.TimeUnit + +@Serializable +data class Ratelimit( + val remaining: Int = 1200, + val resetTime: Instant = Clock.System.now(), + val limit: Int = 1200 +) { + val exceeded: Boolean + get() = !this.expired && this.remaining == 0 + + val expired: Boolean + get() = resetTime <= Clock.System.now() + + fun consume(): Ratelimit = copy(remaining = (remaining - 1).coerceAtLeast(0)) +} + +class Ratelimiter { + private val logger by logging() + private val json by inject() + private val redis by inject() + private val timer = Timer("Nino-APIRatelimitPurge") + private val purgeMutex = Mutex() + private val cachedRatelimits = mutableMapOf() + + init { + val watch = StopWatch.createStarted() + val count = redis.commands.hlen("nino:ratelimits").get() + watch.stop() + + logger.info("Took ${watch.getTime(TimeUnit.MILLISECONDS)}ms to retrieve $count ratelimits!") + val reorderWatch = StopWatch.createStarted() + val result = redis.commands.hgetall("nino:ratelimits").get() as Map + + // Decode from JSON + // TODO: use protobufs > json + // why? - https://i-am.floof.gay/images/8f3b01a0.png + // NQN - not quite nitro discord bot + for ((key, value) in result) { + val ratelimit = json.decodeFromString(Ratelimit.serializer(), value) + cachedRatelimits[key] = ratelimit + } + + reorderWatch.stop() + logger.info("Took ${watch.getTime(TimeUnit.MILLISECONDS)}ms to reorder in-memory rate limit cache.") + + // Clear the expired ones + NinoScope.launch { + val locked = purgeMutex.tryLock() + if (locked) { + try { + purge() + } finally { + purgeMutex.unlock() + } + } + } + + // Set up a timer every hour to purge! + timer.scheduleAtFixedRate( + object: TimerTask() { + override fun run() { + NinoScope.launch { + val locked = purgeMutex.tryLock() + if (locked) { + try { + purge() + } finally { + purgeMutex.unlock() + } + } + } + } + }, + 0, 3600000 + ) + } + + private suspend fun purge() { + logger.info("Finding useless ratelimits...") + + val ratelimits = cachedRatelimits.filter { it.value.expired } + logger.info("Found ${ratelimits.size} ratelimits to purge.") + + for (key in ratelimits.keys) { + // Remove it from Redis and in-memory + redis.commands.hdel("nino:ratelimits", key).await() + cachedRatelimits.remove(key) + } + } + + // https://github.com/go-chi/httprate/blob/master/httprate.go#L25-L47 + fun getRealHost(call: ApplicationCall): String { + val headers = call.request.headers + + val ip: String + if (headers.contains("True-Client-IP")) { + ip = headers["True-Client-IP"]!! + } else if (headers.contains("X-Real-IP")) { + ip = headers["X-Real-IP"]!! + } else if (headers.contains(HttpHeaders.XForwardedFor)) { + var index = headers[HttpHeaders.XForwardedFor]!!.indexOf(", ") + if (index != -1) { + index = headers[HttpHeaders.XForwardedFor]!!.length + } + + ip = headers[HttpHeaders.XForwardedFor]!!.slice(0..index) + } else { + ip = call.request.origin.remoteHost + } + + return ip + } + + suspend fun get(call: ApplicationCall): Ratelimit { + val ip = getRealHost(call) + logger.debug("ip: $ip") + + val result: String? = redis.commands.hget("nino:ratelimits", ip).await() + if (result == null) { + val r = Ratelimit() + + cachedRatelimits[ip] = r + redis.commands.hmset( + "nino:ratelimits", + mapOf( + ip to json.encodeToString(Ratelimit.serializer(), r) + ) + ) + + return r + } + + val ratelimit = json.decodeFromString(Ratelimit.serializer(), result) + val newRl = ratelimit.consume() + + redis.commands.hmset( + "nino:ratelimits", + mapOf( + ip to json.encodeToString(Ratelimit.serializer(), newRl) + ) + ) + + cachedRatelimits[ip] = newRl + return newRl + } + + @Suppress("UNCHECKED_CAST") + suspend fun close() { + logger.warn("Told to close off ratelimiter!") + + // weird compiler error that i have to cast this + // but whatever... + val mapped = cachedRatelimits.toMap() as Map + + // redo cache + val newMap = mutableMapOf() + for ((key, value) in mapped) { + newMap[key] = json.encodeToString(Ratelimit.serializer(), value as Ratelimit) + } + + if (newMap.isNotEmpty()) { + redis.commands.hmset("nino:ratelimits", newMap).await() + } + } +} diff --git a/bot/api/src/main/kotlin/sh/nino/discord/api/middleware/ratelimiting/Ratelimiting.kt b/bot/api/src/main/kotlin/sh/nino/discord/api/middleware/ratelimiting/Ratelimiting.kt new file mode 100644 index 00000000..ab558d9f --- /dev/null +++ b/bot/api/src/main/kotlin/sh/nino/discord/api/middleware/ratelimiting/Ratelimiting.kt @@ -0,0 +1,76 @@ +/* + * Copyright (c) 2019-2022 Nino + * + * 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. + */ + +package sh.nino.discord.api.middleware.ratelimiting + +import io.ktor.application.* +import io.ktor.features.* +import io.ktor.http.* +import io.ktor.response.* +import io.ktor.util.* +import kotlinx.datetime.Clock +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.JsonPrimitive + +class Ratelimiting(val ratelimiter: Ratelimiter) { + companion object: ApplicationFeature { + override val key: AttributeKey = AttributeKey("Ratelimiting") + override fun install(pipeline: ApplicationCallPipeline, configure: Unit.() -> Unit): Ratelimiting { + val ratelimiter = Ratelimiter() + pipeline.sendPipeline.intercept(ApplicationSendPipeline.After) { + val ip = this.call.request.origin.remoteHost + if (ip == "0:0:0:0:0:0:0:1") { + proceed() + return@intercept + } + + val record = ratelimiter.get(call) + context.response.header("X-Ratelimit-Limit", 1200) + context.response.header("X-Ratelimit-Remaining", record.remaining) + context.response.header("X-RateLimit-Reset", record.resetTime.toEpochMilliseconds()) + context.response.header("X-RateLimit-Reset-Date", record.resetTime.toString()) + + if (record.exceeded) { + val resetAfter = (record.resetTime.epochSeconds - Clock.System.now().epochSeconds).coerceAtLeast(0) + context.response.header(HttpHeaders.RetryAfter, resetAfter) + context.respondText(ContentType.Application.Json, HttpStatusCode.TooManyRequests) { + Json.encodeToString( + JsonObject.serializer(), + JsonObject( + mapOf( + "message" to JsonPrimitive("IP ${ratelimiter.getRealHost(call)} has been ratelimited.") + ) + ) + ) + } + + finish() + } else { + proceed() + } + } + + return Ratelimiting(ratelimiter) + } + } +} diff --git a/bot/api/src/main/kotlin/sh/nino/discord/api/routes/HealthRoute.kt b/bot/api/src/main/kotlin/sh/nino/discord/api/routes/HealthRoute.kt new file mode 100644 index 00000000..0e16c424 --- /dev/null +++ b/bot/api/src/main/kotlin/sh/nino/discord/api/routes/HealthRoute.kt @@ -0,0 +1,42 @@ +/* + * Copyright (c) 2019-2022 Nino + * + * 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. + */ + +@file:Suppress("UNUSED") +package sh.nino.discord.api.routes + +import io.ktor.application.* +import io.ktor.http.* +import io.ktor.response.* +import sh.nino.discord.api.Endpoint +import sh.nino.discord.api.annotations.Route + +class HealthRoute: Endpoint("/health") { + @Route(path = "/", method = "GET") + suspend fun health(call: ApplicationCall) { + call.respondText( + contentType = ContentType.Text.Plain, + status = HttpStatusCode.OK + ) { + "OK" + } + } +} diff --git a/bot/api/src/main/kotlin/sh/nino/discord/api/routes/MainRoute.kt b/bot/api/src/main/kotlin/sh/nino/discord/api/routes/MainRoute.kt new file mode 100644 index 00000000..1354013d --- /dev/null +++ b/bot/api/src/main/kotlin/sh/nino/discord/api/routes/MainRoute.kt @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2019-2022 Nino + * + * 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. + */ + +@file:Suppress("UNUSED") +package sh.nino.discord.api.routes + +import io.ktor.application.* +import io.ktor.http.* +import io.ktor.response.* +import sh.nino.discord.api.Endpoint +import sh.nino.discord.api.annotations.Route + +class MainRoute: Endpoint("/") { + @Route("/", method = "GET") + suspend fun owo(call: ApplicationCall) { + call.respondText("hewo world", status = HttpStatusCode.OK) + } +} diff --git a/src/listeners/UserListener.ts b/bot/api/src/main/kotlin/sh/nino/discord/api/routes/MetricsRoute.kt similarity index 54% rename from src/listeners/UserListener.ts rename to bot/api/src/main/kotlin/sh/nino/discord/api/routes/MetricsRoute.kt index 7db169a8..81dcadc9 100644 --- a/src/listeners/UserListener.ts +++ b/bot/api/src/main/kotlin/sh/nino/discord/api/routes/MetricsRoute.kt @@ -1,5 +1,5 @@ -/** - * Copyright (c) 2019-2021 Nino +/* + * Copyright (c) 2019-2022 Nino * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -20,31 +20,26 @@ * SOFTWARE. */ -import { Inject, Subscribe } from '@augu/lilith'; -import AutomodService from '../services/AutomodService'; -import type { User } from 'eris'; -import Database from '../components/Database'; -import Discord from '../components/Discord'; +@file:Suppress("UNUSED") +package sh.nino.discord.api.routes -export default class UserListener { - @Inject - private readonly database!: Database; +import io.ktor.application.* +import io.ktor.http.* +import io.ktor.response.* +import io.prometheus.client.exporter.common.TextFormat +import sh.nino.discord.api.Endpoint +import sh.nino.discord.api.annotations.Route +import sh.nino.discord.common.data.Config +import sh.nino.discord.metrics.MetricsRegistry - @Inject - private readonly discord!: Discord; +class MetricsRoute(private val config: Config, private val metrics: MetricsRegistry): Endpoint("/metrics") { + @Route("/", method = "GET") + suspend fun metrics(call: ApplicationCall) { + if (!config.metrics) + return call.respondText("Cannot GET /metrics", status = HttpStatusCode.NotFound) - @Inject - private readonly automod!: AutomodService; - - @Subscribe('userUpdate', { emitter: 'discord' }) - async onUserUpdate(user: User) { - const mutualGuilds = this.discord.client.guilds.filter((guild) => guild.members.has(user.id)); - - for (const guild of mutualGuilds) { - const automod = await this.database.automod.get(guild.id); - if (!automod) continue; - - if (automod.dehoist === true) await this.automod.run('memberNick', guild.members.get(user.id)!); + call.respondTextWriter(ContentType.parse(TextFormat.CONTENT_TYPE_004), HttpStatusCode.OK) { + TextFormat.write004(this, metrics.registry!!.metricFamilySamples()) + } } - } } diff --git a/bot/api/src/main/kotlin/sh/nino/discord/api/routes/_Module.kt b/bot/api/src/main/kotlin/sh/nino/discord/api/routes/_Module.kt new file mode 100644 index 00000000..1e0836d3 --- /dev/null +++ b/bot/api/src/main/kotlin/sh/nino/discord/api/routes/_Module.kt @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2019-2022 Nino + * + * 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. + */ + +package sh.nino.discord.api.routes + +import org.koin.dsl.bind +import org.koin.dsl.module +import sh.nino.discord.api.Endpoint +import sh.nino.discord.api.routes.api.ApiRoute +import sh.nino.discord.api.routes.api.AutomodRoute + +val endpointModule = module { + single { HealthRoute() } bind Endpoint::class + single { MetricsRoute(get(), get()) } bind Endpoint::class + single { MainRoute() } bind Endpoint::class + single { ApiRoute() } bind Endpoint::class + single { AutomodRoute() } bind Endpoint::class +} diff --git a/bot/api/src/main/kotlin/sh/nino/discord/api/routes/api/ApiRoute.kt b/bot/api/src/main/kotlin/sh/nino/discord/api/routes/api/ApiRoute.kt new file mode 100644 index 00000000..346f50b5 --- /dev/null +++ b/bot/api/src/main/kotlin/sh/nino/discord/api/routes/api/ApiRoute.kt @@ -0,0 +1,58 @@ +/* + * Copyright (c) 2019-2022 Nino + * + * 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. + */ + +@file:Suppress("UNUSED") +package sh.nino.discord.api.routes.api + +import io.ktor.application.* +import io.ktor.http.* +import io.ktor.response.* +import kotlinx.serialization.Serializable +import sh.nino.discord.api.Endpoint +import sh.nino.discord.api.annotations.Route + +@Serializable +data class ApiResponse( + val message: String +) + +class ApiRoute: Endpoint("/api") { + @Route("/", "get") + suspend fun main(call: ApplicationCall) { + call.respond( + HttpStatusCode.OK, + ApiResponse( + message = "hello world!!!!!!!" + ) + ) + } + + @Route("/v1", "get") + suspend fun mainV1(call: ApplicationCall) { + call.respond( + HttpStatusCode.OK, + ApiResponse( + message = "hello world!!!!!!!" + ) + ) + } +} diff --git a/bot/api/src/main/kotlin/sh/nino/discord/api/routes/api/AutomodRoute.kt b/bot/api/src/main/kotlin/sh/nino/discord/api/routes/api/AutomodRoute.kt new file mode 100644 index 00000000..412ad9fb --- /dev/null +++ b/bot/api/src/main/kotlin/sh/nino/discord/api/routes/api/AutomodRoute.kt @@ -0,0 +1,113 @@ +/* + * Copyright (c) 2019-2022 Nino + * + * 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. + */ + +@file:Suppress("UNUSED") +package sh.nino.discord.api.routes.api + +import io.ktor.application.* +import io.ktor.http.* +import io.ktor.response.* +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable +import sh.nino.discord.api.Endpoint +import sh.nino.discord.api.annotations.Route +import sh.nino.discord.database.asyncTransaction +import sh.nino.discord.database.tables.AutomodEntity + +@Serializable +data class AutomodData( + @SerialName("account_age_day_threshold") + val accountAgeDayThreshold: Int, + + @SerialName("mentions_threshold") + val mentionsThreshold: Int, + + @SerialName("omitted_channels") + val omittedChannels: List, + + @SerialName("omitted_users") + val omittedUsers: List, + + @SerialName("account_age") + val accountAge: Boolean, + val dehoisting: Boolean, + val shortlinks: Boolean, + val blacklist: Boolean, + val toxicity: Boolean, + val phishing: Boolean, + val mentions: Boolean, + val invites: Boolean, + val spam: Boolean, + val raid: Boolean +) { + companion object { + fun fromEntity(entity: AutomodEntity): AutomodData = AutomodData( + entity.accountAgeDayThreshold, + entity.mentionThreshold, + entity.omittedChannels.toList(), + entity.omittedUsers.toList(), + entity.accountAge, + entity.dehoisting, + entity.shortlinks, + entity.blacklist, + entity.toxicity, + entity.phishing, + entity.mentions, + entity.invites, + entity.spam, + entity.raid + ) + } +} + +class AutomodRoute: Endpoint("/api/v1/automod") { + @Route("/", "get") + suspend fun get(call: ApplicationCall) { + call.respond( + HttpStatusCode.NotFound, + ApiResponse( + message = "Cannot GET /api/v1/automod - missing guild id as key." + ) + ) + } + + @Route("/{guildId}", "get") + suspend fun getFromGuild(call: ApplicationCall) { + val guildId = call.parameters["guildId"] + val entity = asyncTransaction { + AutomodEntity.findById(guildId!!.toLong()) + } + + if (entity == null) { + call.respond( + HttpStatusCode.NotFound, + ApiResponse( + message = "Cannot find automod settings for guild '$guildId'" + ) + ) + + return + } + + return call.respond(HttpStatusCode.OK, AutomodData.fromEntity(entity)) + } +} diff --git a/bot/api/src/main/kotlin/sh/nino/discord/api/routes/api/CasesRoute.kt b/bot/api/src/main/kotlin/sh/nino/discord/api/routes/api/CasesRoute.kt new file mode 100644 index 00000000..a420f891 --- /dev/null +++ b/bot/api/src/main/kotlin/sh/nino/discord/api/routes/api/CasesRoute.kt @@ -0,0 +1,23 @@ +/* + * Copyright (c) 2019-2022 Nino + * + * 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. + */ + +package sh.nino.discord.api.routes.api diff --git a/bot/api/src/main/kotlin/sh/nino/discord/api/routes/api/GuildsRoute.kt b/bot/api/src/main/kotlin/sh/nino/discord/api/routes/api/GuildsRoute.kt new file mode 100644 index 00000000..a420f891 --- /dev/null +++ b/bot/api/src/main/kotlin/sh/nino/discord/api/routes/api/GuildsRoute.kt @@ -0,0 +1,23 @@ +/* + * Copyright (c) 2019-2022 Nino + * + * 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. + */ + +package sh.nino.discord.api.routes.api diff --git a/bot/api/src/main/kotlin/sh/nino/discord/api/routes/api/LoggingRoute.kt b/bot/api/src/main/kotlin/sh/nino/discord/api/routes/api/LoggingRoute.kt new file mode 100644 index 00000000..a420f891 --- /dev/null +++ b/bot/api/src/main/kotlin/sh/nino/discord/api/routes/api/LoggingRoute.kt @@ -0,0 +1,23 @@ +/* + * Copyright (c) 2019-2022 Nino + * + * 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. + */ + +package sh.nino.discord.api.routes.api diff --git a/bot/api/src/main/kotlin/sh/nino/discord/api/routes/api/PunishmentsRoute.kt b/bot/api/src/main/kotlin/sh/nino/discord/api/routes/api/PunishmentsRoute.kt new file mode 100644 index 00000000..a420f891 --- /dev/null +++ b/bot/api/src/main/kotlin/sh/nino/discord/api/routes/api/PunishmentsRoute.kt @@ -0,0 +1,23 @@ +/* + * Copyright (c) 2019-2022 Nino + * + * 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. + */ + +package sh.nino.discord.api.routes.api diff --git a/bot/api/src/main/kotlin/sh/nino/discord/api/routes/api/UsersRoute.kt b/bot/api/src/main/kotlin/sh/nino/discord/api/routes/api/UsersRoute.kt new file mode 100644 index 00000000..a420f891 --- /dev/null +++ b/bot/api/src/main/kotlin/sh/nino/discord/api/routes/api/UsersRoute.kt @@ -0,0 +1,23 @@ +/* + * Copyright (c) 2019-2022 Nino + * + * 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. + */ + +package sh.nino.discord.api.routes.api diff --git a/bot/api/src/main/kotlin/sh/nino/discord/api/routes/api/WarningsRoute.kt b/bot/api/src/main/kotlin/sh/nino/discord/api/routes/api/WarningsRoute.kt new file mode 100644 index 00000000..a420f891 --- /dev/null +++ b/bot/api/src/main/kotlin/sh/nino/discord/api/routes/api/WarningsRoute.kt @@ -0,0 +1,23 @@ +/* + * Copyright (c) 2019-2022 Nino + * + * 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. + */ + +package sh.nino.discord.api.routes.api diff --git a/bot/api/src/test/kotlin/EndpointTests.kt b/bot/api/src/test/kotlin/EndpointTests.kt new file mode 100644 index 00000000..c559b8c7 --- /dev/null +++ b/bot/api/src/test/kotlin/EndpointTests.kt @@ -0,0 +1,44 @@ +/* + * Copyright (c) 2019-2022 Nino + * + * 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. + */ + +package sh.nino.api.tests + +import io.kotest.core.spec.style.DescribeSpec +import io.kotest.matchers.shouldBe +import sh.nino.discord.api.Endpoint + +class EndpointTests: DescribeSpec({ + describe("Endpoint") { + it("should be equal to \"/\"") { + val path = Endpoint.merge("/", "/") + path shouldBe "/" + } + + it("should be equal to \"/owo\" if the prefix is /owo and the path is /") { + Endpoint.merge("/owo", "/") shouldBe "/owo" + } + + it("should be equal to \"/owo/uwu\" if prefix is /owo and the path is /uwu") { + Endpoint.merge("/owo", "/uwu") shouldBe "/owo/uwu" + } + } +}) diff --git a/src/singletons/Http.ts b/bot/automod/build.gradle.kts similarity index 82% rename from src/singletons/Http.ts rename to bot/automod/build.gradle.kts index 64eeae72..619e7ad3 100644 --- a/src/singletons/Http.ts +++ b/bot/automod/build.gradle.kts @@ -1,5 +1,5 @@ /** - * Copyright (c) 2019-2021 Nino + * Copyright (c) 2019-2022 Nino * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -20,9 +20,11 @@ * SOFTWARE. */ -import { HttpClient } from '@augu/orchid'; -import { version } from '../util/Constants'; +plugins { + `nino-module` +} -export default new HttpClient({ - userAgent: `Nino (v${version}, https://github.com/NinoDiscord/Nino)`, -}); +dependencies { + implementation(project(":bot:punishments")) + implementation(project(":bot:database")) +} diff --git a/bot/automod/src/main/kotlin/sh/nino/discord/automod/AccountAgeAutomod.kt b/bot/automod/src/main/kotlin/sh/nino/discord/automod/AccountAgeAutomod.kt new file mode 100644 index 00000000..6bb2edea --- /dev/null +++ b/bot/automod/src/main/kotlin/sh/nino/discord/automod/AccountAgeAutomod.kt @@ -0,0 +1,68 @@ +/* + * Copyright (c) 2019-2022 Nino + * + * 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. + */ + +package sh.nino.discord.automod + +import dev.kord.core.Kord +import kotlinx.datetime.toJavaInstant +import org.koin.core.context.GlobalContext +import sh.nino.discord.automod.core.automod +import sh.nino.discord.common.extensions.retrieve +import sh.nino.discord.database.asyncTransaction +import sh.nino.discord.database.tables.AutomodEntity +import sh.nino.discord.database.tables.PunishmentType +import sh.nino.discord.punishments.MemberLike +import sh.nino.discord.punishments.PunishmentModule +import java.time.OffsetDateTime +import java.time.temporal.ChronoUnit + +val accountAgeAutomod = automod { + name = "accountAge" + onMemberJoin { event -> + val settings = asyncTransaction { + AutomodEntity.findById(event.guild.id.value.toLong())!! + } + + if (!settings.accountAge) + return@onMemberJoin false + + val totalDays = ChronoUnit.DAYS.between(event.member.joinedAt.toJavaInstant(), OffsetDateTime.now().toLocalDate()) + if (totalDays <= settings.accountAgeDayThreshold) { + val punishments = GlobalContext.retrieve() + val kord = GlobalContext.retrieve() + val guild = event.getGuild() + val selfMember = guild.getMember(kord.selfId) + + punishments.apply( + MemberLike(event.member, event.getGuild(), event.member.id), + selfMember, + PunishmentType.KICK + ) { + reason = "[Automod] Account threshold for member was under ${settings.accountAgeDayThreshold} days." + } + + return@onMemberJoin true + } + + false + } +} diff --git a/bot/automod/src/main/kotlin/sh/nino/discord/automod/BlacklistAutomod.kt b/bot/automod/src/main/kotlin/sh/nino/discord/automod/BlacklistAutomod.kt new file mode 100644 index 00000000..2352b8fc --- /dev/null +++ b/bot/automod/src/main/kotlin/sh/nino/discord/automod/BlacklistAutomod.kt @@ -0,0 +1,58 @@ +/* + * Copyright (c) 2019-2022 Nino + * + * 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. + */ + +package sh.nino.discord.automod + +import org.koin.core.context.GlobalContext +import sh.nino.discord.automod.core.automod +import sh.nino.discord.common.extensions.retrieve +import sh.nino.discord.database.asyncTransaction +import sh.nino.discord.database.tables.AutomodEntity +import sh.nino.discord.punishments.PunishmentModule + +val blacklistAutomod = automod { + name = "blacklist" + onMessage { event -> + val guild = event.message.getGuild() + val settings = asyncTransaction { + AutomodEntity.findById(guild.id.value.toLong())!! + } + + if (!settings.blacklist) + return@onMessage false + + val content = event.message.content.split(" ") + for (word in settings.blacklistedWords) { + if (content.any { it.lowercase() == word.lowercase() }) { + event.message.delete() + event.message.channel.createMessage("Hey! You are not allowed to say that here! qwq") + + val punishments = GlobalContext.retrieve() + punishments.addWarning(event.member!!, guild.getMember(event.kord.selfId), "[Automod] User said a blacklisted word in their message. qwq") + + return@onMessage true + } + } + + false + } +} diff --git a/bot/automod/src/main/kotlin/sh/nino/discord/automod/MentionsAutomod.kt b/bot/automod/src/main/kotlin/sh/nino/discord/automod/MentionsAutomod.kt new file mode 100644 index 00000000..db29897c --- /dev/null +++ b/bot/automod/src/main/kotlin/sh/nino/discord/automod/MentionsAutomod.kt @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2019-2022 Nino + * + * 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. + */ + +package sh.nino.discord.automod + +import sh.nino.discord.automod.core.automod + +val mentionsAutomod = automod { + name = "mentions" + onMessage { + true + } +} diff --git a/bot/automod/src/main/kotlin/sh/nino/discord/automod/MessageLinksAutomod.kt b/bot/automod/src/main/kotlin/sh/nino/discord/automod/MessageLinksAutomod.kt new file mode 100644 index 00000000..f879244e --- /dev/null +++ b/bot/automod/src/main/kotlin/sh/nino/discord/automod/MessageLinksAutomod.kt @@ -0,0 +1,172 @@ +/* + * Copyright (c) 2019-2022 Nino + * + * 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. + */ + +package sh.nino.discord.automod + +import dev.kord.common.Color +import dev.kord.common.entity.ChannelType +import dev.kord.common.entity.optional.value +import dev.kord.core.behavior.channel.createMessage +import dev.kord.core.entity.Message +import dev.kord.core.entity.channel.NewsChannel +import dev.kord.core.entity.channel.TextChannel +import dev.kord.rest.builder.message.EmbedBuilder +import kotlinx.datetime.Instant +import sh.nino.discord.automod.core.automod +import sh.nino.discord.common.COLOR +import sh.nino.discord.common.extensions.asSnowflake + +private val DISCORD_MESSAGE_LINK_REGEX = "(?:https?:\\/\\/)?(?:canary\\.|ptb\\.)?discord\\.com\\/channels\\/(\\d{15,21}|@me)\\/(\\d{15,21})\\/(\\d{15,21})\n".toRegex() + +val messageLinksAutomod = automod { + name = "messageLinks" + onMessage { event -> + if (event.message.content.matches(DISCORD_MESSAGE_LINK_REGEX)) { + val matcher = DISCORD_MESSAGE_LINK_REGEX.toPattern().matcher(event.message.content) + if (!matcher.matches()) return@onMessage false + + val channelId = matcher.group(4).asSnowflake() + val messageId = matcher.group(5).asSnowflake() + + // can we find the channel? + val channel = event.kord.getChannel(channelId) ?: return@onMessage false + + // Try to surf through the channel types and see + // if we can grab the messages. + val message: Message = when (channel.type) { + is ChannelType.GuildText -> { + try { + (channel as TextChannel).getMessage(messageId) + } catch (e: Exception) { + null + } + } + + is ChannelType.GuildNews -> { + try { + (channel as NewsChannel).getMessage(messageId) + } catch (e: Exception) { + null + } + } + + else -> null + } ?: return@onMessage false + + if (message.embeds.isNotEmpty()) { + val first = message.embeds.first() + val member = message.getAuthorAsMember() + + event.message.channel.createMessage { + embeds += EmbedBuilder().apply { + if (first.data.title.value != null) { + title = first.data.title.value + } + + if (first.data.description.value != null) { + description = first.data.description.value + } + + if (first.data.url.value != null) { + url = first.data.url.value + } + + color = if (first.data.color.asNullable != null) { + Color(first.data.color.asOptional.value!!) + } else { + COLOR + } + + if (first.data.timestamp.value != null) { + timestamp = Instant.parse(first.data.timestamp.value!!) + } + + if (first.data.footer.value != null) { + footer { + text = first.data.footer.value!!.text + icon = first.data.footer.value!!.iconUrl.value ?: first.data.footer.value!!.proxyIconUrl.value ?: "" + } + } + + if (first.data.thumbnail.value != null) { + thumbnail { + url = first.data.thumbnail.value!!.url.value ?: first.data.thumbnail.value!!.proxyUrl.value ?: "" + } + } + + if (first.data.author.value != null) { + author { + name = first.data.author.value!!.name.value ?: "" + icon = first.data.author.value!!.iconUrl.value ?: first.data.author.value!!.proxyIconUrl.value ?: "" + url = first.data.author.value!!.url.value ?: "" + } + } else { + author { + name = if (message.author == null) { + "Webhook" + } else { + "${message.author!!.tag} (${message.author!!.id})" + } + + icon = member?.avatar?.url ?: message.author!!.avatar?.url ?: message.author!!.defaultAvatar.url + } + } + + if (first.data.fields.value != null) { + for (f in first.data.fields.value!!) { + field { + name = f.name + value = f.value + inline = f.inline.value ?: true + } + } + } + } + } + + return@onMessage true + } else { + val member = message.getAuthorAsMember() + event.message.channel.createMessage { + embeds += EmbedBuilder().apply { + description = message.content + color = COLOR + + author { + name = if (message.author == null) { + "Webhook" + } else { + "${message.author!!.tag} (${message.author!!.id})" + } + + icon = member?.avatar?.url ?: message.author!!.avatar?.url ?: message.author!!.defaultAvatar.url + } + } + } + + return@onMessage true + } + } + + false + } +} diff --git a/bot/automod/src/main/kotlin/sh/nino/discord/automod/PhishingAutomod.kt b/bot/automod/src/main/kotlin/sh/nino/discord/automod/PhishingAutomod.kt new file mode 100644 index 00000000..8e104088 --- /dev/null +++ b/bot/automod/src/main/kotlin/sh/nino/discord/automod/PhishingAutomod.kt @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2019-2022 Nino + * + * 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. + */ + +package sh.nino.discord.automod + +import sh.nino.discord.automod.core.automod + +val phishingAutomod = automod { + name = "phishing" + onMessage { + true + } +} diff --git a/bot/automod/src/main/kotlin/sh/nino/discord/automod/RaidAutomod.kt b/bot/automod/src/main/kotlin/sh/nino/discord/automod/RaidAutomod.kt new file mode 100644 index 00000000..4c0d4b07 --- /dev/null +++ b/bot/automod/src/main/kotlin/sh/nino/discord/automod/RaidAutomod.kt @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2019-2022 Nino + * + * 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. + */ + +package sh.nino.discord.automod + +import sh.nino.discord.automod.core.automod + +val raidAutomod = automod { + name = "raid" + onMessage { + true + } +} diff --git a/bot/automod/src/main/kotlin/sh/nino/discord/automod/ShortlinksAutomod.kt b/bot/automod/src/main/kotlin/sh/nino/discord/automod/ShortlinksAutomod.kt new file mode 100644 index 00000000..23e8f3ae --- /dev/null +++ b/bot/automod/src/main/kotlin/sh/nino/discord/automod/ShortlinksAutomod.kt @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2019-2022 Nino + * + * 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. + */ + +package sh.nino.discord.automod + +import sh.nino.discord.automod.core.automod + +val shortlinksAutomod = automod { + name = "shortlinks" + onMessage { + true + } +} diff --git a/bot/automod/src/main/kotlin/sh/nino/discord/automod/SpamAutomod.kt b/bot/automod/src/main/kotlin/sh/nino/discord/automod/SpamAutomod.kt new file mode 100644 index 00000000..d26242af --- /dev/null +++ b/bot/automod/src/main/kotlin/sh/nino/discord/automod/SpamAutomod.kt @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2019-2022 Nino + * + * 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. + */ + +package sh.nino.discord.automod + +import sh.nino.discord.automod.core.automod + +val spamAutomod = automod { + name = "spam" + onMessage { + true + } +} diff --git a/bot/automod/src/main/kotlin/sh/nino/discord/automod/ToxicityAutomod.kt b/bot/automod/src/main/kotlin/sh/nino/discord/automod/ToxicityAutomod.kt new file mode 100644 index 00000000..75e8e4ff --- /dev/null +++ b/bot/automod/src/main/kotlin/sh/nino/discord/automod/ToxicityAutomod.kt @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2019-2022 Nino + * + * 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. + */ + +package sh.nino.discord.automod + +import sh.nino.discord.automod.core.automod + +val toxicityAutomod = automod { + name = "toxicity" + onMessage { + true + } +} diff --git a/bot/automod/src/main/kotlin/sh/nino/discord/automod/core/Automod.kt b/bot/automod/src/main/kotlin/sh/nino/discord/automod/core/Automod.kt new file mode 100644 index 00000000..4ed8fa3f --- /dev/null +++ b/bot/automod/src/main/kotlin/sh/nino/discord/automod/core/Automod.kt @@ -0,0 +1,100 @@ +/* + * Copyright (c) 2019-2022 Nino + * + * 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. + */ + +package sh.nino.discord.automod.core + +import dev.kord.common.entity.Permission +import dev.kord.core.Kord +import dev.kord.core.entity.channel.TextChannel +import dev.kord.core.event.guild.MemberJoinEvent +import dev.kord.core.event.guild.MemberUpdateEvent +import dev.kord.core.event.message.MessageCreateEvent +import dev.kord.core.event.user.UserUpdateEvent +import org.koin.core.context.GlobalContext +import sh.nino.discord.common.extensions.retrieve +import sh.nino.discord.common.isMemberAbove +import kotlin.contracts.ExperimentalContracts +import kotlin.contracts.InvocationKind +import kotlin.contracts.contract + +@OptIn(ExperimentalContracts::class) +fun automod(builder: AutomodBuilder.() -> Unit): Automod { + contract { + callsInPlace(builder, InvocationKind.EXACTLY_ONCE) + } + + val obj = AutomodBuilder().apply(builder) + return obj.build() +} + +class Automod( + val name: String, + private val onMessageCall: AutomodCallable?, + private val onUserUpdateCall: AutomodCallable?, + private val onMemberJoinCall: AutomodCallable?, + private val onMemberNickUpdateCall: AutomodCallable? +) { + init { + require(name != "") { "Name cannot be empty." } + } + + // Why is `event` dynamic? + // So you can pass in any event-driven class from Kord, + // and the `execute` function will cast the [event] + // so its corresponding event or else it'll fail. + suspend fun execute(event: Any): Boolean = when { + onMessageCall != null -> { + val ev = event as? MessageCreateEvent ?: error("Unable to cast ${event::class} -> MessageCreateEvent") + val guild = event.getGuild()!! + val kord = GlobalContext.retrieve() + val channel = event.message.getChannel() as? TextChannel + + if ( + (event.member != null && !isMemberAbove(guild.getMember(kord.selfId), event.member!!)) || + (channel != null && channel.getEffectivePermissions(kord.selfId).contains(Permission.ManageMessages)) || + (event.message.author == null || event.message.author!!.isBot) || + (channel != null && channel.getEffectivePermissions(event.message.author!!.id).contains(Permission.BanMembers)) + ) { + false + } else { + onMessageCall.invoke(ev) + } + } + + onUserUpdateCall != null -> { + val ev = event as? UserUpdateEvent ?: error("Unable to cast ${event::class} -> UserUpdateEvent") + onUserUpdateCall.invoke(ev) + } + + onMemberJoinCall != null -> { + val ev = event as? MemberJoinEvent ?: error("Unable to cast ${event::class} -> MemberJoinEvent") + onMemberJoinCall.invoke(ev) + } + + onMemberNickUpdateCall != null -> { + val ev = event as? MemberUpdateEvent ?: error("Unable to cast ${event::class} -> MemberUpdateEvent") + onMemberNickUpdateCall.invoke(ev) + } + + else -> error("Automod $name doesn't implement any automod callables. (Used event ${event::class})") + } +} diff --git a/bot/automod/src/main/kotlin/sh/nino/discord/automod/core/Builder.kt b/bot/automod/src/main/kotlin/sh/nino/discord/automod/core/Builder.kt new file mode 100644 index 00000000..f365219c --- /dev/null +++ b/bot/automod/src/main/kotlin/sh/nino/discord/automod/core/Builder.kt @@ -0,0 +1,73 @@ +/* + * Copyright (c) 2019-2022 Nino + * + * 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. + */ + +package sh.nino.discord.automod.core + +import dev.kord.core.event.guild.MemberJoinEvent +import dev.kord.core.event.guild.MemberUpdateEvent +import dev.kord.core.event.message.MessageCreateEvent +import dev.kord.core.event.user.UserUpdateEvent + +typealias AutomodCallable = suspend (T) -> Boolean + +/** + * Represents a builder class for constructing automod objects. + */ +class AutomodBuilder { + private var onMemberNickUpdateCall: AutomodCallable? = null + private var onMemberJoinCall: AutomodCallable? = null + private var onUserUpdateCall: AutomodCallable? = null + private var onMessageCall: AutomodCallable? = null + + /** + * Returns the name of the automod. + */ + var name: String = "" + + /** + * Hooks this [Automod] object to react on message create events + * @param callable The callable function to execute + */ + fun onMessage(callable: AutomodCallable) { + onMessageCall = callable + } + + fun onUserUpdate(callable: AutomodCallable) { + onUserUpdateCall = callable + } + + fun onMemberJoin(callable: AutomodCallable) { + onMemberJoinCall = callable + } + + fun onMemberNickUpdate(callable: AutomodCallable) { + onMemberNickUpdateCall = callable + } + + fun build(): Automod = Automod( + this.name, + onMessageCall, + onUserUpdateCall, + onMemberJoinCall, + onMemberNickUpdateCall + ) +} diff --git a/bot/automod/src/main/kotlin/sh/nino/discord/automod/core/Container.kt b/bot/automod/src/main/kotlin/sh/nino/discord/automod/core/Container.kt new file mode 100644 index 00000000..a15a6e46 --- /dev/null +++ b/bot/automod/src/main/kotlin/sh/nino/discord/automod/core/Container.kt @@ -0,0 +1,53 @@ +/* + * Copyright (c) 2019-2022 Nino + * + * 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. + */ + +package sh.nino.discord.automod.core + +import dev.kord.core.event.Event +import sh.nino.discord.automod.* + +object Container { + private val automods = mapOf( + "accountAge" to accountAgeAutomod, + "blacklist" to blacklistAutomod, + "mentions" to mentionsAutomod, + "messageLinks" to messageLinksAutomod, + "phishing" to phishingAutomod, + "raid" to raidAutomod, + "shortlinks" to shortlinksAutomod, + "spam" to spamAutomod, + "toxicity" to toxicityAutomod + ) + + suspend fun execute(event: Event): Boolean { + var ret = false + for (auto in automods.values) { + try { + ret = auto.execute(event) + } catch (e: Exception) { + continue + } + } + + return ret + } +} diff --git a/bot/build.gradle.kts b/bot/build.gradle.kts new file mode 100644 index 00000000..98015b77 --- /dev/null +++ b/bot/build.gradle.kts @@ -0,0 +1,104 @@ +/** + * Copyright (c) 2019-2022 Nino + * + * 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. + */ + +import java.text.SimpleDateFormat +import java.util.Date + +plugins { + `nino-module` + application +} + +val commitHash by lazy { + val cmd = "git rev-parse --short HEAD".split("\\s".toRegex()) + val proc = ProcessBuilder(cmd) + .directory(File(".")) + .redirectOutput(ProcessBuilder.Redirect.PIPE) + .redirectError(ProcessBuilder.Redirect.PIPE) + .start() + + proc.waitFor(1, TimeUnit.MINUTES) + proc.inputStream.bufferedReader().readText().trim() +} + +distributions { + main { + distributionBaseName.set("Nino") + } +} + +dependencies { + runtimeOnly(kotlin("scripting-jsr223")) + + // Nino libraries + projects + implementation(project(":bot:automod")) + implementation(project(":bot:commands")) + implementation(project(":bot:core")) + implementation(project(":bot:punishments")) + implementation(project(":bot:api")) + implementation(project(":bot:database")) + + // Logging + implementation("ch.qos.logback:logback-classic:1.2.10") + implementation("ch.qos.logback:logback-core:1.2.10") + + // YAML (configuration) + implementation("com.charleskorn.kaml:kaml:0.40.0") + + // Kord cache + implementation("dev.kord.cache:cache-redis:0.3.0") + api("dev.kord.cache:cache-api:0.3.0") + + // Logstash encoder for Logback + implementation("net.logstash.logback:logstash-logback-encoder:7.0.1") +} + +application { + mainClass.set("sh.nino.discord.Bootstrap") +} + +tasks { + processResources { + filesMatching("build-info.json") { + val date = Date() + val formatter = SimpleDateFormat("EEE, MMM d, YYYY - HH:mm:ss a") + + expand( + mapOf( + "version" to rootProject.version, + "commitSha" to commitHash, + "buildDate" to formatter.format(date) + ) + ) + } + } + + build { + dependsOn(spotlessApply) + dependsOn(kotest) + dependsOn(processResources) + } + + run { + + } +} diff --git a/src/commands/core/AboutCommand.ts b/bot/commands/build.gradle.kts similarity index 78% rename from src/commands/core/AboutCommand.ts rename to bot/commands/build.gradle.kts index b2f388d7..f7037bc8 100644 --- a/src/commands/core/AboutCommand.ts +++ b/bot/commands/build.gradle.kts @@ -1,5 +1,5 @@ /** - * Copyright (c) 2019-2021 Nino + * Copyright (c) 2019-2022 Nino * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -20,14 +20,12 @@ * SOFTWARE. */ -import { Command, CommandInfo, CommandMessage } from '../../structures'; +plugins { + `nino-module` +} -@CommandInfo({ - description: 'descriptions.help', - name: 'about', -}) -export default class AboutCommand extends Command { - override run(msg: CommandMessage) { - return msg.reply('heck'); - } +dependencies { + implementation(project(":bot:automod")) + implementation(project(":bot:database")) + implementation(project(":bot:core")) } diff --git a/bot/commands/src/main/kotlin/sh/nino/discord/commands/AbstractCommand.kt b/bot/commands/src/main/kotlin/sh/nino/discord/commands/AbstractCommand.kt new file mode 100644 index 00000000..44f57b7d --- /dev/null +++ b/bot/commands/src/main/kotlin/sh/nino/discord/commands/AbstractCommand.kt @@ -0,0 +1,45 @@ +/* + * Copyright (c) 2019-2022 Nino + * + * 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. + */ + +package sh.nino.discord.commands + +import sh.nino.discord.commands.annotations.Command +import kotlin.reflect.full.findAnnotation +import kotlin.reflect.full.hasAnnotation +import kotlin.reflect.jvm.jvmName +import sh.nino.discord.commands.annotations.Subcommand as SubcommandAnnotation + +abstract class AbstractCommand { + val info: Command + get() = this::class.findAnnotation() ?: error("Missing @Command annotation on ${this::class.simpleName ?: this::class.jvmName}") + + val subcommands: List + get() = this::class.members.filter { it.hasAnnotation() }.map { + Subcommand( + it, + it.findAnnotation()!!, + this@AbstractCommand + ) + } + + abstract suspend fun execute(msg: CommandMessage) +} diff --git a/bot/commands/src/main/kotlin/sh/nino/discord/commands/Command.kt b/bot/commands/src/main/kotlin/sh/nino/discord/commands/Command.kt new file mode 100644 index 00000000..23a0ae59 --- /dev/null +++ b/bot/commands/src/main/kotlin/sh/nino/discord/commands/Command.kt @@ -0,0 +1,63 @@ +/* + * Copyright (c) 2019-2022 Nino + * + * 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. + */ + +package sh.nino.discord.commands + +import dev.kord.common.DiscordBitSet +import dev.kord.common.entity.Permissions + +class Command private constructor( + val name: String, + val description: String, + val category: CommandCategory = CommandCategory.CORE, + val usage: String = "", + val ownerOnly: Boolean = false, + val aliases: List = listOf(), + val examples: List = listOf(), + val cooldown: Int = 5, + val userPermissions: Permissions = Permissions(), + val botPermissions: Permissions = Permissions(), + val thiz: AbstractCommand +) { + constructor(thiz: AbstractCommand): this( + thiz.info.name, + thiz.info.description, + thiz.info.category, + thiz.info.usage, + thiz.info.ownerOnly, + thiz.info.aliases.toList(), + thiz.info.examples.toList(), + thiz.info.cooldown, + Permissions(DiscordBitSet(thiz.info.userPermissions)), + Permissions(DiscordBitSet(thiz.info.botPermissions)), + thiz + ) + + suspend fun run(msg: CommandMessage, callback: suspend (Exception?, Boolean) -> Unit) { + try { + thiz.execute(msg) + callback(null, true) + } catch (e: Exception) { + callback(e, false) + } + } +} diff --git a/src/commands/core/UptimeCommand.ts b/bot/commands/src/main/kotlin/sh/nino/discord/commands/CommandCategory.kt similarity index 68% rename from src/commands/core/UptimeCommand.ts rename to bot/commands/src/main/kotlin/sh/nino/discord/commands/CommandCategory.kt index f860c190..46c9770a 100644 --- a/src/commands/core/UptimeCommand.ts +++ b/bot/commands/src/main/kotlin/sh/nino/discord/commands/CommandCategory.kt @@ -1,5 +1,5 @@ -/** - * Copyright (c) 2019-2021 Nino +/* + * Copyright (c) 2019-2022 Nino * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -20,20 +20,14 @@ * SOFTWARE. */ -import { Command, CommandMessage } from '../../structures'; -import { humanize } from '@augu/utils'; +package sh.nino.discord.commands -export default class UptimeCommand extends Command { - constructor() { - super({ - description: 'descriptions.uptime', - cooldown: 3, - aliases: ['up', 'upfor', 'online'], - name: 'uptime', - }); - } - - run(msg: CommandMessage) { - return msg.reply(humanize(Math.floor(process.uptime() * 1000), true)); - } +enum class CommandCategory(val emoji: String, val key: String, val localeKey: String = "") { + ADMIN("⚒️", "Administration", "admin"), + CORE("ℹ", "Core", "core"), + EASTER_EGG("", "Easter Egg"), + MODERATION("\uD83D\uDD28", "Moderation", "moderation"), + SYSTEM("", "System Administration"), + THREADS("\uD83E\uDDF5", "Channel Thread Moderation", "thread"), + VOICE("\uD83D\uDD08", "Voice Channel Moderation", "voice"); } diff --git a/bot/commands/src/main/kotlin/sh/nino/discord/commands/CommandHandler.kt b/bot/commands/src/main/kotlin/sh/nino/discord/commands/CommandHandler.kt new file mode 100644 index 00000000..f4f95784 --- /dev/null +++ b/bot/commands/src/main/kotlin/sh/nino/discord/commands/CommandHandler.kt @@ -0,0 +1,438 @@ +/* + * Copyright (c) 2019-2022 Nino + * + * 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. + */ + +package sh.nino.discord.commands + +import dev.kord.common.entity.DiscordUser +import dev.kord.common.entity.Snowflake +import dev.kord.core.Kord +import dev.kord.core.behavior.channel.createMessage +import dev.kord.core.cache.data.UserData +import dev.kord.core.entity.User +import dev.kord.core.event.message.MessageCreateEvent +import dev.kord.core.firstOrNull +import dev.kord.rest.builder.message.EmbedBuilder +import gay.floof.utils.slf4j.logging +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.future.await +import kotlinx.coroutines.future.future +import kotlinx.coroutines.withContext +import org.jetbrains.exposed.sql.or +import org.koin.core.context.GlobalContext +import sh.nino.discord.automod.core.Container +import sh.nino.discord.common.COLOR +import sh.nino.discord.common.FLAG_REGEX +import sh.nino.discord.common.FlagValue +import sh.nino.discord.common.data.Config +import sh.nino.discord.common.data.Environment +import sh.nino.discord.common.extensions.* +import sh.nino.discord.core.NinoBot +import sh.nino.discord.core.NinoScope +import sh.nino.discord.core.localization.LocalizationManager +import sh.nino.discord.database.asyncTransaction +import sh.nino.discord.database.tables.* +import java.io.ByteArrayOutputStream +import java.io.PrintStream +import java.nio.charset.StandardCharsets +import java.util.* +import kotlin.math.round + +class CommandHandler( + private val config: Config, + private val kord: Kord, + private val locales: LocalizationManager, + private val nino: NinoBot +) { + val commands: Map + get() = GlobalContext.retrieveAll() + .map { it.info.name to Command(it) } + .asMap() + + private val timer = Timer("Nino-CooldownTimer") + + // TODO: move to caffeine for this(?) + private val cooldownCache = mutableMapOf>() + private val logger by logging() + + suspend fun onCommand(event: MessageCreateEvent) { + // If the author is a webhook, let's not do anything + if (event.message.author == null) return + + // If the author is a bot, let's not do anything + if (event.message.author!!.isBot) return + + // Run all automod based off the event + Container.execute(event) + + // Retrieve the guild, if `getGuild` returns null, + // it's most likely we're in a DM. + val guild = event.getGuild() ?: return + + // Get guild + user settings + var guildSettings = asyncTransaction { + GuildSettingsEntity.findById(guild.id.value.toLong()) + } + + var userSettings = asyncTransaction { + UserEntity.findById(event.message.author!!.id.value.toLong()) + } + + // Can't find the guild or user settings? + // Let's create it and override the variable! + if (guildSettings == null) { + // create the other guild settings + asyncTransaction { + AutomodEntity.new(guild.id.value.toLong()) {} + LoggingEntity.new(guild.id.value.toLong()) {} + } + + guildSettings = asyncTransaction { + GuildSettingsEntity.new(guild.id.value.toLong()) {} + } + } + + if (userSettings == null) { + userSettings = asyncTransaction { + UserEntity.new(event.message.author!!.id.value.toLong()) {} + } + } + + val selfUser = guild.members.firstOrNull { it.id == kord.selfId } ?: return + val prefixes = ( + listOf("<@${kord.selfId}>", "<@!${kord.selfId}>") + + config.prefixes.toList() + + guildSettings.prefixes.toList() + + userSettings.prefixes.toList() + ).distinct() + + if (event.message.content.matches("^<@!?${kord.selfId}>$".toRegex())) { + val prefix = prefixes.drop(2).random() + event.message.channel.createMessage { + content = ":wave: Hallo **${event.message.author!!.tag}**!!!!" + embeds += EmbedBuilder().apply { + color = COLOR + description = buildString { + appendLine("I am **${selfUser.tag}**, I operate as a moderation bot in this guild! (**${guild.name}**)") + appendLine("> You can see a list of commands from our [website](https://nino.sh/commands) or invoking the **${prefix}help** command!") + appendLine() + appendLine("If you wish to invite ${selfUser.username}, please click [here](https://nino.sh/invite) to do so.") + appendLine("Nino is also open source! If you wish, you can star the [repository](https://github.com/NinoDiscord/Nino)! :hearts:") + appendLine() + appendLine("I will get out of your hair senpai, have a good day/evening~") + } + } + } + } + + // Find the prefix if we can find any + val prefix = prefixes.firstOrNull { event.message.content.startsWith(it) } ?: return + val globalBan = asyncTransaction { + GlobalBans.find { + (GlobalBansTable.id eq guild.id.value.toLong()) or (GlobalBansTable.id eq event.message.author!!.id.value.toLong()) + }.firstOrNull() + } + + if (globalBan != null) { + event.message.channel.createMessage { + embeds += EmbedBuilder().apply { + color = COLOR + + val issuer = kord.rest.user.getUser(globalBan.issuer.asSnowflake()) + description = buildString { + append(if (globalBan.type == BanType.USER) "You" else guild.name) + appendLine(" was globally banned by ${issuer.username}#${issuer.discriminator}.") + appendLine() + appendLine("> **${globalBan.reason ?: "(no reason was specified)"}**") + appendLine() + appendLine("If you think this was a mistake, report it in the [Noelware](https://discord.gg/ATmjFH9kMH) Discord server!") + } + } + } + + // leave the guild if the ban was from a guild. + if (globalBan.type == BanType.GUILD) guild.leave() + + return + } + + val content = event.message.content.substring(prefix.length).trim() + val (name, args) = content.split("\\s+".toRegex()).pairUp() + val cmdName = name.lowercase() + val locale = locales.getLocale(guildSettings.language, userSettings.language) + val flags = parseFlags(content) + + val command = commands[cmdName] + ?: commands.values.firstOrNull { it.aliases.contains(cmdName) } + ?: return + + // omit flags from argument list + val rawArgs = if (command.name != "eval") { + args.filter { !FLAG_REGEX.toRegex().matches(it) } + } else { + args + } + + val message = CommandMessage( + event, + flags, + rawArgs, + guildSettings, + userSettings, + locale, + guild + ) + + val needsHelp = (message.flags["help"] ?: message.flags["h"])?.asBooleanOrNull ?: false + if (needsHelp) { + command.help(message) + return + } + + if (command.ownerOnly && !config.owners.contains(event.message.author!!.id.toString())) { + message.reply(locale.translate("errors.ownerOnly", mapOf("name" to cmdName))) + return + } + + if (command.userPermissions.values.isNotEmpty() && guild.ownerId != event.message.author!!.id) { + val member = event.message.getAuthorAsMember()!! + val missing = command.userPermissions.values.filter { + !member.getPermissions().contains(it) + } + + if (missing.isNotEmpty()) { + val permList = missing.joinToString(", ") { it.asString() } + message.reply( + locale.translate( + "errors.missingPermsUser", + mapOf( + "perms" to permList + ) + ) + ) + + return + } + } + + if (command.botPermissions.values.isNotEmpty()) { + val missing = command.userPermissions.values.filter { + !selfUser.getPermissions().contains(it) + } + + if (missing.isNotEmpty()) { + val permList = missing.joinToString(", ") { it.asString() } + message.reply( + locale.translate( + "errors.missingPermsBot", + mapOf( + "perms" to permList + ) + ) + ) + + return + } + } + + // cooldown stuff + // this is also pretty bare bones pls dont owo me in the chat + // ;-; + if (!cooldownCache.containsKey(command.name)) + cooldownCache[command.name] = mutableMapOf() + + val now = System.currentTimeMillis() + val timestamps = cooldownCache[command.name]!! + val amount = command.cooldown * 1000 + + // Owners of the bot bypass cooldowns, so... :shrug: + if (!config.owners.contains(event.message.author!!.id.toString()) && timestamps.containsKey(event.message.author!!.id.toString())) { + val time = timestamps[event.message.author!!.id.toString()]!! + amount + if (now < time.toLong()) { + val left = (time - now) / 1000 + message.reply( + locale.translate( + "errors.cooldown", + mapOf( + "command" to command.name, + "time" to round(left.toDouble()) + ) + ) + ) + + return + } + + timestamps[event.message.author!!.id.toString()] = now.toInt() + timer.schedule( + object: TimerTask() { + override fun run() { + timestamps.remove(event.message.author!!.id.toString()) + } + }, + amount.toLong() + ) + } + + // Is there a subcommand? maybe! + var subcommand: Subcommand? = null + for (arg in args) { + if (command.thiz.subcommands.isNotEmpty()) { + if (command.thiz.subcommands.find { it.name == arg || it.aliases.contains(arg) } != null) { + subcommand = command.thiz.subcommands.first { it.name == arg || it.aliases.contains(arg) } + break + } + } + } + + if (subcommand != null) { + val newMsg = CommandMessage( + event, + flags, + rawArgs.drop(1), + guildSettings, + userSettings, + locale, + guild + ) + + if (subcommand.permissions.values.isNotEmpty() && guild.ownerId != event.message.author!!.id) { + val member = event.message.getAuthorAsMember()!! + val missing = subcommand.permissions.values.filter { + !member.getPermissions().contains(it) + } + + if (missing.isNotEmpty()) { + val permList = missing.joinToString(", ") { it.asString() } + message.reply( + locale.translate( + "errors.missingPermsUser", + mapOf( + "perms" to permList + ) + ) + ) + + return + } + } + + subcommand.execute(newMsg) { ex, success -> + logger.info("Subcommand \"$prefix${command.name} ${subcommand.name}\" was executed by ${newMsg.author.tag} (${newMsg.author.id}) in ${guild.name} (${guild.id})") + if (!success) { + onCommandError(newMsg, subcommand.name, ex!!, true) + } + } + } else { + command.run(message) { ex, success -> + logger.info("Command \"$prefix${command.name}\" was executed by ${message.author.tag} (${message.author.id}) in ${guild.name} (${guild.id})") + if (!success) { + onCommandError(message, command.name, ex!!, false) + } + } + } + } + + private suspend fun onCommandError( + message: CommandMessage, + name: String, + exception: Exception, + isSub: Boolean = false + ) { + // Report to Sentry if installed + nino.sentryReport(exception) + + // Fetch all owners + val owners = config.owners.map { + val user = NinoScope.future { kord.getUser(it.asSnowflake()) }.await() ?: User( + UserData.Companion.from( + DiscordUser( + id = Snowflake("0"), + username = "Unknown User", + discriminator = "0000", + null + ) + ), + + kord + ) + + user.tag + } + + if (config.environment == Environment.Development) { + val baos = ByteArrayOutputStream() + val stream = withContext(Dispatchers.IO) { + PrintStream(baos, true, StandardCharsets.UTF_8.name()) + } + + stream.use { + exception.printStackTrace(stream) + } + + val stacktrace = withContext(Dispatchers.IO) { + baos.toString(StandardCharsets.UTF_8.name()) + } + + message.replyTranslate( + "errors.unknown.dev", + mapOf( + "prefix" to if (isSub) "subcommand" else "command", + "command" to name, + "owners" to owners.joinToString(", "), + "stacktrace" to stacktrace.elipsis(1550) + ) + ) + } else { + message.replyTranslate( + "errors.unknown.prod", + mapOf( + "prefix" to if (isSub) "subcommand" else "command", + "command" to name, + "owners" to owners.joinToString(", ") + ) + ) + } + + logger.error("Unable to execute ${if (isSub) "subcommand" else "command"} $name:", exception) + } + + private fun parseFlags(content: String): Map { + val flags = mutableMapOf() + val found = FLAG_REGEX.toRegex().findAll(content) + + if (found.toList().isEmpty()) + return flags + + for (match in found) { + val name = match.groups[1]!!.value + val value = match.groups[2]?.value ?: "" + + val flagValue = if (value.isEmpty() || value.isBlank()) "" else value.replace("(^[='\"]+|['\"]+\$)".toRegex(), "").trim() + val flag = + if (value.isEmpty() || value.isBlank()) FlagValue(true) else FlagValue(flagValue) + + flags[name] = flag + } + + return flags + } +} diff --git a/bot/commands/src/main/kotlin/sh/nino/discord/commands/CommandMessage.kt b/bot/commands/src/main/kotlin/sh/nino/discord/commands/CommandMessage.kt new file mode 100644 index 00000000..b1e4218e --- /dev/null +++ b/bot/commands/src/main/kotlin/sh/nino/discord/commands/CommandMessage.kt @@ -0,0 +1,134 @@ +/* + * Copyright (c) 2019-2022 Nino + * + * 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. + */ + +package sh.nino.discord.commands + +import dev.kord.core.behavior.channel.createMessage +import dev.kord.core.entity.Guild +import dev.kord.core.entity.Message +import dev.kord.core.entity.channel.TextChannel +import dev.kord.core.event.message.MessageCreateEvent +import dev.kord.rest.NamedFile +import dev.kord.rest.builder.message.EmbedBuilder +import dev.kord.rest.builder.message.create.allowedMentions +import kotlinx.coroutines.flow.* +import sh.nino.discord.common.COLOR +import sh.nino.discord.common.FlagValue +import sh.nino.discord.core.localization.Locale +import sh.nino.discord.core.messaging.PaginationEmbed +import sh.nino.discord.database.tables.GuildSettingsEntity +import sh.nino.discord.database.tables.UserEntity + +class CommandMessage( + private val event: MessageCreateEvent, + val flags: Map, + val args: List, + val settings: GuildSettingsEntity, + val userSettings: UserEntity, + val locale: Locale, + val guild: Guild +) { + val attachments = event.message.attachments.toList() + val message = event.message + val author = message.author!! + val kord = event.kord + + suspend fun createPaginationEmbed(embeds: List): PaginationEmbed { + val channel = message.channel.asChannel() as TextChannel + return PaginationEmbed(channel, author, embeds) + } + + suspend fun replyFile(content: String, files: List): Message = message.channel.createMessage { + this.content = content + this.files += files + + messageReference = message.id + allowedMentions { + repliedUser = false + } + } + + suspend fun reply(content: String, reply: Boolean = true, embedBuilder: EmbedBuilder.() -> Unit): Message { + val embed = EmbedBuilder().apply(embedBuilder) + embed.color = COLOR + + return message.channel.createMessage { + this.content = content + this.embeds += embed + + if (reply) { + messageReference = message.id + allowedMentions { + repliedUser = false + } + } + } + } + + suspend fun reply(content: String, reply: Boolean = true): Message { + return message.channel.createMessage { + this.content = content + + if (reply) { + messageReference = message.id + allowedMentions { + repliedUser = false + } + } + } + } + + suspend fun reply(content: String): Message = reply(content, true) + suspend fun reply(reply: Boolean = true, embedBuilder: EmbedBuilder.() -> Unit): Message { + val embed = EmbedBuilder().apply(embedBuilder) + embed.color = COLOR + + return message.channel.createMessage { + this.embeds += embed + if (reply) { + messageReference = message.id + allowedMentions { + repliedUser = false + } + } + } + } + + suspend fun replyTranslate(key: String, args: Map = mapOf()): Message = reply(locale.translate(key, args)) + + // not finished since i can't find how to do this :( + suspend fun readFromInput( + message: Message = this.message, + timeout: Long = 60000, + filter: suspend (Message) -> Boolean = { + true + } + ): Message? = event + .kord + .events + .filterIsInstance() + .filter { it.message.author?.id == message.author!!.id } + .map { it.message } + .filter(filter) + .take(1) + .singleOrNull() +} diff --git a/bot/commands/src/main/kotlin/sh/nino/discord/commands/Subcommand.kt b/bot/commands/src/main/kotlin/sh/nino/discord/commands/Subcommand.kt new file mode 100644 index 00000000..ebd2ad39 --- /dev/null +++ b/bot/commands/src/main/kotlin/sh/nino/discord/commands/Subcommand.kt @@ -0,0 +1,74 @@ +/* + * Copyright (c) 2019-2022 Nino + * + * 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. + */ + +package sh.nino.discord.commands + +import dev.kord.common.DiscordBitSet +import dev.kord.common.entity.Permissions +import kotlinx.coroutines.launch +import sh.nino.discord.core.NinoScope +import kotlin.reflect.KCallable +import kotlin.reflect.full.callSuspend +import sh.nino.discord.commands.annotations.Subcommand as Annotation + +class Subcommand private constructor( + val name: String, + val description: String, + val usage: String = "", + val aliases: List = listOf(), + val permissions: Permissions = Permissions(), + private val method: KCallable<*>, + val parent: AbstractCommand +) { + constructor( + method: KCallable<*>, + info: Annotation, + thisCtx: AbstractCommand + ): this( + info.name, + info.description, + info.usage, + info.aliases.toList(), + Permissions(DiscordBitSet(info.permissions)), + method, + thisCtx + ) + + suspend fun execute(msg: CommandMessage, callback: suspend (Exception?, Boolean) -> Unit): Any = + if (method.isSuspend) { + NinoScope.launch { + try { + method.callSuspend(parent, msg) + callback(null, true) + } catch (e: Exception) { + callback(e, false) + } + } + } else { + try { + method.call(parent, msg) + callback(null, true) + } catch (e: Exception) { + callback(e, false) + } + } +} diff --git a/src/commands/core/InviteCommand.ts b/bot/commands/src/main/kotlin/sh/nino/discord/commands/_Module.kt similarity index 55% rename from src/commands/core/InviteCommand.ts rename to bot/commands/src/main/kotlin/sh/nino/discord/commands/_Module.kt index e62b0b4f..f10d89ca 100644 --- a/src/commands/core/InviteCommand.ts +++ b/bot/commands/src/main/kotlin/sh/nino/discord/commands/_Module.kt @@ -1,5 +1,5 @@ -/** - * Copyright (c) 2019-2021 Nino +/* + * Copyright (c) 2019-2022 Nino * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -20,29 +20,22 @@ * SOFTWARE. */ -import { Command, CommandMessage } from '../../structures'; -import { Inject } from '@augu/lilith'; -import Discord from '../../components/Discord'; +package sh.nino.discord.commands -export default class InviteCommand extends Command { - @Inject - private discord!: Discord; +import org.koin.dsl.module +import sh.nino.discord.commands.admin.adminCommandsModule +import sh.nino.discord.commands.core.coreCommandsModule +import sh.nino.discord.commands.easter_egg.easterEggCommandModule +import sh.nino.discord.commands.moderation.moderationCommandsModule +import sh.nino.discord.commands.system.systemCommandsModule +import sh.nino.discord.commands.threads.threadsCommandsModule +import sh.nino.discord.commands.util.utilCommandsModule +import sh.nino.discord.commands.voice.voiceCommandsModule - constructor() { - super({ - description: 'descriptions.invite', - aliases: ['inviteme', 'inv'], - cooldown: 2, - name: 'invite', - }); - } - - run(msg: CommandMessage) { - return msg.translate('commands.invite', [ - msg.author.tag, - `https://discord.com/oauth2/authorize?client_id=${this.discord.client.user.id}&scope=bot`, - 'https://discord.com/oauth2/authorize?client_id=613907896622907425&scope=bot', - 'https://discord.gg/ATmjFH9kMH', - ]); - } +val commandsModule = adminCommandsModule + coreCommandsModule + + easterEggCommandModule + moderationCommandsModule + systemCommandsModule + + threadsCommandsModule + utilCommandsModule + voiceCommandsModule + module { + single { + CommandHandler(get(), get(), get(), get()) + } } diff --git a/bot/commands/src/main/kotlin/sh/nino/discord/commands/_NinoCoreExtensions.kt b/bot/commands/src/main/kotlin/sh/nino/discord/commands/_NinoCoreExtensions.kt new file mode 100644 index 00000000..dd2141b6 --- /dev/null +++ b/bot/commands/src/main/kotlin/sh/nino/discord/commands/_NinoCoreExtensions.kt @@ -0,0 +1,158 @@ +/* + * Copyright (c) 2019-2022 Nino + * + * 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. + */ + +package sh.nino.discord.commands + +import sh.nino.discord.common.data.Config +import sh.nino.discord.common.extensions.asString +import sh.nino.discord.common.extensions.inject + +// ok why is this here? +// this is here because :bot:core cannot correlate with :bot:commands + +/** + * This function returns an embed of the correspondent command available. + */ +suspend fun Command.help(msg: CommandMessage) = msg.reply { + val cmd = this@help + val config by inject() + val prefix = config.prefixes.random() + + title = "[ \uD83D\uDD8C Command ${cmd.name} ]" + description = try { + msg.locale.translate("descriptions.${cmd.category.localeKey}.${cmd.name}") + } catch (e: Exception) { + "*hasn't been translated :<*" + } + + field { + name = "❯ Syntax" + value = "`$prefix${cmd.name} ${cmd.usage.trim()}`" + inline = false + } + + field { + name = "❯ Category" + value = "${cmd.category.emoji} ${cmd.category.key}" + inline = true + } + + field { + name = "❯ Aliases" + value = cmd.aliases.joinToString(", ").ifEmpty { "None" } + inline = true + } + + field { + name = "❯ Examples" + value = cmd.examples.joinToString("\n") { + it.replace("{prefix}", prefix) + }.ifEmpty { "No examples were added." } + + inline = true + } + + field { + name = "❯ Cooldown" + value = "${cmd.cooldown} seconds" + inline = true + } + + field { + name = "❯ Constraints" + value = buildString { + appendLine("• **Owner Only**: ${if (cmd.ownerOnly) "Yes" else "No"}") + } + + inline = true + } + + field { + name = "❯ User Permissions" + value = buildString { + if (cmd.userPermissions.values.isEmpty()) { + appendLine("You do not require any permissions!") + } else { + for (perm in cmd.userPermissions.values.toTypedArray()) { + appendLine("• **${perm.asString()}**") + } + } + } + + inline = true + } + + field { + name = "❯ Bot Permissions" + value = buildString { + if (cmd.botPermissions.values.isEmpty()) { + appendLine("I do not require any permissions!") + } else { + for (perm in cmd.botPermissions.values.toTypedArray()) { + appendLine("• **${perm.asString()}**") + } + } + } + + inline = true + } +} + +suspend fun Subcommand.help(msg: CommandMessage) = msg.reply { + val subcmd = this@help + val config by inject() + val prefix = config.prefixes.random() + + title = "[ \uD83D\uDD8C Subcommand ${subcmd.parent.info.name} ${subcmd.name} ]" + description = try { + msg.locale.translate("descriptions.${subcmd.parent.info.category.localeKey}.${subcmd.parent.info.name}.${subcmd.name}") + } catch (e: Exception) { + "*not translated yet! :<*" + } + + field { + name = "❯ Syntax" + value = "`$prefix${subcmd.parent.info.name} ${subcmd.name}${subcmd.usage.trim()}`".trim() + inline = true + } + + field { + name = "❯ Aliases" + value = subcmd.aliases.joinToString(", ").ifEmpty { "None" } + inline = true + } + + field { + name = "❯ Permissions" + value = buildString { + if (subcmd.permissions.values.isEmpty()) { + appendLine("You do not need any permissions to execute this command!") + } else { + for (perm in subcmd.permissions.values.toTypedArray()) { + appendLine("• **${perm.asString()}**") + } + } + } + + inline = true + } +} diff --git a/bot/commands/src/main/kotlin/sh/nino/discord/commands/admin/AutomodCommand.kt b/bot/commands/src/main/kotlin/sh/nino/discord/commands/admin/AutomodCommand.kt new file mode 100644 index 00000000..39b155ec --- /dev/null +++ b/bot/commands/src/main/kotlin/sh/nino/discord/commands/admin/AutomodCommand.kt @@ -0,0 +1,513 @@ +/* + * Copyright (c) 2019-2022 Nino + * + * 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. + */ + +@file:Suppress("UNUSED") +package sh.nino.discord.commands.admin + +import kotlinx.coroutines.launch +import org.jetbrains.exposed.sql.update +import sh.nino.discord.commands.AbstractCommand +import sh.nino.discord.commands.CommandCategory +import sh.nino.discord.commands.CommandMessage +import sh.nino.discord.commands.annotations.Command +import sh.nino.discord.commands.annotations.Subcommand +import sh.nino.discord.core.NinoScope +import sh.nino.discord.database.asyncTransaction +import sh.nino.discord.database.tables.AutomodEntity +import sh.nino.discord.database.tables.AutomodTable +import java.util.* + +@Command( + name = "automod", + description = "descriptions.admin.automod", + aliases = ["am"], + category = CommandCategory.ADMIN, + userPermissions = [0x00000020] // ManageGuild +) +class AutomodCommand: AbstractCommand() { + private val messageRemoverTimer = Timer("Nino-MessageRemoverTimer") + private fun enabled(value: Boolean): String = if (value) { + "<:success:464708611260678145>" + } else { + "<:xmark:464708589123141634>" + } + + override suspend fun execute(msg: CommandMessage) { + val guild = msg.message.getGuild() + val settings = asyncTransaction { + AutomodEntity.findById(guild.id.value.toLong())!! + } + + msg.reply { + description = buildString { + appendLine("• ${enabled(settings.messageLinks)} **Message Links**") + appendLine("• ${enabled(settings.accountAge)} **Account Age**") + appendLine("• ${enabled(settings.dehoisting)} **Dehoisting**") + appendLine("• ${enabled(settings.shortlinks)} **Shortlinks**") + appendLine("• ${enabled(settings.blacklist)} **Blacklist**") + appendLine("• ${enabled(settings.phishing)} **Phishing**") + appendLine("• ${enabled(settings.mentions)} **Mentions**") + appendLine("• ${enabled(settings.invites)} **Toxicity**") + appendLine("• ${enabled(settings.invites)} **Invites**") + appendLine("• ${enabled(settings.invites)} **Raid**") + appendLine("• ${enabled(settings.invites)} **Spam**") + } + } + } + + @Subcommand( + "messageLinks", + "descriptions.automod.messageLinks", + aliases = ["links", "msglinks", "mlinks", "ml"] + ) + suspend fun messageLinks(msg: CommandMessage) { + val guild = msg.message.getGuild() + val settings = asyncTransaction { + AutomodEntity.findById(guild.id.value.toLong())!! + } + + val prop = !settings.messageLinks + asyncTransaction { + AutomodTable.update({ AutomodTable.id eq guild.id.value.toLong() }) { + it[messageLinks] = prop + } + } + + msg.replyTranslate( + "commands.automod.toggle", + mapOf( + "emoji" to enabled(prop), + "toggle" to msg.locale.translate("generic.${if (prop) "enabled" else "disabled"}"), + "name" to "Message Links" + ) + ) + } + + @Subcommand( + "accountAge", + "descriptions.automod.accountAge", + aliases = ["age", "accAge", "ac"], + usage = "[days]" + ) + suspend fun accountAge(msg: CommandMessage) { + val guild = msg.message.getGuild() + + if (msg.args.isEmpty()) { + val settings = asyncTransaction { + AutomodEntity.findById(guild.id.value.toLong())!! + } + + val prop = !settings.accountAge + asyncTransaction { + AutomodTable.update({ AutomodTable.id eq guild.id.value.toLong() }) { + it[accountAge] = prop + } + } + + msg.replyTranslate( + "commands.automod.toggle", + mapOf( + "emoji" to enabled(prop), + "toggle" to msg.locale.translate("generic.${if (prop) "enabled" else "disabled"}"), + "name" to "Account Age" + ) + ) + + return + } + + val days = msg.args.first() + try { + Integer.parseInt(days) + } catch (e: NumberFormatException) { + msg.reply("The amount of days specified was not a correct number.") + return + } + + val numOfDays = Integer.parseInt(days) + if (numOfDays <= 0) { + msg.reply("Number of days cannot go below zero.") + return + } + + if (numOfDays >= 14) { + msg.reply("Number of days cannot go over 14 days. :<") + return + } + + asyncTransaction { + AutomodTable.update({ AutomodTable.id eq guild.id.value.toLong() }) { + it[accountAgeDayThreshold] = numOfDays + } + } + + msg.reply("<:success:464708611260678145> Successfully set the day threshold to **$numOfDays** days~") + } + + @Subcommand( + "dehoist", + "descriptions.automod.dehoist", + aliases = ["dehoisting", "dh"] + ) + suspend fun dehoist(msg: CommandMessage) { + val guild = msg.message.getGuild() + val settings = asyncTransaction { + AutomodEntity.findById(guild.id.value.toLong())!! + } + + val prop = !settings.dehoisting + asyncTransaction { + AutomodTable.update({ AutomodTable.id eq guild.id.value.toLong() }) { + it[dehoisting] = prop + } + } + + msg.replyTranslate( + "commands.automod.toggle", + mapOf( + "emoji" to enabled(prop), + "toggle" to msg.locale.translate("generic.${if (prop) "enabled" else "disabled"}"), + "name" to "Dehoisting" + ) + ) + } + + @Subcommand( + "shortlinks", + "descriptions.automod.shortlinks", + aliases = ["sl", "links"] + ) + suspend fun shortlinks(msg: CommandMessage) { + val guild = msg.message.getGuild() + val settings = asyncTransaction { + AutomodEntity.findById(guild.id.value.toLong())!! + } + + val prop = !settings.shortlinks + asyncTransaction { + AutomodTable.update({ AutomodTable.id eq guild.id.value.toLong() }) { + it[shortlinks] = prop + } + } + + msg.replyTranslate( + "commands.automod.toggle", + mapOf( + "emoji" to enabled(prop), + "toggle" to msg.locale.translate("generic.${if (prop) "enabled" else "disabled"}"), + "name" to "Shortlinks" + ) + ) + } + + @Subcommand( + "blacklist", + "descriptions.automod.blacklist", + usage = "[\"list\" | \"set \" | \"remove \"]" + ) + suspend fun blacklist(msg: CommandMessage) { + val guild = msg.message.getGuild() + if (msg.args.isEmpty()) { + val settings = asyncTransaction { + AutomodEntity.findById(guild.id.value.toLong())!! + } + + val prop = !settings.blacklist + asyncTransaction { + AutomodTable.update({ AutomodTable.id eq guild.id.value.toLong() }) { + it[blacklist] = prop + } + } + + msg.replyTranslate( + "commands.automod.toggle", + mapOf( + "emoji" to enabled(prop), + "toggle" to msg.locale.translate("generic.${if (prop) "enabled" else "disabled"}"), + "name" to "Blacklist" + ) + ) + + return + } + + val settings = asyncTransaction { + AutomodEntity.findById(guild.id.value.toLong())!! + } + + when (msg.args.first()) { + "remove", "del", "delete" -> { + val words = msg.args.drop(1) + if (words.isEmpty()) { + msg.reply("Missing words to remove from the blacklist.") + return + } + + val wordsRemaining = settings.blacklistedWords.filter { words.contains(it) } + asyncTransaction { + AutomodTable.update({ AutomodTable.id eq guild.id.value.toLong() }) { + it[blacklistedWords] = wordsRemaining.toTypedArray() + } + } + + msg.reply("Successfully removed **${words.size}** words from the blacklist.") + } + + "list" -> { + val original = msg.reply { + title = "[ Blacklisted Words in ${guild.name} ]" + description = buildString { + appendLine("Due to the nature of some words (that can be blacklisted)") + appendLine("This message will be deleted in roughly 5 seconds from now.") + appendLine() + + for (word in settings.blacklistedWords.toList().chunked(5)) { + append("• **${word.joinToString(", ")}**") + } + } + } + + messageRemoverTimer.schedule( + object: TimerTask() { + override fun run() { + NinoScope.launch { original.delete() } + } + }, + 5000L + ) + } + + "add", "set" -> { + val words = msg.args.drop(1) + + asyncTransaction { + AutomodTable.update({ AutomodTable.id eq guild.id.value.toLong() }) { + it[blacklistedWords] = words.toTypedArray() + settings.blacklistedWords + } + } + + msg.reply("Successfully added **${words.size}** new blacklisted words. :D") + } + + else -> msg.reply("Missing subsubcommand: `add`, `list`, or `remove`") + } + } + + @Subcommand( + "phishing", + "descriptions.automod.phishing", + aliases = ["phish", "fish", "\uD83D\uDC1F"] + ) + suspend fun phishing(msg: CommandMessage) { + val guild = msg.message.getGuild() + val settings = asyncTransaction { + AutomodEntity.findById(guild.id.value.toLong())!! + } + + val prop = !settings.phishing + asyncTransaction { + AutomodTable.update({ AutomodTable.id eq guild.id.value.toLong() }) { + it[phishing] = prop + } + } + + msg.replyTranslate( + "commands.automod.toggle", + mapOf( + "emoji" to enabled(prop), + "toggle" to msg.locale.translate("generic.${if (prop) "enabled" else "disabled"}"), + "name" to "Phishing Links" + ) + ) + } + + @Subcommand( + "mentions", + "descriptions.automod.dehoist", + aliases = ["@mention", "@"], + usage = "[threshold]" + ) + suspend fun mentions(msg: CommandMessage) { + val guild = msg.message.getGuild() + val settings = asyncTransaction { + AutomodEntity.findById(guild.id.value.toLong())!! + } + + if (msg.args.isEmpty()) { + val prop = !settings.mentions + asyncTransaction { + AutomodTable.update({ AutomodTable.id eq guild.id.value.toLong() }) { + it[mentions] = prop + } + } + + msg.replyTranslate( + "commands.automod.toggle", + mapOf( + "emoji" to enabled(prop), + "toggle" to msg.locale.translate("generic.${if (prop) "enabled" else "disabled"}"), + "name" to "Dehoisting" + ) + ) + + return + } + + val threshold = msg.args.first() + try { + Integer.parseInt(threshold) + } catch (e: NumberFormatException) { + msg.reply("The mention threshold should be a valid number.") + return + } + + val numOfMentions = Integer.parseInt(threshold) + if (numOfMentions <= 0) { + msg.reply("Cannot below zero. You can just... disable the automod, you know?") + return + } + + if (numOfMentions >= 25) { + msg.reply("Cannot above 25 mentions, don't think that'll be possible...") + return + } + + asyncTransaction { + AutomodTable.update({ AutomodTable.id eq guild.id.value.toLong() }) { + it[mentionsThreshold] = numOfMentions + } + } + + msg.reply("<:success:464708611260678145> Successfully set the mention threshold to **$numOfMentions** mentions~") + } + + @Subcommand( + "toxicity", + "descriptions.automod.toxicity", + aliases = ["toxic"] + ) + suspend fun toxicity(msg: CommandMessage) { + val guild = msg.message.getGuild() + val settings = asyncTransaction { + AutomodEntity.findById(guild.id.value.toLong())!! + } + + val prop = !settings.toxicity + asyncTransaction { + AutomodTable.update({ AutomodTable.id eq guild.id.value.toLong() }) { + it[toxicity] = prop + } + } + + msg.replyTranslate( + "commands.automod.toggle", + mapOf( + "emoji" to enabled(prop), + "toggle" to msg.locale.translate("generic.${if (prop) "enabled" else "disabled"}"), + "name" to "Toxicity" + ) + ) + } + + @Subcommand( + "spam", + "descriptions.automod.spam" + ) + suspend fun spam(msg: CommandMessage) { + val guild = msg.message.getGuild() + val settings = asyncTransaction { + AutomodEntity.findById(guild.id.value.toLong())!! + } + + val prop = !settings.spam + asyncTransaction { + AutomodTable.update({ AutomodTable.id eq guild.id.value.toLong() }) { + it[spam] = prop + } + } + + msg.replyTranslate( + "commands.automod.toggle", + mapOf( + "emoji" to enabled(prop), + "toggle" to msg.locale.translate("generic.${if (prop) "enabled" else "disabled"}"), + "name" to "Spam" + ) + ) + } + + @Subcommand( + "raid", + "descriptions.automod.raid", + aliases = ["raids"] + ) + suspend fun raid(msg: CommandMessage) { + val guild = msg.message.getGuild() + val settings = asyncTransaction { + AutomodEntity.findById(guild.id.value.toLong())!! + } + + val prop = !settings.raid + asyncTransaction { + AutomodTable.update({ AutomodTable.id eq guild.id.value.toLong() }) { + it[raid] = prop + } + } + + msg.replyTranslate( + "commands.automod.toggle", + mapOf( + "emoji" to enabled(prop), + "toggle" to msg.locale.translate("generic.${if (prop) "enabled" else "disabled"}"), + "name" to "Raid" + ) + ) + } + + @Subcommand( + "invites", + "descriptions.automod.invites", + aliases = ["inv", "dinv"] + ) + suspend fun invites(msg: CommandMessage) { + val guild = msg.message.getGuild() + val settings = asyncTransaction { + AutomodEntity.findById(guild.id.value.toLong())!! + } + + val prop = !settings.invites + asyncTransaction { + AutomodTable.update({ AutomodTable.id eq guild.id.value.toLong() }) { + it[invites] = prop + } + } + + msg.replyTranslate( + "commands.automod.toggle", + mapOf( + "emoji" to enabled(prop), + "toggle" to msg.locale.translate("generic.${if (prop) "enabled" else "disabled"}"), + "name" to "Invites" + ) + ) + } +} diff --git a/bot/commands/src/main/kotlin/sh/nino/discord/commands/admin/ExportCommand.kt b/bot/commands/src/main/kotlin/sh/nino/discord/commands/admin/ExportCommand.kt new file mode 100644 index 00000000..5b58b66a --- /dev/null +++ b/bot/commands/src/main/kotlin/sh/nino/discord/commands/admin/ExportCommand.kt @@ -0,0 +1,125 @@ +/* + * Copyright (c) 2019-2022 Nino + * + * 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. + */ + +package sh.nino.discord.commands.admin + +import dev.kord.rest.NamedFile +import kotlinx.coroutines.future.await +import kotlinx.datetime.Clock +import kotlinx.datetime.Instant +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable +import kotlinx.serialization.json.Json +import sh.nino.discord.commands.AbstractCommand +import sh.nino.discord.commands.CommandCategory +import sh.nino.discord.commands.CommandMessage +import sh.nino.discord.commands.annotations.Command +import sh.nino.discord.common.RandomId +import sh.nino.discord.core.redis.RedisManager +import sh.nino.discord.database.asyncTransaction +import sh.nino.discord.database.tables.GuildSettingsEntity +import java.io.ByteArrayInputStream + +@Serializable +data class ExportedGuildSettings( + @SerialName("modlog_webhook_uri") + val modlogWebhookUri: String?, + + @SerialName("use_plain_messages") + val usePlainMessages: Boolean, + + @SerialName("no_threads_role_id") + val noThreadsRoleId: Long?, + + @SerialName("modlog_channel_id") + val modlogChannelId: Long?, + + @SerialName("muted_role_id") + val mutedRoleId: Long?, + + @SerialName("last_export_at") + val lastExportAt: Instant, + val prefixes: List, + val language: String +) { + companion object { + fun fromEntity(entity: GuildSettingsEntity): ExportedGuildSettings = ExportedGuildSettings( + modlogWebhookUri = entity.modlogWebhookUri, + usePlainMessages = entity.usePlainModlogMessage, + noThreadsRoleId = entity.noThreadsRoleId, + modlogChannelId = entity.modlogChannelId, + mutedRoleId = entity.mutedRoleId, + lastExportAt = Clock.System.now(), + prefixes = entity.prefixes.toList(), + language = entity.language + ) + } +} + +@Command( + name = "export", + description = "descriptions.admin.export", + category = CommandCategory.ADMIN, + aliases = ["ex"], + userPermissions = [0x00000020] // ManageGuild +) +class ExportCommand(private val redis: RedisManager, private val json: Json): AbstractCommand() { + override suspend fun execute(msg: CommandMessage) { + val message = msg.reply("Now exporting guild settings...") + val guild = msg.message.getGuild() + + val guildSettings = asyncTransaction { + GuildSettingsEntity.findById(guild.id.value.toLong())!! + } + + val exportedData = ExportedGuildSettings.fromEntity(guildSettings) + val jsonData = json.encodeToString(ExportedGuildSettings.serializer(), exportedData) + + // Save it to Redis + val id = RandomId.generate() + redis.commands.hset( + "nino:recovery:settings", + mapOf( + "${guild.id}:$id" to jsonData + ) + ).await() + + message.delete() + + val bais = ByteArrayInputStream(jsonData.toByteArray(Charsets.UTF_8)) + msg.replyFile( + buildString { + appendLine(":thumbsup: **Done!** — You can import the exact settings below using the **import** command:") + appendLine("> **nino import $id**") + appendLine() + appendLine("If you were curious on what this data is, you can read from our docs: **https://nino.sh/docs/exporting-settings**") + appendLine("Curious on what we do with your data? Read our privacy policy: **https://nino.sh/privacy**") + }, + listOf( + NamedFile( + name = "${guild.id}-settings.json", + inputStream = bais + ) + ) + ) + } +} diff --git a/bot/commands/src/main/kotlin/sh/nino/discord/commands/admin/ImportCommand.kt b/bot/commands/src/main/kotlin/sh/nino/discord/commands/admin/ImportCommand.kt new file mode 100644 index 00000000..a3fd0b0e --- /dev/null +++ b/bot/commands/src/main/kotlin/sh/nino/discord/commands/admin/ImportCommand.kt @@ -0,0 +1,165 @@ +/* + * Copyright (c) 2019-2022 Nino + * + * 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. + */ + +package sh.nino.discord.commands.admin + +import dev.kord.common.DiscordTimestampStyle +import dev.kord.common.toMessageFormat +import io.ktor.client.* +import io.ktor.client.call.* +import io.ktor.client.request.* +import io.ktor.client.statement.* +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.future.await +import kotlinx.coroutines.withContext +import kotlinx.serialization.json.Json +import org.jetbrains.exposed.sql.update +import sh.nino.discord.commands.AbstractCommand +import sh.nino.discord.commands.CommandCategory +import sh.nino.discord.commands.CommandMessage +import sh.nino.discord.commands.annotations.Command +import sh.nino.discord.core.redis.RedisManager +import sh.nino.discord.database.asyncTransaction +import sh.nino.discord.database.tables.GuildSettings + +@Command( + name = "import", + description = "descriptions.admin.import", + category = CommandCategory.ADMIN, + aliases = ["i"], + userPermissions = [0x00000020] // ManageGuild +) +class ImportCommand(private val redis: RedisManager, private val http: HttpClient): AbstractCommand() { + override suspend fun execute(msg: CommandMessage) { + val guild = msg.message.getGuild() + if (msg.args.isEmpty()) { + if (msg.attachments.isNotEmpty()) { + return fromAttachment(msg) + } + + val mapped = redis.commands.hgetall("nino:recovery:settings").await() as Map + val fromGuild = mapped.filter { it.key.startsWith(guild.id.toString()) } + + msg.reply { + title = "[ Import Settings for ${guild.name} ]" + description = buildString { + appendLine("You can revert back to previous settings using any ID available:") + appendLine("> **nino import **") + appendLine() + appendLine("Note that this only saves when you run the `export` command! If you want to revert") + appendLine("to a un-exported state, you can run this command with the file attachment that") + appendLine("was sent to you when you invoked the command and it'll revert from that stage.") + appendLine() + + for (key in fromGuild.keys) { + val id = key.split(":").last() + appendLine("• **$id** (`nino import $id`)") + } + } + } + + return + } + + val content = redis.commands.hget("nino:recovery:settings", "${guild.id}:${msg.args.first()}").await() + if (content == null) { + msg.reply("ID **${msg.args.first()}** doesn't exist.") + return + } + + val message = msg.reply("Now importing settings...") + val decoded: ExportedGuildSettings + try { + decoded = Json.decodeFromString(ExportedGuildSettings.serializer(), content) + } catch (e: Exception) { + message.delete() + msg.reply("Looks like an error has occurred. Did you import the right file? To be honest, I don't think you did...") + + return + } + + asyncTransaction { + GuildSettings.update({ GuildSettings.id eq guild.id.value.toLong() }) { + it[usePlainModlogMessage] = decoded.usePlainMessages + it[modlogWebhookUri] = decoded.modlogWebhookUri + it[noThreadsRoleId] = decoded.noThreadsRoleId + it[modlogChannelId] = decoded.modlogChannelId + it[mutedRoleId] = decoded.mutedRoleId + it[prefixes] = decoded.prefixes.toTypedArray() + it[language] = decoded.language + } + } + + message.delete() + redis.commands.hdel("nino:recovery:settings", "${guild.id}:${msg.args.first()}").await() + msg.reply("Exported settings that was exported ${decoded.lastExportAt.toMessageFormat(DiscordTimestampStyle.RelativeTime)}") + } + + private suspend fun fromAttachment(msg: CommandMessage) { + val attachment = msg.attachments.first() + val guild = msg.message.getGuild() + + if (!attachment.filename.endsWith(".json")) { + msg.reply("This is not a valid JSON file.") + return + } + + val res: HttpResponse = http.get(attachment.url) + if (res.status.value != 200) { + msg.reply("Unable to retrieve file contents. Try again later?") + return + } + + val content = withContext(Dispatchers.IO) { + res.receive() + } + + val message = msg.reply("Now importing from file **${attachment.filename}** that is ${attachment.size} bytes.") + + // We're using `Json` instead of the one from Koin since it'll ignore any unknown keys, + // and we don't really want that to keep the type safety! + val decoded: ExportedGuildSettings + try { + decoded = Json.decodeFromString(ExportedGuildSettings.serializer(), content) + } catch (e: Exception) { + message.delete() + msg.reply("Looks like an error has occurred. Did you import the right file? To be honest, I don't think you did...") + + return + } + + asyncTransaction { + GuildSettings.update({ GuildSettings.id eq guild.id.value.toLong() }) { + it[usePlainModlogMessage] = decoded.usePlainMessages + it[modlogWebhookUri] = decoded.modlogWebhookUri + it[noThreadsRoleId] = decoded.noThreadsRoleId + it[modlogChannelId] = decoded.modlogChannelId + it[mutedRoleId] = decoded.mutedRoleId + it[prefixes] = decoded.prefixes.toTypedArray() + it[language] = decoded.language + } + } + + message.delete() + msg.reply("Exported settings that was exported ${decoded.lastExportAt.toMessageFormat(DiscordTimestampStyle.RelativeTime)}") + } +} diff --git a/bot/commands/src/main/kotlin/sh/nino/discord/commands/admin/LoggingCommand.kt b/bot/commands/src/main/kotlin/sh/nino/discord/commands/admin/LoggingCommand.kt new file mode 100644 index 00000000..895c2b7f --- /dev/null +++ b/bot/commands/src/main/kotlin/sh/nino/discord/commands/admin/LoggingCommand.kt @@ -0,0 +1,564 @@ +/* + * Copyright (c) 2019-2022 Nino + * + * 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. + */ + +@file:Suppress("UNUSED") +package sh.nino.discord.commands.admin + +import dev.kord.common.entity.DiscordUser +import dev.kord.common.entity.Snowflake +import dev.kord.core.Kord +import dev.kord.core.cache.data.UserData +import dev.kord.core.entity.User +import dev.kord.core.entity.channel.TextChannel +import kotlinx.coroutines.future.await +import kotlinx.coroutines.future.future +import org.jetbrains.exposed.sql.update +import sh.nino.discord.commands.AbstractCommand +import sh.nino.discord.commands.CommandCategory +import sh.nino.discord.commands.CommandMessage +import sh.nino.discord.commands.annotations.Command +import sh.nino.discord.commands.annotations.Subcommand +import sh.nino.discord.common.CHANNEL_REGEX +import sh.nino.discord.common.ID_REGEX +import sh.nino.discord.common.extensions.asSnowflake +import sh.nino.discord.common.getMultipleChannelsFromArgs +import sh.nino.discord.common.getMutipleUsersFromArgs +import sh.nino.discord.core.NinoScope +import sh.nino.discord.database.asyncTransaction +import sh.nino.discord.database.tables.GuildLogging +import sh.nino.discord.database.tables.LogEvent +import sh.nino.discord.database.tables.LoggingEntity + +@Command( + name = "logging", + description = "descriptions.admin.logging", + category = CommandCategory.ADMIN, + aliases = ["log"], + examples = [ + "{prefix}logging | Toggles if logging should be enabled or not.", + "{prefix}logging 102569854256857 | Uses the text channel to output logging", + "{prefix}logging events | Views your events configuration.", + "{prefix}logging events enable member.boosted | Enables the **Member Boosting** logging event", + "{prefix}logging events disable | Disables all logging events", + "{prefix}logging omitUsers add 512457854563259 | Omits this user", + "{prefix}logging omitUsers del 512457854563259 | Removes them from the omitted list.", + "{prefix}logging omitChannels | Lists all the omitted channels to be logged from.", + "{prefix}logging config | View your logging configuration" + ], + userPermissions = [0x00000020] // ManageGuild +) +class LoggingCommand(private val kord: Kord): AbstractCommand() { + private fun enabled(value: Boolean): String = if (value) { + "<:success:464708611260678145>" + } else { + "<:xmark:464708589123141634>" + } + + override suspend fun execute(msg: CommandMessage) { + val guild = msg.message.getGuild() + if (msg.args.isEmpty()) { + val settings = asyncTransaction { + LoggingEntity.findById(guild.id.value.toLong())!! + } + + val prop = !settings.enabled + asyncTransaction { + GuildLogging.update({ GuildLogging.id eq guild.id.value.toLong() }) { + it[enabled] = prop + } + } + + msg.replyTranslate( + "commands.admin.logging.toggle", + mapOf( + "emoji" to enabled(prop), + "toggle" to msg.locale.translate("generic.${if (prop) "enabled" else "disabled"}") + ) + ) + + return + } + + val channel = msg.args.first() + if (ID_REGEX.matcher(channel).matches()) { + val textChannel = try { + kord.getChannelOf(channel.asSnowflake()) + } catch (e: Exception) { + null + } + + if (textChannel == null) { + msg.reply("not a text channel noob :(") + return + } + + asyncTransaction { + GuildLogging.update({ + GuildLogging.id eq guild.id.value.toLong() + }) { + it[channelId] = textChannel.id.value.toLong() + } + } + + msg.replyTranslate( + "commands.admin.logging.success", + mapOf( + "emoji" to "<:success:464708611260678145>", + "channel" to textChannel.mention + ) + ) + + return + } + + val channelRegexMatcher = CHANNEL_REGEX.matcher(channel) + if (channelRegexMatcher.matches()) { + val id = channelRegexMatcher.group(1) + val textChannel = try { + kord.getChannelOf(id.asSnowflake()) + } catch (e: Exception) { + null + } + + if (textChannel == null) { + msg.reply("not a text channel noob :(") + return + } + + asyncTransaction { + GuildLogging.update({ + GuildLogging.id eq guild.id.value.toLong() + }) { + it[channelId] = textChannel.id.value.toLong() + } + } + + msg.replyTranslate( + "commands.admin.logging.success", + mapOf( + "emoji" to "<:success:464708611260678145>", + "channel" to textChannel.mention + ) + ) + + return + } + + msg.replyTranslate( + "commands.admin.logging.invalid", + mapOf( + "arg" to channel + ) + ) + } + + @Subcommand( + "users", + "descriptions.logging.omitUsers", + aliases = ["u", "uomit"] + ) + suspend fun omitUsers(msg: CommandMessage) { + val guild = msg.message.getGuild() + if (msg.args.isEmpty()) { + val settings = asyncTransaction { + LoggingEntity.findById(guild.id.value.toLong())!! + } + + // get users from this list + val users = settings.ignoredUsers.map { + val user = NinoScope.future { kord.getUser(it.asSnowflake()) }.await() ?: User( + UserData.from( + DiscordUser( + id = Snowflake("0"), + username = "Unknown User", + discriminator = "0000", + null + ) + ), + + kord + ) + + "• ${user.tag} (${user.id})" + } + + msg.reply { + title = msg.locale.translate("commands.admin.logging.omitUsers.embed.title") + description = msg.locale.translate( + "commands.admin.logging.omitUsers.embed.description", + mapOf( + "list" to if (users.isEmpty()) { + msg.locale.translate("generic.lonely") + } else { + users.joinToString("\n") + } + ) + ) + } + + return + } + + when (msg.args.first()) { + "add", "+" -> { + val args = msg.args.drop(1) + if (args.isEmpty()) { + msg.replyTranslate("commands.admin.logging.omitUsers.add.missingArgs") + return + } + + val users = getMutipleUsersFromArgs(msg.args).map { it.id } + if (users.isEmpty()) { + msg.replyTranslate("commands.admin.logging.omitUsers.add.404") + return + } + + val settings = asyncTransaction { + LoggingEntity.findById(guild.id.value.toLong())!! + } + + asyncTransaction { + GuildLogging.update({ GuildLogging.id eq guild.id.value.toLong() }) { up -> + up[ignoredUsers] = settings.ignoredUsers + users.map { it.value.toLong() }.toTypedArray() + } + } + + val length = (settings.ignoredUsers + users.map { it.value.toLong() }.toTypedArray()).size + msg.replyTranslate( + "commands.admin.logging.omitUsers.success", + mapOf( + "operation" to "Added", + "users" to length, + "suffix" to if (length != 0 && length == 1) "" else "s" + ) + ) + } + + "remove", "del", "-" -> { + val args = msg.args.drop(1) + if (args.isEmpty()) { + msg.replyTranslate("commands.admin.logging.omitUsers.del.missingArgs") + return + } + + val users = getMutipleUsersFromArgs(msg.args).map { it.id } + if (users.isEmpty()) { + msg.replyTranslate("commands.admin.logging.omitUsers.404") + return + } + + val settings = asyncTransaction { + LoggingEntity.findById(guild.id.value.toLong())!! + } + + val filtered = settings.ignoredUsers.filter { + !users.contains(it.asSnowflake()) + } + + asyncTransaction { + GuildLogging.update({ GuildLogging.id eq guild.id.value.toLong() }) { up -> + up[ignoredUsers] = filtered.toTypedArray() + } + } + + msg.replyTranslate( + "commands.admin.logging.omitUsers.success", + mapOf( + "operation" to "Removed", + "users" to filtered.size, + "suffix" to if (filtered.isNotEmpty() && filtered.size == 1) "" else "s" + ) + ) + } + } + } + + @Subcommand( + "channels", + "descriptions.logging.omitChannels", + aliases = ["c", "comit", "comet", "☄️"] // "comet" and the comet emoji are just... easter eggs I think? + ) + suspend fun omitChannels(msg: CommandMessage) { + val guild = msg.message.getGuild() + if (msg.args.isEmpty()) { + val settings = asyncTransaction { + LoggingEntity.findById(guild.id.value.toLong())!! + } + + // get users from this list + val channels = settings.ignoreChannels.mapNotNull { id -> + NinoScope.future { kord.getChannelOf(id.asSnowflake()) }.await() + }.map { "${it.name} <#${it.id}>" } + + msg.reply { + title = msg.locale.translate("commands.admin.logging.omitChannels.embed.title") + description = msg.locale.translate( + "commands.admin.logging.omitChannels.embed.description", + mapOf( + "list" to if (channels.isEmpty()) { + msg.locale.translate("generic.lonely") + } else { + channels.joinToString("\n") + } + ) + ) + } + + return + } + + when (msg.args.first()) { + "add", "+" -> { + val args = msg.args.drop(1) + if (args.isEmpty()) { + msg.replyTranslate("commands.admin.logging.omitChannels.add.missingArgs") + return + } + + val channels = getMultipleChannelsFromArgs(msg.args).filterIsInstance().map { it.id } + if (channels.isEmpty()) { + msg.replyTranslate("commands.admin.logging.omitChannels.add.404") + return + } + + val settings = asyncTransaction { + LoggingEntity.findById(guild.id.value.toLong())!! + } + + asyncTransaction { + GuildLogging.update({ GuildLogging.id eq guild.id.value.toLong() }) { up -> + up[ignoreChannels] = settings.ignoreChannels + channels.map { it.value.toLong() }.toTypedArray() + } + } + + val length = (settings.ignoreChannels + channels.map { it.value.toLong() }.toTypedArray()).size + msg.replyTranslate( + "commands.admin.logging.omitChannels.success", + mapOf( + "operation" to "Added", + "users" to length, + "suffix" to if (length != 0 && length == 1) "" else "s" + ) + ) + } + + "remove", "del", "-" -> { + val args = msg.args.drop(1) + if (args.isEmpty()) { + msg.replyTranslate("commands.admin.logging.omitChannels.add.missingArgs") + return + } + + val channels = getMultipleChannelsFromArgs(msg.args).filterIsInstance().map { it.id } + if (channels.isEmpty()) { + msg.replyTranslate("commands.admin.logging.omitChannels.add.404") + return + } + + val settings = asyncTransaction { + LoggingEntity.findById(guild.id.value.toLong())!! + } + + val channelList = settings.ignoreChannels.filter { !channels.contains(it.asSnowflake()) } + + asyncTransaction { + GuildLogging.update({ GuildLogging.id eq guild.id.value.toLong() }) { up -> + up[ignoreChannels] = channelList.toTypedArray() + } + } + + val length = channelList.size + msg.replyTranslate( + "commands.admin.logging.omitChannels.success", + mapOf( + "operation" to "Removed", + "users" to length, + "suffix" to if (length != 0 && length == 1) "" else "s" + ) + ) + } + } + } + + @Subcommand( + "events", + "descriptions.logging.events", + aliases = ["ev", "event"], + usage = "[\"enable [events...]\" | \"disable [events...]\"]" + ) + suspend fun events(msg: CommandMessage) { + val guild = msg.message.getGuild() + + if (msg.args.isEmpty()) { + val events = LogEvent.values() + val settings = asyncTransaction { + LoggingEntity.findById(guild.id.value.toLong())!! + } + + msg.reply { + title = msg.locale.translate( + "commands.admin.logging.events.list.embed.title", + mapOf( + "name" to guild.name + ) + ) + + description = msg.locale.translate( + "commands.admin.logging.events.list.embed.description", + mapOf( + "list" to events.joinToString("\n") { + "• ${enabled(settings.events.contains(it.key))} ${it.key}" + } + ) + ) + } + + return + } + + when (msg.args.first()) { + "enable" -> { + val args = msg.args.drop(1) + if (args.isEmpty()) { + val allEvents = LogEvent.values().map { it.key }.toTypedArray() + asyncTransaction { + GuildLogging.update({ + GuildLogging.id eq guild.id.value.toLong() + }) { + it[events] = allEvents + } + } + + msg.reply(":thumbsup: Enabled all logging events!") + return + } + + val all = LogEvent.values().map { it.key.replace(" ", ".") } + val _events = args.filter { + all.contains(it) + } + + if (_events.isEmpty()) { + msg.reply(":question: Invalid events: ${args.joinToString(" ")}. View all with `nino logging events view`") + return + } + + val eventsToEnable = _events.map { LogEvent[it.replace(".", " ")] }.toTypedArray() + asyncTransaction { + GuildLogging.update({ + GuildLogging.id eq guild.id.value.toLong() + }) { + it[events] = eventsToEnable.map { it.key }.toTypedArray() + } + } + + msg.reply("enabled ${eventsToEnable.joinToString(", ")}") + } + + "disable" -> { + val args = msg.args.drop(1) + if (args.isEmpty()) { + asyncTransaction { + GuildLogging.update({ + GuildLogging.id eq guild.id.value.toLong() + }) { + it[events] = arrayOf() + } + } + + msg.reply(":thumbsup: Disabled all logging events!") + return + } + + val all = LogEvent.values().map { it.key.replace(" ", ".") } + val _events = args.filter { + all.contains(it) + } + + if (_events.isEmpty()) { + msg.reply(":question: Invalid events: ${args.joinToString(" ")}. View all with `nino logging events view`") + return + } + + val eventsToDisable = _events.map { LogEvent[it.replace(".", " ")].key }.toTypedArray() + val settings = asyncTransaction { + LoggingEntity.findById(guild.id.value.toLong())!! + } + + asyncTransaction { + GuildLogging.update({ + GuildLogging.id eq guild.id.value.toLong() + }) { + it[events] = settings.events.filter { p -> !eventsToDisable.contains(p) }.toTypedArray() + } + } + + msg.reply("disabled ${eventsToDisable.joinToString(", ")}") + } + + "view" -> { + val typedEvents = LogEvent.values().map { it.key to it.key.replace(" ", ".") } + msg.reply( + buildString { + appendLine("```md") + appendLine("# Logging Events") + appendLine() + + for ((key, set) in typedEvents) { + appendLine("- $key (nino logging events enable $set)") + } + + appendLine() + appendLine("```") + } + ) + } + } + } + + @Subcommand( + "config", + "descriptions.logging.config", + aliases = ["cfg", "info", "list", "view"] + ) + suspend fun config(msg: CommandMessage) { + val guild = msg.message.getGuild() + val settings = asyncTransaction { + LoggingEntity.findById(guild.id.value.toLong())!! + } + + val channel = if (settings.channelId != null) { + kord.getChannelOf(settings.channelId!!.asSnowflake()) + } else { + null + } + + msg.replyTranslate( + "commands.admin.logging.config.message", + mapOf( + "name" to guild.name, + "enabled" to if (settings.enabled) msg.locale.translate("generic.yes") else msg.locale.translate("generic.no"), + "channel" to if (channel == null) msg.locale.translate("generic.nothing") else "#${channel.name} (${channel.id})" + ) + ) + } +} diff --git a/bot/commands/src/main/kotlin/sh/nino/discord/commands/admin/PrefixCommand.kt b/bot/commands/src/main/kotlin/sh/nino/discord/commands/admin/PrefixCommand.kt new file mode 100644 index 00000000..a3511f00 --- /dev/null +++ b/bot/commands/src/main/kotlin/sh/nino/discord/commands/admin/PrefixCommand.kt @@ -0,0 +1,278 @@ +/* + * Copyright (c) 2019-2022 Nino + * + * 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. + */ + +package sh.nino.discord.commands.admin + +import dev.kord.common.entity.Permission +import org.jetbrains.exposed.sql.update +import sh.nino.discord.commands.AbstractCommand +import sh.nino.discord.commands.CommandMessage +import sh.nino.discord.commands.annotations.Command +import sh.nino.discord.commands.annotations.Subcommand +import sh.nino.discord.common.data.Config +import sh.nino.discord.common.extensions.findIndex +import sh.nino.discord.database.asyncTransaction +import sh.nino.discord.database.tables.GuildSettings +import sh.nino.discord.database.tables.Users + +@Command( + name = "prefix", + description = "descriptions.admin.prefix", + usage = "[\"set\" | \"view\" | \"delete\"] [prefix] [--user]", + examples = [ + "{prefix}prefix | Views the custom guild's prefixes", + "{prefix}prefix --user/-u | Views your custom prefixes!", + "{prefix}prefix set ! | Set the guild's prefix, requires Manage Guild permission (without --user/-u flag)!", + "{prefix}prefix delete ! | Removes a guild prefix, requires the Manage Guild permission (without --user/-u flag)!" + ] +) +class PrefixCommand(private val config: Config): AbstractCommand() { + override suspend fun execute(msg: CommandMessage) { + val isUserFlagThere = (msg.flags["user"] ?: msg.flags["u"])?.asBooleanOrNull ?: false + if (isUserFlagThere) { + val prefixes = msg.userSettings.prefixes.toList() + msg.replyTranslate( + "commands.admin.prefix.user.list", + mapOf( + "user" to msg.author.tag, + "list" to prefixes.mapIndexed { i, prefix -> + "• $i. ${prefix.trim()} [...args]" + }.joinToString("\n").ifEmpty { + msg.locale.translate("generic.nothing") + }, + + "prefixes" to config.prefixes.mapIndexed { i, prefix -> + "• $i. ${prefix.trim()} [...args]" + }.joinToString("\n") + ) + ) + + return + } + + val prefixes = msg.settings.prefixes.toList() + val guild = msg.message.getGuild() + + msg.replyTranslate( + "commands.admin.prefix.guild.list", + mapOf( + "name" to guild.name, + "list" to prefixes.mapIndexed { i, prefix -> + "• $i. ${prefix.trim()} [...args]" + }.joinToString("\n").ifEmpty { + msg.locale.translate("generic.nothing") + }, + + "prefixes" to config.prefixes.mapIndexed { i, prefix -> + "• $i. ${prefix.trim()} [...args]" + }.joinToString("\n") + ) + ) + } + + @Subcommand( + "set", + "descriptions.admin.prefix.set", + aliases = ["s", "add", "a"], + usage = "" + ) + suspend fun set(msg: CommandMessage) { + val isUser = (msg.flags["user"] ?: msg.flags["u"])?.asBooleanOrNull ?: false + val permissions = msg.message.getAuthorAsMember()!!.getPermissions() + + if (!isUser && (!permissions.contains(Permission.ManageGuild) || !config.owners.contains("${msg.author.id}"))) { + msg.replyTranslate("commands.admin.prefix.set.noPermission") + return + } + + if (msg.args.isEmpty()) { + msg.replyTranslate("commands.admin.prefix.set.missingPrefix") + return + } + + val prefix = msg.args.joinToString(" ").replace("['\"]".toRegex(), "") + if (prefix.length > 25) { + msg.replyTranslate( + "commands.admin.prefix.set.maxLengthExceeded", + mapOf( + "prefix" to prefix, + "chars.over" to prefix.length - 25 + ) + ) + + return + } + + if (isUser) { + val index = msg.userSettings.prefixes.findIndex { + it.lowercase() == prefix.lowercase() + } + + if (index != -1) { + msg.replyTranslate("commands.admin.prefix.set.alreadySet", mapOf("prefix" to prefix)) + return + } + + val prefixesToAdd = msg.userSettings.prefixes + arrayOf(prefix) + + asyncTransaction { + Users.update({ + Users.id eq msg.author.id.value.toLong() + }) { + it[prefixes] = prefixesToAdd + } + } + + msg.replyTranslate("commands.admin.prefix.set.available", mapOf("prefix" to prefix)) + } else { + val index = msg.settings.prefixes.findIndex { + it.lowercase() == prefix.lowercase() + } + + if (index != -1) { + msg.replyTranslate("commands.admin.prefix.set.alreadySet", mapOf("prefix" to prefix)) + return + } + + val prefixesToAdd = msg.settings.prefixes + arrayOf(prefix) + + asyncTransaction { + GuildSettings.update({ + GuildSettings.id eq msg.guild.id.value.toLong() + }) { + it[prefixes] = prefixesToAdd + } + } + + msg.replyTranslate("commands.admin.prefix.set.available", mapOf("prefix" to prefix)) + } + } + + @Subcommand("reset", "descriptions.admin.prefix.reset") + suspend fun reset(msg: CommandMessage) { + val isUser = (msg.flags["user"] ?: msg.flags["u"])?.asBooleanOrNull ?: false + if (msg.args.isEmpty()) { + return if (isUser) { + displaySelectionForUser(msg) + } else { + displaySelectionForGuild(msg) + } + } + + val prefix = msg.args.joinToString(" ").replace("['\"]".toRegex(), "") + if (prefix.length > 25) { + msg.replyTranslate( + "commands.admin.prefix.set.maxLengthExceeded", + mapOf( + "prefix" to prefix, + "chars.over" to prefix.length - 25 + ) + ) + + return + } + + if (isUser) { + val index = msg.userSettings.prefixes.findIndex { + it.lowercase() == prefix.lowercase() + } + + if (index == -1) { + msg.replyTranslate("commands.admin.prefix.reset.alreadyRemoved", mapOf("prefix" to prefix)) + return + } + + val prefixesToRemove = msg.userSettings.prefixes.filter { + it.lowercase() == prefix.lowercase() + }.toTypedArray() + + asyncTransaction { + Users.update({ + Users.id eq msg.author.id.value.toLong() + }) { + it[prefixes] = prefixesToRemove + } + } + + msg.replyTranslate("commands.admin.prefix.reset.unavailable", mapOf("prefix" to prefix)) + } else { + val index = msg.settings.prefixes.findIndex { + it.lowercase() == prefix.lowercase() + } + + if (index == -1) { + msg.replyTranslate("commands.admin.prefix.reset.alreadyRemoved", mapOf("prefix" to prefix)) + return + } + + val prefixesToRemove = msg.settings.prefixes.filter { + it.lowercase() != prefix.lowercase() + }.toTypedArray() + + asyncTransaction { + GuildSettings.update({ + GuildSettings.id eq msg.guild.id.value.toLong() + }) { + it[prefixes] = prefixesToRemove + } + } + + msg.replyTranslate("commands.admin.prefix.reset.unavailable", mapOf("prefix" to prefix)) + } + } + + private suspend fun displaySelectionForUser(msg: CommandMessage) { + val prefixes = msg.userSettings.prefixes.toList() + msg.reply { + title = msg.locale.translate("commands.admin.prefix.reset.user.embed.title", mapOf("name" to msg.author.tag)) + description = msg.locale.translate( + "commands.admin.prefix.reset.user.embed.description", + mapOf( + "prefixes" to prefixes.mapIndexed { i, prefix -> + "• $i. \"${prefix.trim()} [...args / --flags]" + }.joinToString("\n").ifEmpty { + msg.locale.translate("generic.nothing") + } + ) + ) + } + } + + private suspend fun displaySelectionForGuild(msg: CommandMessage) { + val prefixes = msg.settings.prefixes.toList() + msg.reply { + title = msg.locale.translate("commands.admin.prefix.reset.guild.embed.title", mapOf("name" to msg.guild.name)) + description = msg.locale.translate( + "commands.admin.prefix.reset.guild.embed.description", + mapOf( + "prefixes" to prefixes.mapIndexed { i, prefix -> + "• $i. \"${prefix.trim()} [...args / --flags]" + }.joinToString("\n").ifEmpty { + msg.locale.translate("generic.nothing") + }, + + "config.prefixes" to config.prefixes.joinToString(", ") + ) + ) + } + } +} diff --git a/bot/commands/src/main/kotlin/sh/nino/discord/commands/admin/RoleConfigCommand.kt b/bot/commands/src/main/kotlin/sh/nino/discord/commands/admin/RoleConfigCommand.kt new file mode 100644 index 00000000..c9e0a789 --- /dev/null +++ b/bot/commands/src/main/kotlin/sh/nino/discord/commands/admin/RoleConfigCommand.kt @@ -0,0 +1,214 @@ +/* + * Copyright (c) 2019-2022 Nino + * + * 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. + */ + +package sh.nino.discord.commands.admin + +import org.jetbrains.exposed.sql.update +import sh.nino.discord.commands.AbstractCommand +import sh.nino.discord.commands.CommandCategory +import sh.nino.discord.commands.CommandMessage +import sh.nino.discord.commands.annotations.Command +import sh.nino.discord.commands.annotations.Subcommand +import sh.nino.discord.common.ID_REGEX +import sh.nino.discord.common.extensions.asSnowflake +import sh.nino.discord.common.extensions.runSuspended +import sh.nino.discord.database.asyncTransaction +import sh.nino.discord.database.tables.GuildSettings + +@Command( + name = "rolecfg", + description = "descriptions.admin.rolecfg", + aliases = ["roles", "role-config"], + category = CommandCategory.ADMIN, + examples = [ + "{prefix}rolecfg | View your current role configuration", + "{prefix}rolecfg muted <@&roleId> | Sets the Muted role to that specific role by ID or snowflake.", + "{prefix}rolecfg threads reset | Resets the No Threads role in the database." + ], + + userPermissions = [0x00000020] // ManageGuild +) +class RoleConfigCommand: AbstractCommand() { + override suspend fun execute(msg: CommandMessage) { + val guild = msg.message.getGuild() + val mutedRole = runSuspended { + if (msg.settings.mutedRoleId == null) { + msg.locale.translate("generic.nothing") + } else { + val role = guild.getRole(msg.settings.mutedRoleId!!.asSnowflake()) + role.name + } + } + + val noThreadsRole = runSuspended { + if (msg.settings.noThreadsRoleId == null) { + msg.locale.translate("generic.nothing") + } else { + val role = guild.getRole(msg.settings.noThreadsRoleId!!.asSnowflake()) + role.name + } + } + + msg.replyTranslate( + "commands.admin.rolecfg.message", + mapOf( + "guild" to guild.name, + "mutedRole" to mutedRole, + "noThreadsRole" to noThreadsRole + ) + ) + } + + @Subcommand( + "muted", + "descriptions.admin.rolecfg.muted", + aliases = ["m", "moot", "shutup"], + usage = "" + ) + suspend fun muted(msg: CommandMessage) { + if (msg.args.isEmpty()) { + msg.replyTranslate("commands.admin.rolecfg.muted.noArgs") + return + } + + when (msg.args.first()) { + "reset" -> { + // Check if a muted role was not already defined + if (msg.settings.mutedRoleId == null) { + msg.replyTranslate("commands.admin.rolecfg.muted.noMutedRole") + return + } + + asyncTransaction { + GuildSettings.update({ + GuildSettings.id eq msg.guild.id.value.toLong() + }) { + it[mutedRoleId] = null + } + } + + msg.replyTranslate("commands.admin.rolecfg.muted.reset.success") + } + + else -> { + val roleId = msg.args.first() + val role = msg.kord.defaultSupplier.getRoleOrNull(msg.guild.id, roleId.asSnowflake()) + + // Check if it's a valid snowflake + if (ID_REGEX.toRegex().matches(roleId)) { + if (role == null) { + msg.replyTranslate( + "commands.admin.rolecfg.unknownRole", + mapOf( + "roleId" to roleId + ) + ) + + return + } + + asyncTransaction { + GuildSettings.update({ + GuildSettings.id eq msg.guild.id.value.toLong() + }) { + it[mutedRoleId] = roleId.toLong() + } + } + + msg.replyTranslate( + "commands.admin.rolecfg.muted.set.success", + mapOf( + "name" to role.name + ) + ) + } + } + } + } + + @Subcommand( + "noThreads", + "descriptions.admin.rolecfg.noThreads", + aliases = ["threads", "t"], + usage = "" + ) + suspend fun noThreads(msg: CommandMessage) { + if (msg.args.isEmpty()) { + msg.replyTranslate("commands.admin.rolecfg.noThreads.noArgs") + return + } + + when (msg.args.first()) { + "reset" -> { + // Check if a no threads role was not already defined + if (msg.settings.noThreadsRoleId == null) { + msg.replyTranslate("commands.admin.rolecfg.noThreads.noRoleId") + return + } + + asyncTransaction { + GuildSettings.update({ + GuildSettings.id eq msg.guild.id.value.toLong() + }) { + it[noThreadsRoleId] = null + } + } + + msg.replyTranslate("commands.admin.rolecfg.noThreads.reset.success") + } + + else -> { + val roleId = msg.args.first() + val role = msg.kord.defaultSupplier.getRoleOrNull(msg.guild.id, roleId.asSnowflake()) + + // Check if it's a valid snowflake + if (ID_REGEX.toRegex().matches(roleId)) { + if (role == null) { + msg.replyTranslate( + "commands.admin.rolecfg.unknownRole", + mapOf( + "roleId" to roleId + ) + ) + + return + } + + asyncTransaction { + GuildSettings.update({ + GuildSettings.id eq msg.guild.id.value.toLong() + }) { + it[mutedRoleId] = roleId.toLong() + } + } + + msg.replyTranslate( + "commands.admin.rolecfg.noThreads.set.success", + mapOf( + "name" to role.name + ) + ) + } + } + } + } +} diff --git a/bot/commands/src/main/kotlin/sh/nino/discord/commands/admin/_Module.kt b/bot/commands/src/main/kotlin/sh/nino/discord/commands/admin/_Module.kt new file mode 100644 index 00000000..d37aab72 --- /dev/null +++ b/bot/commands/src/main/kotlin/sh/nino/discord/commands/admin/_Module.kt @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2019-2022 Nino + * + * 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. + */ + +package sh.nino.discord.commands.admin + +import org.koin.dsl.bind +import org.koin.dsl.module +import sh.nino.discord.commands.AbstractCommand + +val adminCommandsModule = module { + single { AutomodCommand() } bind AbstractCommand::class + single { ExportCommand(get(), get()) } bind AbstractCommand::class + single { ImportCommand(get(), get()) } bind AbstractCommand::class + single { LoggingCommand(get()) } bind AbstractCommand::class + single { PrefixCommand(get()) } bind AbstractCommand::class + single { RoleConfigCommand() } bind AbstractCommand::class +} diff --git a/bot/commands/src/main/kotlin/sh/nino/discord/commands/annotations/Command.kt b/bot/commands/src/main/kotlin/sh/nino/discord/commands/annotations/Command.kt new file mode 100644 index 00000000..ae6be4b1 --- /dev/null +++ b/bot/commands/src/main/kotlin/sh/nino/discord/commands/annotations/Command.kt @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2019-2022 Nino + * + * 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. + */ + +package sh.nino.discord.commands.annotations + +import sh.nino.discord.commands.CommandCategory + +annotation class Command( + val name: String, + val description: String = "descriptions.unknown", + val usage: String = "", + val category: CommandCategory = CommandCategory.CORE, + val cooldown: Int = 5, + val ownerOnly: Boolean = false, + val userPermissions: LongArray = [], + val botPermissions: LongArray = [], + val examples: Array = [], + val aliases: Array = [] +) diff --git a/bot/commands/src/main/kotlin/sh/nino/discord/commands/annotations/Subcommand.kt b/bot/commands/src/main/kotlin/sh/nino/discord/commands/annotations/Subcommand.kt new file mode 100644 index 00000000..d143863b --- /dev/null +++ b/bot/commands/src/main/kotlin/sh/nino/discord/commands/annotations/Subcommand.kt @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2019-2022 Nino + * + * 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. + */ + +package sh.nino.discord.commands.annotations + +annotation class Subcommand( + val name: String, + val description: String, + val usage: String = "", + val aliases: Array = [], + val permissions: LongArray = [] +) diff --git a/bot/commands/src/main/kotlin/sh/nino/discord/commands/core/HelpCommand.kt b/bot/commands/src/main/kotlin/sh/nino/discord/commands/core/HelpCommand.kt new file mode 100644 index 00000000..a3c269ab --- /dev/null +++ b/bot/commands/src/main/kotlin/sh/nino/discord/commands/core/HelpCommand.kt @@ -0,0 +1,187 @@ +/* + * Copyright (c) 2019-2022 Nino + * + * 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. + */ + +package sh.nino.discord.commands.core + +import dev.kord.core.Kord +import sh.nino.discord.commands.* +import sh.nino.discord.commands.annotations.Command +import sh.nino.discord.common.data.Config +import java.util.* + +@Command( + name = "help", + description = "descriptions.core.help", + aliases = ["halp", "?", "h", "cmds", "command"] +) +class HelpCommand(private val handler: CommandHandler, private val config: Config, private val kord: Kord): AbstractCommand() { + private val commandByCategoryCache = mutableMapOf>() + + override suspend fun execute(msg: CommandMessage) = if (msg.args.isEmpty()) renderHelpCommand(msg) else renderCommandHelp(msg) + + private suspend fun renderHelpCommand(msg: CommandMessage) { + // Cache the commands by their category if the cache is empty + if (commandByCategoryCache.isEmpty()) { + for (command in handler.commands.values) { + // We do not add easter eggs + system commands + if (command.category == CommandCategory.SYSTEM || command.category == CommandCategory.EASTER_EGG) continue + + // Check if it was cached + if (!commandByCategoryCache.containsKey(command.category)) + commandByCategoryCache[command.category] = mutableListOf() + + commandByCategoryCache[command.category]!!.add(command) + } + } + + val prefixes = (msg.settings.prefixes + msg.userSettings.prefixes + config.prefixes).distinct() + val prefix = prefixes.random() + + val self = kord.getSelf() + msg.reply { + title = "${self.username}#${self.discriminator} | Command List" + description = buildString { + appendLine(":pencil2: For more documentation on a command, you can type [${prefix}help ](https://nino.sh/commands), replace **** is the command or module you want to view.") + appendLine() + appendLine("There are currently **${handler.commands.size}** commands available.") + appendLine("[**Privacy Policy**](https://nino.sh/privacy) | [**Terms of Service**](https://nino.sh/tos)") + } + + for ((cat, commands) in commandByCategoryCache) { + field { + name = "${cat.emoji} ${cat.key}" + value = commands.joinToString(", ") { "**`${it.name}`**" } + inline = false + } + } + } + } + + private suspend fun renderCommandHelp(msg: CommandMessage) { + // You can basically do "nino [subcommand] -h" to execute the subcommand's + // information or do "nino -h" to execute the command's information. + // + // BUT! In the help command, if you do "nino help [subcommand]", + // it will run the subcommand's information, and if you do "nino help " + // it'll render the command or module's information + + val prefixes = (msg.settings.prefixes + msg.userSettings.prefixes + config.prefixes).distinct() + val prefix = prefixes.random() + val arg = msg.args.first() + + if (arg == "usage") { + msg.reply { + title = "[ Nino's Command Usage Guide ]" + description = buildString { + appendLine("Hallo **${msg.author.tag}**! I am here to help you on how to do arguments for text-based commands") + appendLine("when using me for your moderation needs! A more of a detailed guide can be seen on my [website](https://nino.sh/docs/getting-started/syntax)!") + appendLine("To see what I mean, I will give you a visualization of the arguments broken down:") + appendLine() + appendLine("```") + appendLine("|-----------------------------------------------------|") + appendLine("| |") + appendLine("| x! help [ cmdOrMod | \"usage\"] |") + appendLine("| ^ ^ ^ ^ ^ ^ |") + appendLine("| | | | | | | |") + appendLine("| | | | | | | |") + appendLine("| | | | / | | |") + appendLine("| | | | / | | |") + appendLine("| | | |/ - name | | |") + appendLine("| | | | | | |") + appendLine("| prefix command param \"or\" literal |") + appendLine("|-----------------------------------------------------|") + appendLine("```") + appendLine("If you didn't get this visualization, that's completely alright! I'll give you a run down:") + appendLine("- **prefix** refers to the command prefix you executed, like **${prefixes.random()}**!") + appendLine("- **command** refers to the command's name or the alias of that executed command.") + appendLine("- **param** is referred to a command parameter, or an argument! It'll be referenced with the prefix of `[` or `<`") + appendLine(" If a parameter starts with `[`, it is referred as a optional argument, so you don't need to use it!") + appendLine(" If a parameter starts with `<`, it is referred as a required argument, so you are required to specify an argument or the command will not work. :<") + appendLine("In the **2.x** release of Nino, we added the ability to use slash commands when executing commands, but it is very limiting!") + } + } + + return + } + + // Check if there is 2 arguments supplied + if (msg.args.size == 2) { + val (command, subcommand) = msg.args + val cmd = handler.commands.values.firstOrNull { + (it.name == command || it.aliases.contains(command)) && (it.category != CommandCategory.EASTER_EGG && it.category != CommandCategory.SYSTEM) + } + + if (cmd != null) { + val subcmd = cmd.thiz.subcommands.firstOrNull { + it.name == subcommand || it.aliases.contains(command) + } + + if (subcmd != null) { + subcmd.help(msg) + } else { + msg.reply("Command **$command** existed but not subcommand **$subcommand**.") + } + } else { + msg.reply("Command **$command** doesn't exist.") + } + } else { + val command = handler.commands.values.firstOrNull { + (it.name == arg || it.aliases.contains(arg)) && (it.category != CommandCategory.EASTER_EGG && it.category != CommandCategory.SYSTEM) + } + + if (command != null) { + command.help(msg) + } else { + // Check if it is a module + val module = handler.commands.values.filter { + it.category.key.lowercase() == arg.lowercase() && !listOf("system", "easter_egg").contains(arg.lowercase()) + } + + if (module.isNotEmpty()) { + val propLen = { name: String -> name.length } + val longestCommandName = propLen( + module.sortedWith { a, b -> + propLen(b.name) - propLen(a.name) + }.first().name + ) + + msg.reply { + title = "[ Module ${arg.replaceFirstChar { if (it.isLowerCase()) it.titlecase(Locale.getDefault()) else it.toString() }} ]" + description = buildString { + for (c in module) { + val description = try { + msg.locale.translate(c.description) + } catch (e: Exception) { + "*not translated yet!*" + } + + appendLine("`$prefix${c.name.padEnd((longestCommandName * 2) - c.name.length, '\u200b')}` |\u200b \u200b$description") + } + } + } + } else { + msg.reply("Command or module **$arg** doesn't exist. :(") + } + } + } + } +} diff --git a/bot/commands/src/main/kotlin/sh/nino/discord/commands/core/InviteMeCommand.kt b/bot/commands/src/main/kotlin/sh/nino/discord/commands/core/InviteMeCommand.kt new file mode 100644 index 00000000..d3f57a7f --- /dev/null +++ b/bot/commands/src/main/kotlin/sh/nino/discord/commands/core/InviteMeCommand.kt @@ -0,0 +1,48 @@ +/* + * Copyright (c) 2019-2022 Nino + * + * 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. + */ + +package sh.nino.discord.commands.core + +import sh.nino.discord.commands.AbstractCommand +import sh.nino.discord.commands.CommandMessage +import sh.nino.discord.commands.annotations.Command + +@Command( + "invite", + "descriptions.core.invite", + aliases = ["inviteme", "i"] +) +class InviteMeCommand: AbstractCommand() { + override suspend fun execute(msg: CommandMessage) { + msg.reply( + buildString { + appendLine(":wave: Hello, **${msg.author.tag}**! Thanks for considering inviting me, you can invite") + appendLine("me using the \"Add Server\" button when you click my profile or you can use the link below:") + appendLine("****") + appendLine() + appendLine(":question: Need any help when using my moderation features? You can read the FAQ at ****") + appendLine("or you can directly ask in the **Noelware** Discord server:") + appendLine("https://discord.gg/ATmjFH9kMH") + } + ) + } +} diff --git a/bot/commands/src/main/kotlin/sh/nino/discord/commands/core/PingCommand.kt b/bot/commands/src/main/kotlin/sh/nino/discord/commands/core/PingCommand.kt new file mode 100644 index 00000000..250017f8 --- /dev/null +++ b/bot/commands/src/main/kotlin/sh/nino/discord/commands/core/PingCommand.kt @@ -0,0 +1,105 @@ +/* + * Copyright (c) 2019-2022 Nino + * + * 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. + */ + +package sh.nino.discord.commands.core + +import kotlinx.coroutines.flow.filter +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.map +import org.apache.commons.lang3.time.StopWatch +import sh.nino.discord.commands.AbstractCommand +import sh.nino.discord.commands.CommandMessage +import sh.nino.discord.commands.annotations.Command +import sh.nino.discord.common.DEDI_NODE +import sh.nino.discord.common.extensions.humanize +import sh.nino.discord.common.extensions.runSuspended +import sh.nino.discord.core.redis.RedisManager +import sh.nino.discord.database.asyncTransaction +import java.util.concurrent.TimeUnit + +@Command( + "ping", + "descriptions.core.ping", + aliases = ["amionline", "pang", "peng", "pong", "pung", "latency"], + cooldown = 2 +) +class PingCommand(private val redis: RedisManager): AbstractCommand() { + private val messages = listOf( + "what does the fox say?", + ":fox:", + ":polar_bear:", + "im a pretty girl, im a pretty girl!", + "im the best quintuplet!", + "sometimes, life sucks! but, you'll get better! <3", + "yiff", + ":thinking: are potatoes really food?" + ) + + override suspend fun execute(msg: CommandMessage) { + val random = messages.random() + + val stopwatch = StopWatch.createStarted() + val message = msg.reply(random) + stopwatch.stop() + + val delStopwatch = StopWatch.createStarted() + message.delete() + delStopwatch.stop() + + // Now, we calculate Redis + Postgre ping + val redisPing = redis.getPing().inWholeMilliseconds + val postgresPing = runSuspended { + val sw = StopWatch.createStarted() + asyncTransaction { + exec("SELECT 1;") { + it.close() + } + } + + sw.stop() + sw.getTime(TimeUnit.MILLISECONDS) + } + + val (shardId) = msg.kord.guilds.map { + ((it.id.value.toLong() shr 22) % msg.kord.gateway.gateways.size) to it.id.value.toLong() + }.filter { + it.second == msg.guild.id.value.toLong() + }.first() + + val gateway = msg.kord.gateway.gateways[shardId.toInt()]!! + msg.reply( + buildString { + if (DEDI_NODE != "none") { + appendLine(":satellite_orbital: Running under node **$DEDI_NODE**") + appendLine() + } + + appendLine("**Deleting Messages**: ${delStopwatch.getTime(TimeUnit.MILLISECONDS).humanize(long = false, includeMs = true)}") + appendLine("**Sending Messages**: ${stopwatch.getTime(TimeUnit.MILLISECONDS).humanize(long = false, includeMs = true)}") + appendLine("**Shard #$shardId**: ${if (gateway.ping.value == null) "?" else gateway.ping.value!!.inWholeMilliseconds.humanize(long = false, includeMs = true)}") + appendLine("**All Shards**: ${if (msg.guild.kord.gateway.averagePing == null) "" else msg.guild.kord.gateway.averagePing!!.inWholeMilliseconds.humanize(long = false, includeMs = true)}") + appendLine("**PostgreSQL**: ${postgresPing.humanize(long = false, includeMs = true)}") + appendLine("**Redis**: ${redisPing.humanize(long = false, includeMs = true)}") + } + ) + } +} diff --git a/bot/commands/src/main/kotlin/sh/nino/discord/commands/core/ShardInfoCommand.kt b/bot/commands/src/main/kotlin/sh/nino/discord/commands/core/ShardInfoCommand.kt new file mode 100644 index 00000000..e8581777 --- /dev/null +++ b/bot/commands/src/main/kotlin/sh/nino/discord/commands/core/ShardInfoCommand.kt @@ -0,0 +1,90 @@ +/* + * Copyright (c) 2019-2022 Nino + * + * 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. + */ + +package sh.nino.discord.commands.core + +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.toList +import org.apache.commons.lang3.time.StopWatch +import sh.nino.discord.commands.AbstractCommand +import sh.nino.discord.commands.CommandMessage +import sh.nino.discord.commands.annotations.Command +import sh.nino.discord.common.extensions.asSnowflake +import sh.nino.discord.common.extensions.humanize +import java.util.concurrent.TimeUnit +import kotlin.time.Duration + +private data class ShardInfo( + val guilds: MutableList, + var users: Int, + val ping: Duration +) + +@Command( + "shardinfo", + "descriptions.core.shardinfo", + aliases = ["shards"] +) +class ShardInfoCommand: AbstractCommand() { + override suspend fun execute(msg: CommandMessage) { + val stopwatch = StopWatch.createStarted() + val message = msg.reply(":thinking: Now calculating shard information...") + + val guildShardMap = msg.kord.guilds.map { + ((it.id.value.toLong() shr 22) % msg.kord.gateway.gateways.size) to it.id.value.toLong() + }.toList() + + // TODO: i don't think this will scale well + // but, oh well! + val shardMap = mutableMapOf() + for ((id, guildId) in guildShardMap) { + if (!shardMap.containsKey(id)) { + shardMap[id] = ShardInfo(mutableListOf(), 0, msg.kord.gateway.gateways[id.toInt()]!!.ping.value ?: Duration.ZERO) + } + + val shardInfo = shardMap[id]!! + shardInfo.guilds.add(guildId) + shardInfo.users += msg.kord.getGuild(guildId.asSnowflake())!!.memberCount ?: 0 + + shardMap[id] = shardInfo + } + + message.delete() + stopwatch.stop() + + val currentShard = guildShardMap.first { it.second == msg.guild.id.value.toLong() }.first + msg.reply( + buildString { + appendLine("```md") + appendLine("# Shard Information") + appendLine("> Took ${stopwatch.getTime(TimeUnit.MILLISECONDS).humanize(long = false, includeMs = true)} to calculate!") + appendLine() + + for ((id, info) in shardMap) { + appendLine("* Shard #$id${if (currentShard == id) " (Current)" else ""} | G: ${info.guilds.size} - U: ${info.users} - P: ${if (info.ping == Duration.ZERO) "?" else info.ping.inWholeMilliseconds.humanize(long = false, includeMs = true)}") + } + + appendLine("```") + } + ) + } +} diff --git a/bot/commands/src/main/kotlin/sh/nino/discord/commands/core/SourceCommand.kt b/bot/commands/src/main/kotlin/sh/nino/discord/commands/core/SourceCommand.kt new file mode 100644 index 00000000..b51ebeb0 --- /dev/null +++ b/bot/commands/src/main/kotlin/sh/nino/discord/commands/core/SourceCommand.kt @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2019-2022 Nino + * + * 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. + */ + +package sh.nino.discord.commands.core + +import sh.nino.discord.commands.AbstractCommand +import sh.nino.discord.commands.CommandMessage +import sh.nino.discord.commands.annotations.Command + +@Command( + "source", + "descriptions.core.source", + aliases = ["github", "code", "sauce"] +) +class SourceCommand: AbstractCommand() { + override suspend fun execute(msg: CommandMessage) { + msg.reply(":eyes: **Here you go ${msg.author.tag}**: ") + } +} diff --git a/bot/commands/src/main/kotlin/sh/nino/discord/commands/core/StatisticsCommand.kt b/bot/commands/src/main/kotlin/sh/nino/discord/commands/core/StatisticsCommand.kt new file mode 100644 index 00000000..361c42c8 --- /dev/null +++ b/bot/commands/src/main/kotlin/sh/nino/discord/commands/core/StatisticsCommand.kt @@ -0,0 +1,219 @@ +/* + * Copyright (c) 2019-2022 Nino + * + * 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. + */ + +package sh.nino.discord.commands.core + +import kotlinx.coroutines.flow.count +import org.apache.commons.lang3.time.StopWatch +import sh.nino.discord.commands.AbstractCommand +import sh.nino.discord.commands.CommandMessage +import sh.nino.discord.commands.annotations.Command +import sh.nino.discord.common.DEDI_NODE +import sh.nino.discord.common.NinoInfo +import sh.nino.discord.common.extensions.formatSize +import sh.nino.discord.common.extensions.humanize +import sh.nino.discord.common.extensions.reduceWith +import sh.nino.discord.common.extensions.titleCase +import sh.nino.discord.core.redis.RedisManager +import sh.nino.discord.database.asyncTransaction +import java.lang.management.ManagementFactory +import java.util.concurrent.TimeUnit +import kotlin.math.floor +import kotlin.time.Duration.Companion.milliseconds + +private data class DatabaseStats( + val version: String, + val fetched: Long, + val updated: Long, + val deleted: Long, + val inserted: Long, + val uptime: Long, + val ping: Long +) + +@Command( + "statistics", + "descriptions.core.statistics", + aliases = ["stats", "botinfo", "me", "info"], + cooldown = 8 +) +class StatisticsCommand(private val redis: RedisManager): AbstractCommand() { + override suspend fun execute(msg: CommandMessage) { + val self = msg.kord.getSelf() + val guilds = msg.kord.guilds.count() + val processHandle = ProcessHandle.current() + val runtime = Runtime.getRuntime() + val os = ManagementFactory.getOperatingSystemMXBean() + val memory = ManagementFactory.getMemoryMXBean() + + val users = msg.kord.guilds.reduceWith(0) { acc, curr -> + val res = curr.memberCount ?: 0 + acc + res + } + + val channels = msg.kord.guilds.reduceWith(0) { acc, guild -> + val chan = guild.channels.count() + acc + chan + } + + val stats = getDbStats() + val redis = redis.getStats() + + msg.reply( + buildString { + appendLine("```md") + appendLine("# ${self.tag} - Statistics${if (DEDI_NODE != "none") " [$DEDI_NODE]" else ""}") + appendLine("> This is a bunch of ~~useless~~ statistics you might care, I don't know!") + appendLine() + + appendLine("## Bot") + appendLine("* Channels: $channels") + appendLine("* Guilds: $guilds") + appendLine("* Shards: ${msg.kord.gateway.gateways.size} (~${msg.kord.gateway.averagePing?.inWholeMilliseconds ?: 0}ms)") + appendLine("* Users: $users") + appendLine() + + appendLine("## Process [${processHandle.pid()}]") + appendLine("* Memory Usage [Used / Total]: ${memory.heapMemoryUsage.used.formatSize()}") + appendLine("* JVM Memory [Free / Total]: ${runtime.freeMemory().formatSize()} / ${runtime.totalMemory().formatSize()}") + appendLine("* CPU Processor Count: ${runtime.availableProcessors()}") + appendLine("* Operating System: ${os.name} (${os.arch}; ${os.version})") + appendLine("* CPU Load: ${os.systemLoadAverage}%") + appendLine("* Uptime: ${ManagementFactory.getRuntimeMXBean().uptime.humanize(long = true, includeMs = false)}") + appendLine() + + appendLine("## Versions") + appendLine("* Kotlin: v${KotlinVersion.CURRENT}") + appendLine("* Java: v${System.getProperty("java.version")} (${System.getProperty("java.vendor")})") + appendLine("* Nino: v${NinoInfo.VERSION} (${NinoInfo.COMMIT_SHA} - ${NinoInfo.BUILD_DATE})") + appendLine("* Kord: v0.8.0-M9") + appendLine() + + appendLine("## PostgreSQL [${stats.version}]") + appendLine("* Uptime: ${stats.uptime.humanize(true, includeMs = true)}") + appendLine("* Ping: ${stats.ping}ms") + appendLine("* Query Stats:") + appendLine(" * Fetched: ${stats.fetched} documents") + appendLine(" * Updated: ${stats.updated} documents") + appendLine(" * Deleted: ${stats.deleted} documents") + appendLine(" * Inserted: ${stats.inserted} documents") + appendLine() + + appendLine("# Redis [v${redis.serverStats["redis_version"]} - ${redis.serverStats["redis_mode"]!!.titleCase()})") + appendLine("* Network Input/Output: ${redis.stats["total_net_input_bytes"]!!.toLong().formatSize()} / ${redis.stats["total_net_output_bytes"]!!.toLong().formatSize()}") + appendLine("* Operations/s: ${redis.stats["instantaneous_ops_per_sec"]}") + appendLine("* Uptime: ${(redis.serverStats["uptime_in_seconds"]!!.toLong() * 1000).humanize(false, includeMs = false)}") + appendLine("* Ping: ${redis.ping.inWholeMilliseconds}ms") + + appendLine("```") + } + ) + } + + private suspend fun getDbStats(): DatabaseStats { + // Get the ping of the database + val sw = StopWatch.createStarted() + asyncTransaction { + exec("SELECT 1;") { + it.close() + } + } + + sw.stop() + + val version = asyncTransaction { + exec("SELECT version();") { + if (!it.next()) return@exec "?" + + val version = it.getString("version") + it.close() + + return@exec version + }!! + } + + val uptime = asyncTransaction { + exec("SELECT extract(epoch FROM current_timestamp - pg_postmaster_start_time()) AS uptime;") { + if (!it.next()) return@exec 0.1 + + val uptime = it.getDouble("uptime") + it.close() + + uptime + } + } + + val fetched = asyncTransaction { + exec("SELECT tup_fetched FROM pg_stat_database;") { + if (!it.next()) return@exec 0L + + val fetched = it.getLong("tup_fetched") + it.close() + + fetched + } + } + + val deleted = asyncTransaction { + exec("SELECT tup_deleted FROM pg_stat_database;") { + if (!it.next()) return@exec 0L + + val deleted = it.getLong("tup_deleted") + it.close() + + deleted + } + } + + val updated = asyncTransaction { + exec("SELECT tup_updated FROM pg_stat_database;") { + if (!it.next()) return@exec 0L + + val updated = it.getLong("tup_updated") + it.close() + + updated + } + } + + val inserted = asyncTransaction { + exec("SELECT tup_inserted FROM pg_stat_database;") { + if (!it.next()) return@exec 0L + + val inserted = it.getLong("tup_inserted") + it.close() + + inserted + } + } + + return DatabaseStats( + version = if (version == "?") "?" else version.split(" ")[1], + uptime = floor(uptime!! * 1000.0).milliseconds.inWholeMilliseconds, + ping = sw.getTime(TimeUnit.MILLISECONDS), + fetched = fetched!!, + updated = updated!!, + deleted = deleted!!, + inserted = inserted!! + ) + } +} diff --git a/bot/commands/src/main/kotlin/sh/nino/discord/commands/core/UptimeCommand.kt b/bot/commands/src/main/kotlin/sh/nino/discord/commands/core/UptimeCommand.kt new file mode 100644 index 00000000..d7dc9374 --- /dev/null +++ b/bot/commands/src/main/kotlin/sh/nino/discord/commands/core/UptimeCommand.kt @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2019-2022 Nino + * + * 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. + */ + +package sh.nino.discord.commands.core + +import sh.nino.discord.commands.AbstractCommand +import sh.nino.discord.commands.CommandMessage +import sh.nino.discord.commands.annotations.Command +import sh.nino.discord.common.extensions.humanize +import sh.nino.discord.core.NinoBot + +@Command( + "uptime", + "descriptions.core.uptime", + aliases = ["upfor", "alive", "rualive"] +) +class UptimeCommand(private val nino: NinoBot): AbstractCommand() { + override suspend fun execute(msg: CommandMessage) { + msg.reply(":gear: **${(System.currentTimeMillis() - nino.bootTime).humanize(true, includeMs = false)}**") + } +} diff --git a/bot/commands/src/main/kotlin/sh/nino/discord/commands/core/_Module.kt b/bot/commands/src/main/kotlin/sh/nino/discord/commands/core/_Module.kt new file mode 100644 index 00000000..132a32fd --- /dev/null +++ b/bot/commands/src/main/kotlin/sh/nino/discord/commands/core/_Module.kt @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2019-2022 Nino + * + * 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. + */ + +package sh.nino.discord.commands.core + +import org.koin.dsl.bind +import org.koin.dsl.module +import sh.nino.discord.commands.AbstractCommand + +val coreCommandsModule = module { + single { HelpCommand(get(), get(), get()) } bind AbstractCommand::class + single { InviteMeCommand() } bind AbstractCommand::class + single { PingCommand(get()) } bind AbstractCommand::class + single { ShardInfoCommand() } bind AbstractCommand::class + single { SourceCommand() } bind AbstractCommand::class + single { StatisticsCommand(get()) } bind AbstractCommand::class + single { UptimeCommand(get()) } bind AbstractCommand::class +} diff --git a/bot/commands/src/main/kotlin/sh/nino/discord/commands/easter_egg/LonelyCommand.kt b/bot/commands/src/main/kotlin/sh/nino/discord/commands/easter_egg/LonelyCommand.kt new file mode 100644 index 00000000..0c0c52ee --- /dev/null +++ b/bot/commands/src/main/kotlin/sh/nino/discord/commands/easter_egg/LonelyCommand.kt @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2019-2022 Nino + * + * 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. + */ + +package sh.nino.discord.commands.easter_egg + +import sh.nino.discord.commands.AbstractCommand +import sh.nino.discord.commands.CommandCategory +import sh.nino.discord.commands.CommandMessage +import sh.nino.discord.commands.annotations.Command + +@Command( + "lonely", + "I wonder what this could be? I don't really know myself...", + aliases = ["owo", "lone", ":eyes:"], + category = CommandCategory.EASTER_EGG +) +class LonelyCommand: AbstractCommand() { + override suspend fun execute(msg: CommandMessage) { + msg.replyTranslate("generic.lonely") + } +} diff --git a/bot/commands/src/main/kotlin/sh/nino/discord/commands/easter_egg/TestCommand.kt b/bot/commands/src/main/kotlin/sh/nino/discord/commands/easter_egg/TestCommand.kt new file mode 100644 index 00000000..c9859588 --- /dev/null +++ b/bot/commands/src/main/kotlin/sh/nino/discord/commands/easter_egg/TestCommand.kt @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2019-2022 Nino + * + * 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. + */ + +package sh.nino.discord.commands.easter_egg + +import sh.nino.discord.commands.AbstractCommand +import sh.nino.discord.commands.CommandCategory +import sh.nino.discord.commands.CommandMessage +import sh.nino.discord.commands.annotations.Command + +@Command( + name = "test", + description = "A secret test command. :eyes:", + category = CommandCategory.EASTER_EGG +) +class TestCommand: AbstractCommand() { + override suspend fun execute(msg: CommandMessage) { + msg.reply("blep!") + } +} diff --git a/bot/commands/src/main/kotlin/sh/nino/discord/commands/easter_egg/WahCommand.kt b/bot/commands/src/main/kotlin/sh/nino/discord/commands/easter_egg/WahCommand.kt new file mode 100644 index 00000000..90f065ef --- /dev/null +++ b/bot/commands/src/main/kotlin/sh/nino/discord/commands/easter_egg/WahCommand.kt @@ -0,0 +1,59 @@ +/* + * Copyright (c) 2019-2022 Nino + * + * 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. + */ + +package sh.nino.discord.commands.easter_egg + +import io.ktor.client.* +import io.ktor.client.call.* +import io.ktor.client.request.* +import io.ktor.client.statement.* +import kotlinx.serialization.Serializable +import sh.nino.discord.commands.AbstractCommand +import sh.nino.discord.commands.CommandCategory +import sh.nino.discord.commands.CommandMessage +import sh.nino.discord.commands.annotations.Command + +@Serializable +data class WahResponse( + val link: String +) + +@Command( + name = "wah", + description = "beautiful wah :D", + category = CommandCategory.EASTER_EGG, + aliases = ["wah", "weh", "pamda", "PANDUH", "panduh", "panda"] +) +class WahCommand(private val httpClient: HttpClient): AbstractCommand() { + override suspend fun execute(msg: CommandMessage) { + val res: HttpResponse = httpClient.get("https://some-random-api.ml/img/red_panda") + val body = res.receive() + + msg.reply { + title = "wah!" + image = body.link + footer { + text = "good job on finding a easter egg command!" + } + } + } +} diff --git a/bot/commands/src/main/kotlin/sh/nino/discord/commands/easter_egg/_Module.kt b/bot/commands/src/main/kotlin/sh/nino/discord/commands/easter_egg/_Module.kt new file mode 100644 index 00000000..cfb553ab --- /dev/null +++ b/bot/commands/src/main/kotlin/sh/nino/discord/commands/easter_egg/_Module.kt @@ -0,0 +1,33 @@ +/* + * Copyright (c) 2019-2022 Nino + * + * 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. + */ + +package sh.nino.discord.commands.easter_egg + +import org.koin.dsl.bind +import org.koin.dsl.module +import sh.nino.discord.commands.AbstractCommand + +val easterEggCommandModule = module { + single { TestCommand() } bind AbstractCommand::class + single { WahCommand(get()) } bind AbstractCommand::class + single { LonelyCommand() } bind AbstractCommand::class +} diff --git a/bot/commands/src/main/kotlin/sh/nino/discord/commands/moderation/BanCommand.kt b/bot/commands/src/main/kotlin/sh/nino/discord/commands/moderation/BanCommand.kt new file mode 100644 index 00000000..8776dd6a --- /dev/null +++ b/bot/commands/src/main/kotlin/sh/nino/discord/commands/moderation/BanCommand.kt @@ -0,0 +1,23 @@ +/* + * Copyright (c) 2019-2022 Nino + * + * 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. + */ + +package sh.nino.discord.commands.moderation diff --git a/bot/commands/src/main/kotlin/sh/nino/discord/commands/moderation/CaseCommand.kt b/bot/commands/src/main/kotlin/sh/nino/discord/commands/moderation/CaseCommand.kt new file mode 100644 index 00000000..8776dd6a --- /dev/null +++ b/bot/commands/src/main/kotlin/sh/nino/discord/commands/moderation/CaseCommand.kt @@ -0,0 +1,23 @@ +/* + * Copyright (c) 2019-2022 Nino + * + * 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. + */ + +package sh.nino.discord.commands.moderation diff --git a/bot/commands/src/main/kotlin/sh/nino/discord/commands/moderation/HistoryCommand.kt b/bot/commands/src/main/kotlin/sh/nino/discord/commands/moderation/HistoryCommand.kt new file mode 100644 index 00000000..8776dd6a --- /dev/null +++ b/bot/commands/src/main/kotlin/sh/nino/discord/commands/moderation/HistoryCommand.kt @@ -0,0 +1,23 @@ +/* + * Copyright (c) 2019-2022 Nino + * + * 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. + */ + +package sh.nino.discord.commands.moderation diff --git a/bot/commands/src/main/kotlin/sh/nino/discord/commands/moderation/KickCommand.kt b/bot/commands/src/main/kotlin/sh/nino/discord/commands/moderation/KickCommand.kt new file mode 100644 index 00000000..8776dd6a --- /dev/null +++ b/bot/commands/src/main/kotlin/sh/nino/discord/commands/moderation/KickCommand.kt @@ -0,0 +1,23 @@ +/* + * Copyright (c) 2019-2022 Nino + * + * 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. + */ + +package sh.nino.discord.commands.moderation diff --git a/bot/commands/src/main/kotlin/sh/nino/discord/commands/moderation/MuteCommand.kt b/bot/commands/src/main/kotlin/sh/nino/discord/commands/moderation/MuteCommand.kt new file mode 100644 index 00000000..8776dd6a --- /dev/null +++ b/bot/commands/src/main/kotlin/sh/nino/discord/commands/moderation/MuteCommand.kt @@ -0,0 +1,23 @@ +/* + * Copyright (c) 2019-2022 Nino + * + * 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. + */ + +package sh.nino.discord.commands.moderation diff --git a/bot/commands/src/main/kotlin/sh/nino/discord/commands/moderation/PardonCommand.kt b/bot/commands/src/main/kotlin/sh/nino/discord/commands/moderation/PardonCommand.kt new file mode 100644 index 00000000..8776dd6a --- /dev/null +++ b/bot/commands/src/main/kotlin/sh/nino/discord/commands/moderation/PardonCommand.kt @@ -0,0 +1,23 @@ +/* + * Copyright (c) 2019-2022 Nino + * + * 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. + */ + +package sh.nino.discord.commands.moderation diff --git a/bot/commands/src/main/kotlin/sh/nino/discord/commands/moderation/UnmuteCommand.kt b/bot/commands/src/main/kotlin/sh/nino/discord/commands/moderation/UnmuteCommand.kt new file mode 100644 index 00000000..8776dd6a --- /dev/null +++ b/bot/commands/src/main/kotlin/sh/nino/discord/commands/moderation/UnmuteCommand.kt @@ -0,0 +1,23 @@ +/* + * Copyright (c) 2019-2022 Nino + * + * 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. + */ + +package sh.nino.discord.commands.moderation diff --git a/bot/commands/src/main/kotlin/sh/nino/discord/commands/moderation/WarnCommand.kt b/bot/commands/src/main/kotlin/sh/nino/discord/commands/moderation/WarnCommand.kt new file mode 100644 index 00000000..8776dd6a --- /dev/null +++ b/bot/commands/src/main/kotlin/sh/nino/discord/commands/moderation/WarnCommand.kt @@ -0,0 +1,23 @@ +/* + * Copyright (c) 2019-2022 Nino + * + * 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. + */ + +package sh.nino.discord.commands.moderation diff --git a/bot/commands/src/main/kotlin/sh/nino/discord/commands/moderation/_Module.kt b/bot/commands/src/main/kotlin/sh/nino/discord/commands/moderation/_Module.kt new file mode 100644 index 00000000..f95fc63f --- /dev/null +++ b/bot/commands/src/main/kotlin/sh/nino/discord/commands/moderation/_Module.kt @@ -0,0 +1,27 @@ +/* + * Copyright (c) 2019-2022 Nino + * + * 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. + */ + +package sh.nino.discord.commands.moderation + +import org.koin.dsl.module + +val moderationCommandsModule = module {} diff --git a/bot/commands/src/main/kotlin/sh/nino/discord/commands/system/DumpThreadInfoCommand.kt b/bot/commands/src/main/kotlin/sh/nino/discord/commands/system/DumpThreadInfoCommand.kt new file mode 100644 index 00000000..1b522513 --- /dev/null +++ b/bot/commands/src/main/kotlin/sh/nino/discord/commands/system/DumpThreadInfoCommand.kt @@ -0,0 +1,104 @@ +/* + * Copyright (c) 2019-2022 Nino + * + * 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. + */ + +package sh.nino.discord.commands.system + +import dev.kord.rest.NamedFile +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import kotlinx.datetime.Clock +import org.apache.commons.lang3.time.StopWatch +import sh.nino.discord.commands.AbstractCommand +import sh.nino.discord.commands.CommandCategory +import sh.nino.discord.commands.CommandMessage +import sh.nino.discord.commands.annotations.Command +import sh.nino.discord.common.extensions.humanize +import java.io.ByteArrayInputStream +import java.lang.management.ManagementFactory +import java.util.concurrent.TimeUnit + +@Command( + "threads", + "Shows the thread information within the bot so far", + aliases = ["dump.threads", "dump"], + ownerOnly = true, + category = CommandCategory.SYSTEM +) +class DumpThreadInfoCommand: AbstractCommand() { + override suspend fun execute(msg: CommandMessage) { + val message = msg.reply("Now collecting thread information, this might take a while...") + val watch = StopWatch.createStarted() + + val builder = StringBuilder() + val mxBean = ManagementFactory.getThreadMXBean() + val infos = mxBean.getThreadInfo(mxBean.allThreadIds) + + builder.appendLine("-- Thread dump created by ${msg.author.tag} (${msg.author.id}) at ${Clock.System.now()} --") + builder.appendLine() + + for (info in infos) { + builder.appendLine("[ Thread ${info.threadName} (#${info.threadId}) - ${info.threadState} ]") + if (mxBean.isThreadCpuTimeSupported) { + val actualCpuTime = TimeUnit.MILLISECONDS.convert(mxBean.getThreadCpuTime(info.threadId), TimeUnit.NANOSECONDS) + builder.appendLine("• CPU Time: ${actualCpuTime.humanize(long = true, includeMs = true)}") + } + + builder.appendLine("• User Time: ${TimeUnit.MILLISECONDS.convert(mxBean.getThreadUserTime(info.threadId), TimeUnit.NANOSECONDS).humanize(long = true, includeMs = true)}") + builder.appendLine() + + if (info.stackTrace.isEmpty()) { + builder.appendLine("-- Stacktrace is not available! --") + } else { + val stacktrace = info.stackTrace + for (element in stacktrace) { + builder.append("\n at ") + builder.append(element) + } + } + + builder.append("\n\n") + } + + message.delete() + + val stream = withContext(Dispatchers.IO) { + ByteArrayInputStream(builder.toString().toByteArray(Charsets.UTF_8)) + } + + val file = NamedFile("thread_dump.txt", stream) + watch.stop() + + msg.replyFile( + buildString { + appendLine( + ":thumbsup: I have collected the thread information for you! It only took **${watch.getTime( + TimeUnit.MILLISECONDS + )}**ms to calculate!" + ) + + appendLine(":eyes: You can inspect it in the file I created for you, say thanks after, please? :3") + appendLine(":pencil: There is currently **${mxBean.threadCount}** threads in this current Java Virtual Machine, only ${mxBean.daemonThreadCount} are background threads.") + }, + listOf(file) + ) + } +} diff --git a/bot/commands/src/main/kotlin/sh/nino/discord/commands/system/EvalCommand.kt b/bot/commands/src/main/kotlin/sh/nino/discord/commands/system/EvalCommand.kt new file mode 100644 index 00000000..add5eb6f --- /dev/null +++ b/bot/commands/src/main/kotlin/sh/nino/discord/commands/system/EvalCommand.kt @@ -0,0 +1,204 @@ +/* + * Copyright (c) 2019-2022 Nino + * + * 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. + */ + +package sh.nino.discord.commands.system + +import dev.kord.core.Kord +import dev.kord.x.emoji.Emojis +import dev.kord.x.emoji.toReaction +import io.ktor.client.* +import io.ktor.client.request.* +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import kotlinx.serialization.Serializable +import org.apache.commons.lang3.time.StopWatch +import org.koin.core.context.GlobalContext +import sh.nino.discord.commands.AbstractCommand +import sh.nino.discord.commands.CommandCategory +import sh.nino.discord.commands.CommandMessage +import sh.nino.discord.commands.annotations.Command +import sh.nino.discord.common.data.Config +import sh.nino.discord.common.extensions.elipsis +import java.io.ByteArrayOutputStream +import java.io.PrintStream +import java.nio.charset.StandardCharsets +import java.util.concurrent.TimeUnit +import java.util.regex.Pattern +import javax.script.ScriptEngineManager + +@Serializable +data class HastebinResult( + val key: String +) + +@Command( + "eval", + "Evaluates arbitrary Kotlin code within the current Noel scope", + aliases = ["ev", "kt", "code"], + ownerOnly = true, + category = CommandCategory.SYSTEM +) +class EvalCommand( + private val httpClient: HttpClient, + private val kord: Kord, + private val config: Config +): AbstractCommand() { + private val engine = ScriptEngineManager().getEngineByName("kotlin") + + override suspend fun execute(msg: CommandMessage) { + if (msg.args.isEmpty()) { + msg.reply("ok, what should i do? idk what to do man!") + return + } + + var script = msg.args.joinToString(" ") + val silent = (msg.flags["silent"] ?: msg.flags["s"])?.asBooleanOrNull ?: false + val stopwatch = StopWatch.createStarted() + + if (script.startsWith("```kt") && script.endsWith("```")) { + script = script.replace("```kt", "").replace("```", "") + } + + val koin = GlobalContext.get() + engine.put("this", this) + engine.put("koin", koin) + engine.put("kord", kord) + engine.put("msg", msg) + + val response: Any? = try { + engine.eval( + """ + import kotlinx.coroutines.* + import kotlinx.coroutines.flow.* + import kotlinx.serialization.json.* + import kotlinx.serialization.* + import sh.nino.discord.core.* + import dev.kord.core.* + + $script + """.trimIndent() + ) + } catch (e: Exception) { + e + } + + stopwatch.stop() + if (response is Exception) { + val baos = ByteArrayOutputStream() + val stream = withContext(Dispatchers.IO) { + PrintStream(baos, true, StandardCharsets.UTF_8.name()) + } + + stream.use { response.printStackTrace(stream) } + + val stacktrace = withContext(Dispatchers.IO) { + baos.toString(StandardCharsets.UTF_8.name()) + } + + msg.reply( + buildString { + appendLine(":timer: **${stopwatch.getTime(TimeUnit.MILLISECONDS)}**ms") + appendLine("```kotlin") + appendLine(stacktrace.elipsis(1500)) + appendLine("```") + } + ) + + return + } + + if (response != null && response.toString().length > 2000) { + val resp: HastebinResult = httpClient.post("https://haste.red-panda.red/documents") { + body = redact(config, response.toString()) + } + + msg.reply("<:noelHug:815113851133624342> The result of the evaluation was too long! So, I uploaded to hastebin: ****") + return + } + + if (response == null) { + msg.message.addReaction(Emojis.whiteCheckMark.toReaction()) + return + } + + if (silent) return + + msg.reply( + buildString { + appendLine(":timer: **${stopwatch.getTime(TimeUnit.MILLISECONDS)}**ms") + appendLine("```kotlin") + appendLine(redact(config, response.toString()).elipsis(1500)) + appendLine("```") + } + ) + } + + companion object { + fun redact(config: Config, script: String): String { + val tokens = mutableListOf( + config.token, + config.database.username, + config.database.password, + config.redis.password, + config.redis.host, + config.database.host, + config.sentryDsn, + config.timeouts.uri, + config.publicKey + ) + + if (config.instatus != null) + tokens += listOf(config.instatus!!.token, config.instatus!!.gatewayMetricId) + + if (config.ravy != null) + tokens += config.ravy + + if (config.timeouts.auth != null) + tokens += config.timeouts.auth + + if (config.api != null) + tokens += config.api!!.host + + if (config.botlists != null) { + if (config.botlists!!.discordServicesToken != null) + tokens += config.botlists!!.discordServicesToken + + if (config.botlists!!.discordBoatsToken != null) + tokens += config.botlists!!.discordBoatsToken + + if (config.botlists!!.discordBoatsToken != null) + tokens += config.botlists!!.discordBoatsToken + + if (config.botlists!!.discordBotsToken != null) + tokens += config.botlists!!.discordBotsToken + + if (config.botlists!!.discordsToken != null) + tokens += config.botlists!!.discordsToken + + if (config.botlists!!.dellyToken != null) + tokens += config.botlists!!.dellyToken + } + + return script.replace(Pattern.compile(tokens.joinToString("|"), Pattern.CASE_INSENSITIVE).toRegex(), "") + } + } +} diff --git a/bot/commands/src/main/kotlin/sh/nino/discord/commands/system/GlobalBansCommand.kt b/bot/commands/src/main/kotlin/sh/nino/discord/commands/system/GlobalBansCommand.kt new file mode 100644 index 00000000..c7ea9a4b --- /dev/null +++ b/bot/commands/src/main/kotlin/sh/nino/discord/commands/system/GlobalBansCommand.kt @@ -0,0 +1,23 @@ +/* + * Copyright (c) 2019-2022 Nino + * + * 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. + */ + +package sh.nino.discord.commands.system diff --git a/bot/commands/src/main/kotlin/sh/nino/discord/commands/system/ShellCommand.kt b/bot/commands/src/main/kotlin/sh/nino/discord/commands/system/ShellCommand.kt new file mode 100644 index 00000000..307c9bdb --- /dev/null +++ b/bot/commands/src/main/kotlin/sh/nino/discord/commands/system/ShellCommand.kt @@ -0,0 +1,122 @@ +/* + * Copyright (c) 2019-2022 Nino + * + * 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. + */ + +package sh.nino.discord.commands.system + +import dev.kord.x.emoji.Emojis +import dev.kord.x.emoji.toReaction +import io.ktor.client.* +import io.ktor.client.request.* +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import org.apache.commons.lang3.time.StopWatch +import sh.nino.discord.commands.AbstractCommand +import sh.nino.discord.commands.CommandCategory +import sh.nino.discord.commands.CommandMessage +import sh.nino.discord.commands.annotations.Command +import sh.nino.discord.common.data.Config +import sh.nino.discord.common.extensions.elipsis +import sh.nino.discord.common.extensions.shell +import java.io.ByteArrayOutputStream +import java.io.PrintStream +import java.nio.charset.StandardCharsets +import java.util.concurrent.TimeUnit + +@Command( + "shell", + "Executes shell commands within the current context.", + aliases = ["exec", "$", "$>", "sh"], + ownerOnly = true, + category = CommandCategory.SYSTEM +) +class ShellCommand(private val config: Config, private val httpClient: HttpClient): AbstractCommand() { + override suspend fun execute(msg: CommandMessage) { + if (msg.args.isEmpty()) { + msg.reply("ok, what should i do? idk what to do man!") + return + } + + var script = msg.args.joinToString(" ") + val silent = (msg.flags["silent"] ?: msg.flags["s"])?.asBooleanOrNull ?: false + val stopwatch = StopWatch.createStarted() + + if (script.startsWith("```sh") && script.endsWith("```")) { + script = script.replace("```sh", "").replace("```", "") + } + + val response: Any = try { + script.shell() + } catch (e: Exception) { + e + } + + stopwatch.stop() + if (response is Exception) { + val baos = ByteArrayOutputStream() + val stream = withContext(Dispatchers.IO) { + PrintStream(baos, true, StandardCharsets.UTF_8.name()) + } + + stream.use { response.printStackTrace(stream) } + + val stacktrace = withContext(Dispatchers.IO) { + baos.toString(StandardCharsets.UTF_8.name()) + } + + msg.reply( + buildString { + appendLine(":timer: **${stopwatch.getTime(TimeUnit.MILLISECONDS)}**ms") + appendLine("```sh") + appendLine(stacktrace.elipsis(1500)) + appendLine("```") + } + ) + + return + } + + if (response.toString().length > 2000) { + val resp: HastebinResult = httpClient.post("https://haste.red-panda.red/documents") { + body = EvalCommand.redact(config, response.toString()) + } + + msg.reply("<:noelHug:815113851133624342> The result of the evaluation was too long! So, I uploaded to hastebin: ****") + return + } + + if (response.toString().isEmpty()) { + msg.message.addReaction(Emojis.whiteCheckMark.toReaction()) + return + } + + if (silent) return + + msg.reply( + buildString { + appendLine(":timer: **${stopwatch.getTime(TimeUnit.MILLISECONDS)}**ms") + appendLine("```sh") + appendLine(EvalCommand.redact(config, response.toString()).elipsis(1500)) + appendLine("```") + } + ) + } +} diff --git a/src/structures/decorators/Command.ts b/bot/commands/src/main/kotlin/sh/nino/discord/commands/system/_Module.kt similarity index 71% rename from src/structures/decorators/Command.ts rename to bot/commands/src/main/kotlin/sh/nino/discord/commands/system/_Module.kt index 1fdd352b..ebf07d66 100644 --- a/src/structures/decorators/Command.ts +++ b/bot/commands/src/main/kotlin/sh/nino/discord/commands/system/_Module.kt @@ -1,5 +1,5 @@ -/** - * Copyright (c) 2019-2021 Nino +/* + * Copyright (c) 2019-2022 Nino * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -20,15 +20,14 @@ * SOFTWARE. */ -import { createProxyDecorator } from '../../util/proxy/ProxyDecoratorUtil'; -import type { CommandInfo } from '../Command'; +package sh.nino.discord.commands.system -/** - * Class decorator to apply metadata within a {@link Command}. - */ -export function Command(metadata: CommandInfo): ClassDecorator { - return (target) => - createProxyDecorator(target, { - construct: (ctor: any) => new ctor(metadata), - }); +import org.koin.dsl.bind +import org.koin.dsl.module +import sh.nino.discord.commands.AbstractCommand + +val systemCommandsModule = module { + single { DumpThreadInfoCommand() } bind AbstractCommand::class + single { EvalCommand(get(), get(), get()) } bind AbstractCommand::class + single { ShellCommand(get(), get()) } bind AbstractCommand::class } diff --git a/bot/commands/src/main/kotlin/sh/nino/discord/commands/threads/AddThreadsCommand.kt b/bot/commands/src/main/kotlin/sh/nino/discord/commands/threads/AddThreadsCommand.kt new file mode 100644 index 00000000..7b7632fc --- /dev/null +++ b/bot/commands/src/main/kotlin/sh/nino/discord/commands/threads/AddThreadsCommand.kt @@ -0,0 +1,23 @@ +/* + * Copyright (c) 2019-2022 Nino + * + * 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. + */ + +package sh.nino.discord.commands.threads diff --git a/bot/commands/src/main/kotlin/sh/nino/discord/commands/threads/NoThreadsCommand.kt b/bot/commands/src/main/kotlin/sh/nino/discord/commands/threads/NoThreadsCommand.kt new file mode 100644 index 00000000..7b7632fc --- /dev/null +++ b/bot/commands/src/main/kotlin/sh/nino/discord/commands/threads/NoThreadsCommand.kt @@ -0,0 +1,23 @@ +/* + * Copyright (c) 2019-2022 Nino + * + * 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. + */ + +package sh.nino.discord.commands.threads diff --git a/bot/commands/src/main/kotlin/sh/nino/discord/commands/threads/_Module.kt b/bot/commands/src/main/kotlin/sh/nino/discord/commands/threads/_Module.kt new file mode 100644 index 00000000..aa15f529 --- /dev/null +++ b/bot/commands/src/main/kotlin/sh/nino/discord/commands/threads/_Module.kt @@ -0,0 +1,27 @@ +/* + * Copyright (c) 2019-2022 Nino + * + * 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. + */ + +package sh.nino.discord.commands.threads + +import org.koin.dsl.module + +val threadsCommandsModule = module {} diff --git a/bot/commands/src/main/kotlin/sh/nino/discord/commands/util/InfoCommand.kt b/bot/commands/src/main/kotlin/sh/nino/discord/commands/util/InfoCommand.kt new file mode 100644 index 00000000..baf25e25 --- /dev/null +++ b/bot/commands/src/main/kotlin/sh/nino/discord/commands/util/InfoCommand.kt @@ -0,0 +1,23 @@ +/* + * Copyright (c) 2019-2022 Nino + * + * 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. + */ + +package sh.nino.discord.commands.util diff --git a/bot/commands/src/main/kotlin/sh/nino/discord/commands/util/_Module.kt b/bot/commands/src/main/kotlin/sh/nino/discord/commands/util/_Module.kt new file mode 100644 index 00000000..19392a72 --- /dev/null +++ b/bot/commands/src/main/kotlin/sh/nino/discord/commands/util/_Module.kt @@ -0,0 +1,27 @@ +/* + * Copyright (c) 2019-2022 Nino + * + * 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. + */ + +package sh.nino.discord.commands.util + +import org.koin.dsl.module + +val utilCommandsModule = module {} diff --git a/bot/commands/src/main/kotlin/sh/nino/discord/commands/voice/VoiceDeafenCommand.kt b/bot/commands/src/main/kotlin/sh/nino/discord/commands/voice/VoiceDeafenCommand.kt new file mode 100644 index 00000000..08ff02e0 --- /dev/null +++ b/bot/commands/src/main/kotlin/sh/nino/discord/commands/voice/VoiceDeafenCommand.kt @@ -0,0 +1,23 @@ +/* + * Copyright (c) 2019-2022 Nino + * + * 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. + */ + +package sh.nino.discord.commands.voice diff --git a/bot/commands/src/main/kotlin/sh/nino/discord/commands/voice/VoiceKickBotsCommand.kt b/bot/commands/src/main/kotlin/sh/nino/discord/commands/voice/VoiceKickBotsCommand.kt new file mode 100644 index 00000000..08ff02e0 --- /dev/null +++ b/bot/commands/src/main/kotlin/sh/nino/discord/commands/voice/VoiceKickBotsCommand.kt @@ -0,0 +1,23 @@ +/* + * Copyright (c) 2019-2022 Nino + * + * 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. + */ + +package sh.nino.discord.commands.voice diff --git a/bot/commands/src/main/kotlin/sh/nino/discord/commands/voice/VoiceMuteCommand.kt b/bot/commands/src/main/kotlin/sh/nino/discord/commands/voice/VoiceMuteCommand.kt new file mode 100644 index 00000000..08ff02e0 --- /dev/null +++ b/bot/commands/src/main/kotlin/sh/nino/discord/commands/voice/VoiceMuteCommand.kt @@ -0,0 +1,23 @@ +/* + * Copyright (c) 2019-2022 Nino + * + * 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. + */ + +package sh.nino.discord.commands.voice diff --git a/bot/commands/src/main/kotlin/sh/nino/discord/commands/voice/VoiceUndeafenCommand.kt b/bot/commands/src/main/kotlin/sh/nino/discord/commands/voice/VoiceUndeafenCommand.kt new file mode 100644 index 00000000..08ff02e0 --- /dev/null +++ b/bot/commands/src/main/kotlin/sh/nino/discord/commands/voice/VoiceUndeafenCommand.kt @@ -0,0 +1,23 @@ +/* + * Copyright (c) 2019-2022 Nino + * + * 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. + */ + +package sh.nino.discord.commands.voice diff --git a/bot/commands/src/main/kotlin/sh/nino/discord/commands/voice/VoiceUnmuteCommand.kt b/bot/commands/src/main/kotlin/sh/nino/discord/commands/voice/VoiceUnmuteCommand.kt new file mode 100644 index 00000000..08ff02e0 --- /dev/null +++ b/bot/commands/src/main/kotlin/sh/nino/discord/commands/voice/VoiceUnmuteCommand.kt @@ -0,0 +1,23 @@ +/* + * Copyright (c) 2019-2022 Nino + * + * 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. + */ + +package sh.nino.discord.commands.voice diff --git a/bot/commands/src/main/kotlin/sh/nino/discord/commands/voice/_Module.kt b/bot/commands/src/main/kotlin/sh/nino/discord/commands/voice/_Module.kt new file mode 100644 index 00000000..df9d4b48 --- /dev/null +++ b/bot/commands/src/main/kotlin/sh/nino/discord/commands/voice/_Module.kt @@ -0,0 +1,27 @@ +/* + * Copyright (c) 2019-2022 Nino + * + * 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. + */ + +package sh.nino.discord.commands.voice + +import org.koin.dsl.module + +val voiceCommandsModule = module {} diff --git a/bot/commons/build.gradle.kts b/bot/commons/build.gradle.kts new file mode 100644 index 00000000..16969d66 --- /dev/null +++ b/bot/commons/build.gradle.kts @@ -0,0 +1,83 @@ +/** + * Copyright (c) 2019-2022 Nino + * + * 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. + */ + +plugins { + `nino-module` +} + +dependencies { + // common kotlin libraries for all projects + api(kotlin("reflect")) + + // kotlinx libraries + api(platform("org.jetbrains.kotlinx:kotlinx-coroutines-bom:1.6.0-native-mt")) + api("org.jetbrains.kotlinx:kotlinx-coroutines-core") + api("org.jetbrains.kotlinx:kotlinx-coroutines-jdk8") + api(platform("org.jetbrains.kotlinx:kotlinx-serialization-bom:1.3.1")) + api("org.jetbrains.kotlinx:kotlinx-serialization-protobuf") + api("org.jetbrains.kotlinx:kotlinx-serialization-json") + api("org.jetbrains.kotlinx:kotlinx-datetime:0.3.1") + api("org.jetbrains.kotlinx:kotlinx-serialization-core") + + // Noel Utilities + api("gay.floof.commons", "commons-slf4j", "1.1.0") + + // Apache Utilities + api("org.apache.commons:commons-lang3:3.12.0") + + // Common dependencies that most projects need + // Kord, Koin, DB, etc + api(platform("io.ktor:ktor-bom:1.6.7")) +// implementation("io.ktor:ktor-client-websockets") + api("com.squareup.okhttp3:okhttp:4.9.3") +// implementation("io.ktor:ktor-client-okhttp") +// implementation("io.ktor:ktor-client-core") + api("io.insert-koin:koin-core:3.1.5") + api("dev.kord:kord-core:0.8.0-M9") + api("io.lettuce:lettuce-core:6.1.6.RELEASE") + api(platform("org.jetbrains.exposed:exposed-bom:0.36.1")) + api("org.jetbrains.exposed:exposed-kotlin-datetime") + api("org.jetbrains.exposed:exposed-core") + api("org.jetbrains.exposed:exposed-jdbc") + api("org.jetbrains.exposed:exposed-dao") + api("org.postgresql:postgresql:42.3.1") + api("com.zaxxer:HikariCP:5.0.1") + api("org.slf4j:slf4j-api:1.7.35") + api("io.sentry:sentry:5.6.0") + api("io.sentry:sentry-logback:5.6.0") +// implementation("io.ktor:ktor-serialization-kotlinx-json") +// implementation("io.ktor:ktor-client-content-negotiation") + api("dev.kord.x:emoji:0.5.0") + + // TODO: remove this once Kord supports KTOR 2 + api("io.ktor:ktor-serialization") + api("io.ktor:ktor-client-okhttp") + api("io.ktor:ktor-client-core") + + + // Conditional logic for logback + api("org.codehaus.janino:janino:3.1.6") + + // Prometheus + api("io.prometheus:simpleclient_hotspot:0.14.1") + api("io.prometheus:simpleclient:0.14.0") +} diff --git a/bot/commons/src/main/kotlin/sh/nino/discord/common/DiscordUtils.kt b/bot/commons/src/main/kotlin/sh/nino/discord/common/DiscordUtils.kt new file mode 100644 index 00000000..a0b1396d --- /dev/null +++ b/bot/commons/src/main/kotlin/sh/nino/discord/common/DiscordUtils.kt @@ -0,0 +1,95 @@ +/* + * Copyright (c) 2019-2022 Nino + * + * 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. + */ + +package sh.nino.discord.common + +import dev.kord.core.Kord +import dev.kord.core.entity.User +import dev.kord.core.entity.channel.Channel +import org.koin.core.context.GlobalContext +import sh.nino.discord.common.extensions.asSnowflake +import sh.nino.discord.common.extensions.retrieve + +/** + * Returns multiple users from a list of [arguments][args]. + * ```kt + * val users = getMultipleUsersFromArgs(listOf("<@!280158289667555328>", "Polarboi#2535")) + * // => [kotlin.List{280158289667555328, 743701282790834247}] + * ``` + */ +suspend fun getMutipleUsersFromArgs(args: List): List { + val kord = GlobalContext.retrieve() + val users = mutableListOf() + val usersByMention = args.filter { + it.matches(USER_MENTION_REGEX.toRegex()) + } + + for (mention in usersByMention) { + val matcher = USER_MENTION_REGEX.matcher(mention) + if (!matcher.matches()) continue + + val id = matcher.group(1) + val user = kord.getUser(id.asSnowflake()) + if (user != null) users.add(user) + } + + val usersById = args.filter { + it.matches(ID_REGEX.toRegex()) + } + + for (userId in usersById) { + val user = kord.getUser(userId.asSnowflake()) + if (user != null) users.add(user) + } + + return users + .distinct() // remove duplicates + .toList() // immutable +} + +suspend fun getMultipleChannelsFromArgs(args: List): List { + val kord = GlobalContext.retrieve() + val channels = mutableListOf() + val channelsByMention = args.filter { + it.matches(CHANNEL_REGEX.toRegex()) + } + + for (mention in channelsByMention) { + val matcher = CHANNEL_REGEX.matcher(mention) + if (!matcher.matches()) continue + + val id = matcher.group(1) + val channel = kord.getChannel(id.asSnowflake()) + if (channel != null) channels.add(channel) + } + + val channelsById = args.filter { + it.matches(ID_REGEX.toRegex()) + } + + for (channelId in channelsById) { + val channel = kord.getChannel(channelId.asSnowflake()) + if (channel != null) channels.add(channel) + } + + return channels.distinct().toList() +} diff --git a/bot/commons/src/main/kotlin/sh/nino/discord/common/FlagValue.kt b/bot/commons/src/main/kotlin/sh/nino/discord/common/FlagValue.kt new file mode 100644 index 00000000..df8d824a --- /dev/null +++ b/bot/commons/src/main/kotlin/sh/nino/discord/common/FlagValue.kt @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2019-2022 Nino + * + * 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. + */ + +package sh.nino.discord.common + +/** + * Represents a value of a command flag (`-c`, `--c=owo`, `-c owo`) + */ +class FlagValue(private val value: Any) { + init { + check(value is String || value is Boolean) { + "Value was not a String or Boolean, received ${value::class}" + } + } + + val asString: String + get() = asStringOrNull ?: error("Unable to cast value to String from ${value::class}") + + val asBoolean: Boolean + get() = asBooleanOrNull ?: error("Unable to cast value to Boolean from ${value::class}") + + val asStringOrNull: String? + get() = value as? String + + val asBooleanOrNull: Boolean? + get() = value as? Boolean +} diff --git a/bot/commons/src/main/kotlin/sh/nino/discord/common/NinoInfo.kt b/bot/commons/src/main/kotlin/sh/nino/discord/common/NinoInfo.kt new file mode 100644 index 00000000..b03df3dd --- /dev/null +++ b/bot/commons/src/main/kotlin/sh/nino/discord/common/NinoInfo.kt @@ -0,0 +1,45 @@ +/* + * Copyright (c) 2019-2022 Nino + * + * 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. + */ + +package sh.nino.discord.common + +import kotlinx.serialization.ExperimentalSerializationApi +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.decodeFromStream +import kotlinx.serialization.json.jsonPrimitive + +@OptIn(ExperimentalSerializationApi::class) +object NinoInfo { + val VERSION: String + val COMMIT_SHA: String + val BUILD_DATE: String + + init { + val reader = this::class.java.getResourceAsStream("/build-info.json")!! + val data = Json.decodeFromStream(JsonObject.serializer(), reader) + + VERSION = data["version"]!!.jsonPrimitive.content + COMMIT_SHA = data["commit_sha"]!!.jsonPrimitive.content + BUILD_DATE = data["build_date"]!!.jsonPrimitive.content + } +} diff --git a/bot/commons/src/main/kotlin/sh/nino/discord/common/PermissionUtil.kt b/bot/commons/src/main/kotlin/sh/nino/discord/common/PermissionUtil.kt new file mode 100644 index 00000000..ae60f18e --- /dev/null +++ b/bot/commons/src/main/kotlin/sh/nino/discord/common/PermissionUtil.kt @@ -0,0 +1,60 @@ +/* + * Copyright (c) 2019-2022 Nino + * + * 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. + */ + +package sh.nino.discord.common + +import dev.kord.core.entity.Member +import dev.kord.core.entity.Role +import kotlinx.coroutines.flow.firstOrNull +import sh.nino.discord.common.extensions.sortWith + +/** + * Returns the highest role this [member] has. Returns null if nothing was found. + * @param member The member to check. + */ +suspend fun getTopRole(member: Member): Role? = member + .roles + .sortWith { a, b -> b.rawPosition - a.rawPosition } + .firstOrNull() + +/** + * Checks if [role a][a] is above [role b][b] in hierarchy (or vice-versa) + * @param a The role that should be higher + * @param b The role that should be lower + */ +fun isRoleAbove(a: Role?, b: Role?): Boolean { + if (a == null || b == null) return false + + return a.rawPosition > b.rawPosition +} + +/** + * Checks if [member a][a] is above [member b][b] in hierarchy (or vice-versa) + */ +suspend fun isMemberAbove(a: Member, b: Member): Boolean = isRoleAbove(getTopRole(a), getTopRole(b)) + +/** + * Returns if the user's bitfield permission reaches the threshold of the [required] bitfield. + * @param user The user bitfield to use. + * @param required The required permission bitfield. + */ +fun hasOverlap(user: Int, required: Int): Boolean = (user and 8) != 0 || (user and required) == required diff --git a/src/migrations/1622346188448-addAttachmentColumn.ts b/bot/commons/src/main/kotlin/sh/nino/discord/common/RandomId.kt similarity index 66% rename from src/migrations/1622346188448-addAttachmentColumn.ts rename to bot/commons/src/main/kotlin/sh/nino/discord/common/RandomId.kt index 3c29a727..1c8e5e34 100644 --- a/src/migrations/1622346188448-addAttachmentColumn.ts +++ b/bot/commons/src/main/kotlin/sh/nino/discord/common/RandomId.kt @@ -1,5 +1,5 @@ -/** - * Copyright (c) 2019-2021 Nino +/* + * Copyright (c) 2019-2022 Nino * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -20,16 +20,22 @@ * SOFTWARE. */ -import { MigrationInterface, QueryRunner } from 'typeorm'; +package sh.nino.discord.common -export class addAttachmentColumn1622346188448 implements MigrationInterface { - name = 'addAttachmentColumn1622346188448'; +import java.security.SecureRandom - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query('ALTER TABLE "cases" ADD "attachments" text array NOT NULL DEFAULT \'{}\''); - } +object RandomId { + private const val ALPHA_CHARS = "abcdefghijklmnopqrstuvxyz0123456789" + private val random by lazy { SecureRandom() } - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query('ALTER TABLE "cases" DROP COLUMN "attachments"'); - } + fun generate(len: Int = 8): String { + val builder = StringBuilder() + for (i in 0 until len) { + val index = random.nextInt(ALPHA_CHARS.length) + val char = ALPHA_CHARS[index] + builder.append(char) + } + + return builder.toString() + } } diff --git a/src/automod/Dehoisting.ts b/bot/commons/src/main/kotlin/sh/nino/discord/common/StringOrArray.kt similarity index 50% rename from src/automod/Dehoisting.ts rename to bot/commons/src/main/kotlin/sh/nino/discord/common/StringOrArray.kt index 47f14368..aeb02cac 100644 --- a/src/automod/Dehoisting.ts +++ b/bot/commons/src/main/kotlin/sh/nino/discord/common/StringOrArray.kt @@ -1,5 +1,5 @@ -/** - * Copyright (c) 2019-2021 Nino +/* + * Copyright (c) 2019-2022 Nino * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -20,39 +20,34 @@ * SOFTWARE. */ -import type { Member } from 'eris'; -import { Automod } from '../structures'; -import { Inject } from '@augu/lilith'; -import Database from '../components/Database'; -import Discord from '../components/Discord'; - -export default class Dehoisting implements Automod { - public name: string = 'dehoisting'; - - @Inject - private database!: Database; - - @Inject - private discord!: Discord; - - async onMemberNickUpdate(member: Member) { - const settings = await this.database.automod.get(member.guild.id); - if (settings !== undefined && settings.dehoist === false) return false; +package sh.nino.discord.common - if (member.nick === null) return false; +import kotlinx.serialization.Serializable +import sh.nino.discord.common.extensions.every +import sh.nino.discord.common.serializers.StringOrArraySerializer - if (!member.guild.members.get(this.discord.client.user.id)?.permissions.has('manageNicknames')) return false; +/** + * This class exists, so we can perform operations if we have a [List] or a [String]. + */ +@Serializable(with = StringOrArraySerializer::class) +class StringOrArray(private val value: Any) { + init { + check(value is List<*> || value is String) { "`value` is not a supplied List, Array, or a String." } + + if (value is List<*>) { + check(value.every { it is String }) { "Not every value was a List of strings." } + } + } - if (member.nick[0] < '0') { - const origin = member.nick; - await member.edit( - { nick: 'hoister' }, - `[Automod] Member ${member.username}#${member.discriminator} has updated their nick to "${origin}" and dehoisting is enabled.` - ); + @Suppress("UNCHECKED_CAST") + val asList: List + get() = value as? List ?: error("Value was not a instance of `List`.") - return true; - } + @Suppress("UNCHECKED_CAST") + val asString: String + get() = value as? String ?: error("Value was not a instance of `String`") - return false; - } + @Suppress("UNCHECKED_CAST") + val asListOrNull: List? + get() = value as? List } diff --git a/bot/commons/src/main/kotlin/sh/nino/discord/common/constants.kt b/bot/commons/src/main/kotlin/sh/nino/discord/common/constants.kt new file mode 100644 index 00000000..7860c3dc --- /dev/null +++ b/bot/commons/src/main/kotlin/sh/nino/discord/common/constants.kt @@ -0,0 +1,53 @@ +/* + * Copyright (c) 2019-2022 Nino + * + * 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. + */ + +package sh.nino.discord.common + +import sh.nino.discord.common.extensions.asKordColor +import java.awt.Color +import java.util.regex.Pattern + +val COLOR = Color.decode("#f092af").asKordColor() +val USERNAME_DISCRIM_REGEX = Pattern.compile("^(\\w.+)#(\\d{4})\$")!! +val DISCORD_INVITE_REGEX = Pattern.compile("(http(s)?://(www.)?)?(discord.gg|discord.io|discord.me|discord.link|invite.gg)/\\w+")!! +val USER_MENTION_REGEX = Pattern.compile("^<@!?([0-9]+)>$")!! +val CHANNEL_REGEX = Pattern.compile("<#([0-9]+)>$")!! +val QUOTES_REGEX = Pattern.compile("['\"]")!! +val ID_REGEX = Pattern.compile("^\\d+\$")!! +val FLAG_REGEX = Pattern.compile("(?:--?|—)([\\w]+)(=?(\\w+|['\"].*['\"]))?", Pattern.CASE_INSENSITIVE) + +val DEDI_NODE: String by lazy { + // Check if it's in the system properties, i.e, injected with `-D` + // This is the case with the Docker image + val node1 = System.getProperty("winterfox.dedi", "?")!! + if (node1 != "?" && node1 != "none") { + return@lazy node1 + } + + // Check if it is in environment variables + val node2 = System.getenv("WINTERFOX_DEDI_NODE") ?: "none" + if (node2 != "none") { + return@lazy node2 + } + + "none" +} diff --git a/bot/commons/src/main/kotlin/sh/nino/discord/common/data/ApiConfig.kt b/bot/commons/src/main/kotlin/sh/nino/discord/common/data/ApiConfig.kt new file mode 100644 index 00000000..071366d0 --- /dev/null +++ b/bot/commons/src/main/kotlin/sh/nino/discord/common/data/ApiConfig.kt @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2019-2022 Nino + * + * 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. + */ + +package sh.nino.discord.common.data + +import kotlinx.serialization.Serializable + +@Serializable +data class ApiConfig( + val host: String = "0.0.0.0", + val port: Int = 8989 +) diff --git a/bot/commons/src/main/kotlin/sh/nino/discord/common/data/BotlistsConfig.kt b/bot/commons/src/main/kotlin/sh/nino/discord/common/data/BotlistsConfig.kt new file mode 100644 index 00000000..35654ea9 --- /dev/null +++ b/bot/commons/src/main/kotlin/sh/nino/discord/common/data/BotlistsConfig.kt @@ -0,0 +1,47 @@ +/* + * Copyright (c) 2019-2022 Nino + * + * 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. + */ + +package sh.nino.discord.common.data + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class BotlistsConfig( + @SerialName("dservices") + val discordServicesToken: String? = null, + + @SerialName("dboats") + val discordBoatsToken: String? = null, + + @SerialName("dbots") + val discordBotsToken: String? = null, + + @SerialName("topgg") + val topGGToken: String? = null, + + @SerialName("delly") + val dellyToken: String? = null, + + @SerialName("discords") + val discordsToken: String? = null +) diff --git a/src/entities/CaseEntity.ts b/bot/commons/src/main/kotlin/sh/nino/discord/common/data/Config.kt similarity index 50% rename from src/entities/CaseEntity.ts rename to bot/commons/src/main/kotlin/sh/nino/discord/common/data/Config.kt index fd877e17..d68185d2 100644 --- a/src/entities/CaseEntity.ts +++ b/bot/commons/src/main/kotlin/sh/nino/discord/common/data/Config.kt @@ -1,5 +1,5 @@ -/** - * Copyright (c) 2019-2021 Nino +/* + * Copyright (c) 2019-2022 Nino * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -20,41 +20,40 @@ * SOFTWARE. */ -import { Entity, Column, PrimaryColumn, BaseEntity } from 'typeorm'; -import { PunishmentType } from './PunishmentsEntity'; +package sh.nino.discord.common.data -@Entity({ name: 'cases' }) -export default class CaseEntity extends BaseEntity { - @Column({ default: '{}', array: true, type: 'text' }) - public attachments!: string[]; +import dev.kord.common.entity.ActivityType +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable - @Column({ name: 'moderator_id' }) - public moderatorID!: string; +@Serializable +enum class Environment { + @SerialName("development") + Development, - @Column({ name: 'message_id', nullable: true, default: undefined }) - public messageID?: string; - - @Column({ name: 'victim_id' }) - public victimID!: string; - - @Column({ name: 'guild_id', nullable: true }) - public guildID!: string; - - @Column({ nullable: true, default: undefined }) - public reason?: string; - - @PrimaryColumn() - public index!: number; - - @Column({ - type: 'enum', - enum: PunishmentType, - }) - public type!: PunishmentType; - - @Column({ default: false }) - public soft!: boolean; - - @Column({ nullable: true, default: undefined, type: 'bigint' }) - public time?: string; + @SerialName("production") + Production } + +@Serializable +data class Config( + val defaultLocale: String = "en_US", + val environment: Environment = Environment.Development, + val sentryDsn: String? = null, + val publicKey: String, + val prefixes: List = listOf("x!"), + val botlists: BotlistsConfig? = null, + val database: PostgresConfig = PostgresConfig(), + val instatus: InstatusConfig? = null, + val timeouts: TimeoutsConfig, + val metrics: Boolean = false, + val owners: List = listOf(), + val status: StatusConfig = StatusConfig( + type = ActivityType.Game, + status = "with {guilds} guilds [#{shard_id}] https://nino.sh" + ), + val redis: RedisConfig, + val token: String, + val ravy: String? = null, + val api: ApiConfig? = null +) diff --git a/bot/commons/src/main/kotlin/sh/nino/discord/common/data/InstatusConfig.kt b/bot/commons/src/main/kotlin/sh/nino/discord/common/data/InstatusConfig.kt new file mode 100644 index 00000000..f4b665b5 --- /dev/null +++ b/bot/commons/src/main/kotlin/sh/nino/discord/common/data/InstatusConfig.kt @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2019-2022 Nino + * + * 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. + */ + +package sh.nino.discord.common.data + +import kotlinx.serialization.Serializable + +@Serializable +data class InstatusConfig( + val gatewayMetricId: String? = null, + val token: String +) diff --git a/bot/commons/src/main/kotlin/sh/nino/discord/common/data/PostgresConfig.kt b/bot/commons/src/main/kotlin/sh/nino/discord/common/data/PostgresConfig.kt new file mode 100644 index 00000000..a4de5062 --- /dev/null +++ b/bot/commons/src/main/kotlin/sh/nino/discord/common/data/PostgresConfig.kt @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2019-2022 Nino + * + * 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. + */ + +package sh.nino.discord.common.data + +import kotlinx.serialization.Serializable + +@Serializable +data class PostgresConfig( + val username: String = "postgres", + val password: String = "postgres", + val schema: String = "public", + val host: String = "localhost", + val port: Int = 5432, + val name: String = "nino" +) diff --git a/bot/commons/src/main/kotlin/sh/nino/discord/common/data/RedisConfig.kt b/bot/commons/src/main/kotlin/sh/nino/discord/common/data/RedisConfig.kt new file mode 100644 index 00000000..2d540e72 --- /dev/null +++ b/bot/commons/src/main/kotlin/sh/nino/discord/common/data/RedisConfig.kt @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2019-2022 Nino + * + * 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. + */ + +package sh.nino.discord.common.data + +import kotlinx.serialization.Serializable + +@Serializable +data class RedisConfig( + val sentinels: List = listOf(), + val master: String? = null, + val password: String? = null, + val index: Int = 5, + val host: String = "localhost", + val port: Int = 6379, + val ssl: Boolean = false +) diff --git a/bot/commons/src/main/kotlin/sh/nino/discord/common/data/StatusConfig.kt b/bot/commons/src/main/kotlin/sh/nino/discord/common/data/StatusConfig.kt new file mode 100644 index 00000000..13834ff9 --- /dev/null +++ b/bot/commons/src/main/kotlin/sh/nino/discord/common/data/StatusConfig.kt @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2019-2022 Nino + * + * 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. + */ + +package sh.nino.discord.common.data + +import dev.kord.common.entity.ActivityType +import dev.kord.common.entity.PresenceStatus +import kotlinx.serialization.Serializable + +@Serializable +data class StatusConfig( + val presence: PresenceStatus = PresenceStatus.Online, + val status: String, + val type: ActivityType +) diff --git a/bot/commons/src/main/kotlin/sh/nino/discord/common/data/TimeoutsConfig.kt b/bot/commons/src/main/kotlin/sh/nino/discord/common/data/TimeoutsConfig.kt new file mode 100644 index 00000000..c93f110d --- /dev/null +++ b/bot/commons/src/main/kotlin/sh/nino/discord/common/data/TimeoutsConfig.kt @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2019-2022 Nino + * + * 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. + */ + +package sh.nino.discord.common.data + +import kotlinx.serialization.Serializable + +@Serializable +data class TimeoutsConfig( + val auth: String? = null, + val uri: String +) diff --git a/src/structures/Automod.ts b/bot/commons/src/main/kotlin/sh/nino/discord/common/extensions/FlowExtensions.kt similarity index 58% rename from src/structures/Automod.ts rename to bot/commons/src/main/kotlin/sh/nino/discord/common/extensions/FlowExtensions.kt index 2445a937..e91169cb 100644 --- a/src/structures/Automod.ts +++ b/bot/commons/src/main/kotlin/sh/nino/discord/common/extensions/FlowExtensions.kt @@ -1,5 +1,5 @@ -/** - * Copyright (c) 2019-2021 Nino +/* + * Copyright (c) 2019-2022 Nino * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -20,37 +20,30 @@ * SOFTWARE. */ -import type { Member, Message, TextChannel, User } from 'eris'; +package sh.nino.discord.common.extensions + +import kotlinx.coroutines.flow.* /** - * Interface to implement as a [Automod] action. + * Sorts the [flow] from the [comparator] callback. This will emit entities to + * returned as a flow. */ -export interface Automod { - /** - * Handles any member's nickname updates - * @param member The member - */ - onMemberNickUpdate?(member: Member): Promise; - - /** - * Handles any user updates - */ - onUserUpdate?(user: User): Promise; +fun Flow.sortWith(comparator: (T, T) -> Int): Flow = flow { + for (entity in toList().sortedWith(comparator)) emit(entity) +} - /** - * Handles any members joining the guild - * @param member The member - */ - onMemberJoin?(member: Member): Promise; +/** + * Returns if the original Flow contains an entity + */ +suspend fun Flow.contains(value: T): Boolean = filter { it == value }.firstOrNull() != null - /** - * Handles any message updates or creation - * @param message The message - */ - onMessage?(message: Message): Promise; +suspend fun Flow.reduceWith(initialValue: U, operation: suspend (U, T) -> U): U { + var value: Any? = initialValue + collect { + @Suppress("UNCHECKED_CAST") + value = operation(value as U, it) + } - /** - * The name for this [Automod] class. - */ - name: string; + @Suppress("UNCHECKED_CAST") + return value as U } diff --git a/bot/commons/src/main/kotlin/sh/nino/discord/common/extensions/KoinExtensions.kt b/bot/commons/src/main/kotlin/sh/nino/discord/common/extensions/KoinExtensions.kt new file mode 100644 index 00000000..49a9f7d0 --- /dev/null +++ b/bot/commons/src/main/kotlin/sh/nino/discord/common/extensions/KoinExtensions.kt @@ -0,0 +1,57 @@ +/* + * Copyright (c) 2019-2022 Nino + * + * 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. + */ + +package sh.nino.discord.common.extensions + +import org.koin.core.context.GlobalContext +import kotlin.properties.ReadOnlyProperty + +/** + * Injects a singleton into a property. + * ```kt + * class Owo { + * val kord: Kord by inject() + * } + * ``` + */ +inline fun inject(): ReadOnlyProperty = + ReadOnlyProperty { _, _ -> + val koin = GlobalContext.get() + koin.get() + } + +/** + * Retrieve a singleton from the Koin application without chaining `.get()` methods twice. + * ```kt + * val kord: Kord = GlobalContext.retrieve() + * ``` + */ +inline fun GlobalContext.retrieve(): T = get().get() + +/** + * Returns a list of singletons that match with type [T]. + * ```kt + * val commands: List = GlobalContext.retrieveAll() + * // => List [ ... ] + * ``` + */ +inline fun GlobalContext.retrieveAll(): List = get().getAll() diff --git a/bot/commons/src/main/kotlin/sh/nino/discord/common/extensions/KordExtensions.kt b/bot/commons/src/main/kotlin/sh/nino/discord/common/extensions/KordExtensions.kt new file mode 100644 index 00000000..9f85754f --- /dev/null +++ b/bot/commons/src/main/kotlin/sh/nino/discord/common/extensions/KordExtensions.kt @@ -0,0 +1,157 @@ +/* + * Copyright (c) 2019-2022 Nino + * + * 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. + */ + +package sh.nino.discord.common.extensions + +import dev.kord.common.Color +import dev.kord.common.annotation.KordPreview +import dev.kord.common.entity.Permission +import dev.kord.common.entity.Snowflake +import dev.kord.core.event.Event +import dev.kord.core.event.channel.* +import dev.kord.core.event.channel.thread.* +import dev.kord.core.event.gateway.ReadyEvent +import dev.kord.core.event.gateway.ResumedEvent +import dev.kord.core.event.guild.* +import dev.kord.core.event.interaction.ApplicationCommandCreateEvent +import dev.kord.core.event.interaction.ApplicationCommandDeleteEvent +import dev.kord.core.event.interaction.ApplicationCommandUpdateEvent +import dev.kord.core.event.interaction.InteractionCreateEvent +import dev.kord.core.event.message.* +import dev.kord.core.event.role.RoleCreateEvent +import dev.kord.core.event.role.RoleDeleteEvent +import dev.kord.core.event.role.RoleUpdateEvent +import dev.kord.core.event.user.PresenceUpdateEvent +import dev.kord.core.event.user.UserUpdateEvent +import dev.kord.core.event.user.VoiceStateUpdateEvent +import kotlinx.datetime.Instant +import kotlin.math.floor +import java.awt.Color as AwtColor + +fun AwtColor.asKordColor(): Color = Color(this.red, this.green, this.blue) +fun String.asSnowflake(): Snowflake = Snowflake(this) +fun Long.asSnowflake(): Snowflake = Snowflake(this) + +/** + * Returns a [Instant] on when this [Snowflake] was created at. + */ +val Snowflake.createdAt: Instant + get() = Instant.fromEpochMilliseconds(floor((this.value.toLong() / 4194304).toDouble()).toLong() + 1420070400000L) + +@OptIn(KordPreview::class) +val Event.name: String + get() = when (this) { + is ResumedEvent -> "RESUMED" + is ReadyEvent -> "READY" + is ChannelCreateEvent -> "CHANNEL_CREATE" + is ChannelUpdateEvent -> "CHANNEL_UPDATE" + is ChannelDeleteEvent -> "CHANNEL_DELETE" + is ChannelPinsUpdateEvent -> "CHANNEL_PINS_UPDATE" + is TypingStartEvent -> "TYPING_START" + is GuildCreateEvent -> "GUILD_CREATE" + is GuildUpdateEvent -> "GUILD_UPDATE" + is GuildDeleteEvent -> "GUILD_DELETE" + is BanAddEvent -> "GUILD_BAN_ADD" + is BanRemoveEvent -> "GUILD_BAN_REMOVE" + is EmojisUpdateEvent -> "GUILD_EMOJIS_UPDATE" + is IntegrationsUpdateEvent -> "GUILD_INTEGRATIONS_UPDATE" + is MemberJoinEvent -> "GUILD_MEMBER_ADD" + is MemberLeaveEvent -> "GUILD_MEMBER_REMOVE" + is MemberUpdateEvent -> "GUILD_MEMBER_UPDATE" + is RoleCreateEvent -> "GUILD_ROLE_CREATE" + is RoleDeleteEvent -> "GUILD_ROLE_DELETE" + is RoleUpdateEvent -> "GUILD_ROLE_UPDATE" + is MembersChunkEvent -> "GUILD_MEMBERS_CHUNK" + is InviteCreateEvent -> "INVITE_CREATE" + is InviteDeleteEvent -> "INVITE_DELETE" + is MessageCreateEvent -> "MESSAGE_CREATE" + is MessageUpdateEvent -> "MESSAGE_UPDATE" + is MessageDeleteEvent -> "MESSAGE_DELETE" + is MessageBulkDeleteEvent -> "MESSAGE_DELETE_BULK" + is ReactionAddEvent -> "MESSAGE_REACTION_ADD" + is ReactionRemoveEvent -> "MESSAGE_REACTION_REMOVE" + is ReactionRemoveEmojiEvent -> "MESSAGE_REACTION_REMOVE_EMOJI" + is PresenceUpdateEvent -> "PRESENCE_UPDATE" + is UserUpdateEvent -> "USER_UPDATE" + is VoiceStateUpdateEvent -> "VOICE_STATE_UPDATE" + is VoiceServerUpdateEvent -> "VOICE_SERVER_UPDATE" + is WebhookUpdateEvent -> "WEBHOOKS_UPDATE" + is InteractionCreateEvent -> "INTERACTION_CREATE" + is ApplicationCommandCreateEvent -> "APPLICATION_COMMAND_CREATE" + is ApplicationCommandDeleteEvent -> "APPLICATION_COMMAND_DELETE" + is ApplicationCommandUpdateEvent -> "APPLICATION_COMMAND_UPDATE" + is ThreadChannelCreateEvent -> "THREAD_CREATE" + is ThreadChannelDeleteEvent -> "THREAD_DELETE" + is ThreadUpdateEvent -> "THREAD_UPDATE" + is ThreadListSyncEvent -> "THREAD_LIST_SYNC" + is ThreadMemberUpdateEvent -> "THREAD_MEMBER_UPDATE" + is ThreadMembersUpdateEvent -> "THREAD_MEMBERS_UPDATE" + is GuildScheduledEventCreateEvent -> "GUILD_SCHEDULED_EVENT_CREATE" + is GuildScheduledEventDeleteEvent -> "GUILD_SCHEDULED_EVENT_DELETE" + is GuildScheduledEventUpdateEvent -> "GUILD_SCHEDULED_EVENT_UPDATE" + is GuildScheduledEventUserAddEvent -> "GUILD_SCHEDULED_EVENT_USER_ADD" + is GuildScheduledEventUserRemoveEvent -> "GUILD_SCHEDULED_EVENT_USER_REMOVE" + else -> "UNKNOWN (${this::class})" + } + +fun Permission.asString(): String = when (this) { + is Permission.CreateInstantInvite -> "Create Instant Invite" + is Permission.KickMembers -> "Kick Members" + is Permission.BanMembers -> "Ban Members" + is Permission.Administrator -> "Administrator" + is Permission.ManageChannels -> "Manage Channels" + is Permission.AddReactions -> "Add Reactions" + is Permission.ViewAuditLog -> "View Audit Log" + is Permission.Stream -> "Stream in Voice Channels" + is Permission.ViewChannel -> "Read Messages in Guild Channels" + is Permission.SendMessages -> "Send Messages in Guild Channels" + is Permission.SendTTSMessages -> "Send Text-to-Speech Messages in Guild Channels" + is Permission.EmbedLinks -> "Embed Links" + is Permission.AttachFiles -> "Attach Files to Messages" + is Permission.ReadMessageHistory -> "Read Message History in Guild Channels" + is Permission.MentionEveryone -> "Mention Everyone" + is Permission.UseExternalEmojis -> "Use External Emojis in Messages" + is Permission.ViewGuildInsights -> "View Guild Insights" + is Permission.Connect -> "Connect in Voice Channels" + is Permission.Speak -> "Speak in Voice Channels" + is Permission.MuteMembers -> "Mute Members in Voice Channels" + is Permission.DeafenMembers -> "Deafen Members in Voice Channels" + is Permission.MoveMembers -> "Move Members in Voice Channels" + is Permission.UseVAD -> "Use VAD" + is Permission.PrioritySpeaker -> "Priority Speaker" + is Permission.ChangeNickname -> "Change Nickname" + is Permission.ManageNicknames -> "Manage Member Nicknames" + is Permission.ManageRoles -> "Manage Guild Roles" + is Permission.ManageWebhooks -> "Manage Guild Webhooks" + is Permission.ManageEmojis -> "Manage Guild Emojis" + is Permission.ManageThreads -> "Manage Channel Threads" + is Permission.CreatePrivateThreads -> "Create Private Threads" + is Permission.CreatePublicThreads -> "Create Public Threads" + is Permission.SendMessagesInThreads -> "Send Messages in Threads" + is Permission.ManageGuild -> "Manage Guild" + is Permission.ManageMessages -> "Manage Messages" + is Permission.UseSlashCommands -> "Use /commands in Guild Channels" + is Permission.RequestToSpeak -> "Request To Speak" + is Permission.ManageEvents -> "Manage Guild Events" + is Permission.ModerateMembers -> "Moderate Guild Members" + is Permission.All -> "All" +} diff --git a/src/util/proxy/ProxyDecoratorUtil.ts b/bot/commons/src/main/kotlin/sh/nino/discord/common/extensions/KotlinExtensions.kt similarity index 66% rename from src/util/proxy/ProxyDecoratorUtil.ts rename to bot/commons/src/main/kotlin/sh/nino/discord/common/extensions/KotlinExtensions.kt index 20804590..3fb95043 100644 --- a/src/util/proxy/ProxyDecoratorUtil.ts +++ b/bot/commons/src/main/kotlin/sh/nino/discord/common/extensions/KotlinExtensions.kt @@ -1,5 +1,5 @@ -/** - * Copyright (c) 2019-2021 Nino +/* + * Copyright (c) 2019-2022 Nino * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -20,17 +20,20 @@ * SOFTWARE. */ +package sh.nino.discord.common.extensions + +import kotlin.contracts.ExperimentalContracts +import kotlin.contracts.InvocationKind +import kotlin.contracts.contract + /** - * Creates a proxy decorator without modifying prototypes - * with in a `AbstractCommand`. + * Runs a function [block] that is suspended to return a value. + * @param block The function to call in a suspended context. + * @return The value of [R]. */ -// Credit: https://github.com/skyra-project/decorators/blob/main/src/utils.ts#L92 -export function createProxyDecorator(target: T, handler: Omit, 'get'>) { - return new Proxy(target, { - ...handler, - get(target, property) { - const value = Reflect.get(target, property); - return typeof value === 'function' ? (...args: unknown[]) => value.call(target, ...args) : value; - }, - }); +@OptIn(ExperimentalContracts::class) +suspend inline fun T.runSuspended(noinline block: suspend T.() -> R): R { + contract { callsInPlace(block, InvocationKind.EXACTLY_ONCE) } + + return block() } diff --git a/bot/commons/src/main/kotlin/sh/nino/discord/common/extensions/ListExtensions.kt b/bot/commons/src/main/kotlin/sh/nino/discord/common/extensions/ListExtensions.kt new file mode 100644 index 00000000..7efbddfc --- /dev/null +++ b/bot/commons/src/main/kotlin/sh/nino/discord/common/extensions/ListExtensions.kt @@ -0,0 +1,83 @@ +/* + * Copyright (c) 2019-2022 Nino + * + * 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. + */ + +package sh.nino.discord.common.extensions + +/** + * This extension creates a new [Map] of a [List] with [Pair]s. + * ```kt + * val owo = listOf(Pair("first", "second"), Pair("third", "fourth")) + * owo.asMap() + * // { "first": "second", "third": "fourth" } + * ``` + */ +fun List>.asMap(): Map { + val map = mutableMapOf() + for (item in this) { + map[item.first] = item.second + } + + return map.toMap() +} + +/** + * This extension returns a [Pair] from a list which the first item + * is from [List.first] while the second item is a [List] of the underlying + * data left over. + */ +fun List.pairUp(): Pair> = Pair(first(), drop(1)) + +/** + * Returns a [Boolean] if every element appears to be true from the [predicate] function. + */ +fun List.every(predicate: (T) -> Boolean): Boolean { + for (item in this) { + if (!predicate(item)) return false + } + + return true +} + +/** + * Returns the index of an item from a [predicate] function. + * @param predicate The lambda function to find the item you need. + * @return If the item was found, it'll return the index in the [List], + * or -1 if nothing was found. + */ +fun List.findIndex(predicate: (T) -> Boolean): Int { + for ((index, item) in this.withIndex()) { + if (predicate(item)) + return index + } + + return -1 +} + +/** + * Returns the index of an item from a [predicate] function. + * @param predicate The lambda function to find the item you need. + * @return If the item was found, it'll return the index in the [List], + * or -1 if nothing was found. + */ +fun Array.findIndex(predicate: (T) -> Boolean): Int = this.toList().findIndex(predicate) + +fun List.removeFirst(): List = drop(1) diff --git a/bot/commons/src/main/kotlin/sh/nino/discord/common/extensions/StringExtensions.kt b/bot/commons/src/main/kotlin/sh/nino/discord/common/extensions/StringExtensions.kt new file mode 100644 index 00000000..41220782 --- /dev/null +++ b/bot/commons/src/main/kotlin/sh/nino/discord/common/extensions/StringExtensions.kt @@ -0,0 +1,65 @@ +/* + * Copyright (c) 2019-2022 Nino + * + * 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. + */ + +package sh.nino.discord.common.extensions + +import java.io.File +import java.util.* +import java.util.concurrent.TimeUnit + +fun String.shell(): String { + val parts = this.split("\\s".toRegex()) + val process = ProcessBuilder(*parts.toTypedArray()) + .directory(File(".")) + .redirectOutput(ProcessBuilder.Redirect.PIPE) + .redirectError(ProcessBuilder.Redirect.PIPE) + .start() + + process.waitFor(60, TimeUnit.SECONDS) + return process.inputStream.bufferedReader().readText() +} + +fun String.titleCase(): String { + if (isNotEmpty()) { + val first = this[0] + if (first.isLowerCase()) { + return buildString { + val titleChar = first.titlecaseChar() + if (titleChar != first.uppercaseChar()) { + append(titleChar) + } else { + append(this@titleCase.substring(0, 1).uppercase(Locale.getDefault())) + } + + append(this@titleCase.substring(1)) + } + } + } + + return this +} + +fun String.elipsis(textLen: Int = 1995): String = if (this.length > textLen) { + "${this.slice(0..textLen)}..." +} else { + this +} diff --git a/bot/commons/src/main/kotlin/sh/nino/discord/common/extensions/TimeFormatExtensions.kt b/bot/commons/src/main/kotlin/sh/nino/discord/common/extensions/TimeFormatExtensions.kt new file mode 100644 index 00000000..c4b60b93 --- /dev/null +++ b/bot/commons/src/main/kotlin/sh/nino/discord/common/extensions/TimeFormatExtensions.kt @@ -0,0 +1,65 @@ +/* + * Copyright (c) 2019-2022 Nino + * + * 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. + */ + +package sh.nino.discord.common.extensions + +/** + * Format this [Long] into a readable byte format. + */ +fun Long.formatSize(): String { + val kilo = this / 1024L + val mega = kilo / 1024L + val giga = mega / 1024L + + return when { + kilo < 1024 -> "${kilo}KB" + mega < 1024 -> "${mega}MB" + else -> "${giga}GB" + } +} + +/** + * Returns the humanized time for a [java.lang.Long] instance + * @credit https://github.com/DV8FromTheWorld/Yui/blob/master/src/main/java/net/dv8tion/discord/commands/UptimeCommand.java#L34 + */ +fun Long.humanize(long: Boolean = false, includeMs: Boolean = false): String { + val months = this / 2592000000L % 12 + val weeks = this / 604800000L % 7 + val days = this / 86400000L % 30 + val hours = this / 3600000L % 24 + val minutes = this / 60000L % 60 + val seconds = this / 1000L % 60 + + val str = StringBuilder() + if (months > 0) str.append(if (long) "$months month${if (months == 1L) "" else "s"}, " else "${months}mo") + if (weeks > 0) str.append(if (long) "$weeks week${if (weeks == 1L) "" else "s"}, " else "${weeks}w") + if (days > 0) str.append(if (long) "$days day${if (days == 1L) "" else "s"}, " else "${days}d") + if (hours > 0) str.append(if (long) "$hours hour${if (hours == 1L) "" else "s"}, " else "${hours}h") + if (minutes > 0) str.append(if (long) "$minutes minute${if (minutes == 1L) "" else "s"}, " else "${minutes}m") + if (seconds > 0) str.append(if (long) "$seconds second${if (seconds == 1L) "" else "s"}${if (includeMs && this < 1000) ", " else ""}" else "${seconds}s") + + // Check if this is not over 1000 milliseconds (1 second), so we don't display + // 1 second, 1893 milliseconds + if (includeMs && this < 1000) str.append(if (long) "$this millisecond${if (this == 1L) "" else "s"}" else "${this}ms") + + return str.toString() +} diff --git a/bot/commons/src/main/kotlin/sh/nino/discord/common/ms.kt b/bot/commons/src/main/kotlin/sh/nino/discord/common/ms.kt new file mode 100644 index 00000000..914f75e1 --- /dev/null +++ b/bot/commons/src/main/kotlin/sh/nino/discord/common/ms.kt @@ -0,0 +1,94 @@ +/* + * Copyright (c) 2019-2022 Nino + * + * 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. + */ + +package sh.nino.discord.common + +import java.util.regex.Pattern +import kotlin.math.round + +// This is a Kotlin port of the NPM package: ms +// Project: https://github.com/vercel/ms/blob/master/src/index.ts + +private const val SECONDS = 1000 +private const val MINUTES = SECONDS * 60 +private const val HOURS = MINUTES * 60 +private const val DAYS = HOURS * 24 +private const val WEEKS = DAYS * 7 +private const val YEARS = DAYS * 365.25 +private val MS_REGEX = Pattern.compile("^(-?(?:\\d+)?\\.?\\d+) *(milliseconds?|msecs?|ms|seconds?|secs?|s|minutes?|mins?|m|hours?|hrs?|h|days?|d|weeks?|w|years?|yrs?|y)?\$", Pattern.CASE_INSENSITIVE) + +object ms { + /** + * Converts the [value] into the milliseconds needed. + * @param value The value to convert + * @throws NumberFormatException If `value` is not a non-empty number. + * @throws IllegalStateException If `value` is not a valid string. + */ + fun fromString(value: String): Long { + if (value.length > 100) throw IllegalStateException("Value exceeds the max length of 100 chars.") + + val matcher = MS_REGEX.matcher(value) + if (!matcher.matches()) throw IllegalStateException("Invalid value: `$value` (regex=$MS_REGEX)") + + val n = java.lang.Float.parseFloat(matcher.group(1)) + + return when (val type = (matcher.group(2) ?: "ms").lowercase()) { + "years", "year", "yrs", "yr", "y" -> (n * YEARS).toLong() + "weeks", "week", "w" -> (n * WEEKS).toLong() + "days", "day", "d" -> (n * DAYS).toLong() + "hours", "hour", "hrs", "hr", "h" -> (n * HOURS).toLong() + "minutes", "minute", "mins", "min", "m" -> (n * MINUTES).toLong() + "seconds", "second", "secs", "sec", "s" -> (n * SECONDS).toLong() + "milliseconds", "millisecond", "msecs", "msec", "ms" -> n.toLong() + else -> throw IllegalStateException("Unit $type was matched, but no matching cases exists.") + } + } + + /** + * Parse the given [value] to return a unified time string. + * + * @param value The value to convert from + * @param long Set to `true` to use verbose formatting. Defaults to `false`. + */ + fun fromLong(value: Long, long: Boolean = true): String = if (long) { + fun pluralize(ms: Long, msAbs: Long, n: Int, name: String): String { + val isPlural = msAbs >= n * 1.5 + return "${round((ms / n).toDouble())} $name${if (isPlural) "s" else ""}" + } + + val msAbs = kotlin.math.abs(value) + if (msAbs >= DAYS) pluralize(value, msAbs, DAYS, "day") + if (msAbs >= HOURS) pluralize(value, msAbs, DAYS, "hour") + if (msAbs >= MINUTES) pluralize(value, msAbs, DAYS, "minute") + if (msAbs >= SECONDS) pluralize(value, msAbs, DAYS, "second") + + "$value ms" + } else { + val msAbs = kotlin.math.abs(value) + if (msAbs >= DAYS) "${round((value / DAYS).toDouble())}d" + if (msAbs >= HOURS) "${round((value / HOURS).toDouble())}h" + if (msAbs >= MINUTES) "${round((value / MINUTES).toDouble())}m" + if (msAbs >= SECONDS) "${round((value / SECONDS).toDouble())}s" + + "${value}ms" + } +} diff --git a/bot/commons/src/main/kotlin/sh/nino/discord/common/serializers/StringOrArraySerializer.kt b/bot/commons/src/main/kotlin/sh/nino/discord/common/serializers/StringOrArraySerializer.kt new file mode 100644 index 00000000..5c510224 --- /dev/null +++ b/bot/commons/src/main/kotlin/sh/nino/discord/common/serializers/StringOrArraySerializer.kt @@ -0,0 +1,66 @@ +/* + * Copyright (c) 2019-2022 Nino + * + * 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. + */ + +package sh.nino.discord.common.serializers + +import kotlinx.serialization.KSerializer +import kotlinx.serialization.builtins.ListSerializer +import kotlinx.serialization.builtins.serializer +import kotlinx.serialization.descriptors.SerialDescriptor +import kotlinx.serialization.descriptors.buildClassSerialDescriptor +import kotlinx.serialization.encoding.Decoder +import kotlinx.serialization.encoding.Encoder +import sh.nino.discord.common.StringOrArray + +private val ListStringSerializer = ListSerializer(String.serializer()) + +object StringOrArraySerializer: KSerializer { + override val descriptor: SerialDescriptor = buildClassSerialDescriptor("nino.StringToArray") + + override fun deserialize(decoder: Decoder): StringOrArray { + return try { + val list = decoder.decodeSerializableValue(ListStringSerializer) + StringOrArray(list) + } catch (_: Exception) { + try { + val str = decoder.decodeString() + StringOrArray(str) + } catch (e: Exception) { + throw e + } + } + } + + override fun serialize(encoder: Encoder, value: StringOrArray) { + return try { + val list = value.asList + encoder.encodeSerializableValue(ListStringSerializer, list) + } catch (ex: Exception) { + try { + val str = value.asString + encoder.encodeString(str) + } catch (e: Exception) { + throw e + } + } + } +} diff --git a/bot/commons/src/main/kotlin/sh/nino/discord/common/unions/StringOrBoolean.kt b/bot/commons/src/main/kotlin/sh/nino/discord/common/unions/StringOrBoolean.kt new file mode 100644 index 00000000..50b10bc0 --- /dev/null +++ b/bot/commons/src/main/kotlin/sh/nino/discord/common/unions/StringOrBoolean.kt @@ -0,0 +1,33 @@ +/* + * Copyright (c) 2019-2022 Nino + * + * 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. + */ + +package sh.nino.discord.common.unions + +class StringOrBoolean(value: Any): XOrY(value) { + init { + check(value is String || value is Boolean) { + "Value was not a String or Boolean value." + } + } + + override fun toString(): String = value.toString() +} diff --git a/bot/commons/src/main/kotlin/sh/nino/discord/common/unions/XOrY.kt b/bot/commons/src/main/kotlin/sh/nino/discord/common/unions/XOrY.kt new file mode 100644 index 00000000..c8c68984 --- /dev/null +++ b/bot/commons/src/main/kotlin/sh/nino/discord/common/unions/XOrY.kt @@ -0,0 +1,52 @@ +/* + * Copyright (c) 2019-2022 Nino + * + * 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. + */ + +package sh.nino.discord.common.unions + +@Suppress("UNCHECKED_CAST") +open class XOrY(val value: Any) { + val asXOrNull: X? + get() = try { + value as? X + } catch (e: java.lang.ClassCastException) { + null + } + + val asYOrNull: Y? + get() = try { + value as? Y + } catch (e: java.lang.ClassCastException) { + null + } + + val asX: X + get() = asXOrNull ?: error("Value cannot be casted to X") + + val asY: Y + get() = asYOrNull ?: error("Value cannot be casted as Y") + + override fun toString(): String = buildString { + appendLine("value\$sh.nino.discord.common.XOrY {") + appendLine(" value = $value") + appendLine("}") + } +} diff --git a/bot/commons/src/test/kotlin/StringOrArrayTests.kt b/bot/commons/src/test/kotlin/StringOrArrayTests.kt new file mode 100644 index 00000000..fabf8f89 --- /dev/null +++ b/bot/commons/src/test/kotlin/StringOrArrayTests.kt @@ -0,0 +1,107 @@ +/* + * Copyright (c) 2019-2022 Nino + * + * 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. + */ + +package sh.nino.tests.common + +import io.kotest.assertions.throwables.shouldNotThrow +import io.kotest.assertions.throwables.shouldThrow +import io.kotest.core.spec.style.DescribeSpec +import io.kotest.matchers.shouldBe +import kotlinx.serialization.builtins.serializer +import kotlinx.serialization.json.Json +import sh.nino.discord.common.StringOrArray + +class StringOrArrayTests: DescribeSpec({ + describe("StringOrArray") { + it("should not return a error when initialized") { + shouldNotThrow { + StringOrArray("owo") + } + + shouldNotThrow { + StringOrArray(listOf("owo", "uwu")) + } + } + + it("should throw a error if initialized") { + shouldThrow { + StringOrArray(123) + } + + shouldThrow { + StringOrArray(true) + } + } + + it("should not throw if `StringOrArray.asList` is called, but throw if `StringOrArray.asString` is called") { + val instance = StringOrArray(listOf("owo", "uwu")) + shouldNotThrow { + instance.asList + } + + shouldThrow { + instance.asString + } + } + + it("should not throw if `StringOrArray.asString` is called, but throw if `StringOrArray.asList` is called") { + val instance = StringOrArray("owo da \${uwu}") + shouldNotThrow { + instance.asString + } + + shouldThrow { + instance.asList + } + } + } + + describe("StringOrArray - kotlinx.serialization") { + it("should be encoded successfully") { + val encoded = Json.encodeToString(StringOrArray.serializer(), StringOrArray("owo")) + encoded shouldBe "\"owo\"" + + val encodedString = Json.encodeToString(StringOrArray.serializer(), StringOrArray(listOf("owo"))) + encodedString shouldBe "[\"owo\"]" + } + + it("should be decoded successfully") { + val decoded = Json.decodeFromString(StringOrArray.serializer(), "\"owo\"") + shouldNotThrow { + decoded.asString + } + + shouldThrow { + decoded.asList + } + + val decodedList = Json.decodeFromString(StringOrArray.serializer(), "[\"owo\"]") + shouldNotThrow { + decodedList.asList + } + + shouldThrow { + decodedList.asString + } + } + } +}) diff --git a/bot/core/build.gradle.kts b/bot/core/build.gradle.kts new file mode 100644 index 00000000..7db0d662 --- /dev/null +++ b/bot/core/build.gradle.kts @@ -0,0 +1,32 @@ +/** + * Copyright (c) 2019-2022 Nino + * + * 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. + */ +plugins { + `nino-module` +} + +dependencies { + implementation(project(":bot:punishments")) + implementation(project(":bot:timeouts")) + implementation(project(":bot:database")) + implementation(project(":bot:metrics")) + implementation(project(":bot:automod")) +} diff --git a/bot/core/src/main/kotlin/sh/nino/discord/core/AutoSuspendCloseable.kt b/bot/core/src/main/kotlin/sh/nino/discord/core/AutoSuspendCloseable.kt new file mode 100644 index 00000000..f831413a --- /dev/null +++ b/bot/core/src/main/kotlin/sh/nino/discord/core/AutoSuspendCloseable.kt @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2019-2022 Nino + * + * 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. + */ + +package sh.nino.discord.core + +/** + * Represents a [AutoCloseable] interface but uses suspending functions + * rather than a synchronous function. + */ +interface AutoSuspendCloseable { + /** + * Closes this resource, possibly relinquishing any resources. This method + * cannot be invoked using the try-with-resources statement. + */ + suspend fun close() +} diff --git a/bot/core/src/main/kotlin/sh/nino/discord/core/NinoBot.kt b/bot/core/src/main/kotlin/sh/nino/discord/core/NinoBot.kt new file mode 100644 index 00000000..bf02a24c --- /dev/null +++ b/bot/core/src/main/kotlin/sh/nino/discord/core/NinoBot.kt @@ -0,0 +1,166 @@ +/* + * Copyright (c) 2019-2022 Nino + * + * 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. + */ + +package sh.nino.discord.core + +import dev.kord.common.annotation.KordExperimental +import dev.kord.common.annotation.KordUnsafe +import dev.kord.common.entity.ActivityType +import dev.kord.common.entity.DiscordBotActivity +import dev.kord.common.entity.PresenceStatus +import dev.kord.core.Kord +import dev.kord.gateway.DiscordPresence +import dev.kord.gateway.Intent +import dev.kord.gateway.Intents +import dev.kord.gateway.PrivilegedIntent +import dev.kord.rest.route.Route +import gay.floof.utils.slf4j.logging +import io.sentry.Sentry +import kotlinx.coroutines.launch +import org.koin.core.context.GlobalContext +import sh.nino.discord.common.DEDI_NODE +import sh.nino.discord.common.NinoInfo +import sh.nino.discord.common.data.Config +import sh.nino.discord.common.extensions.retrieve +import sh.nino.discord.common.extensions.retrieveAll +import sh.nino.discord.core.listeners.applyGenericEvents +import sh.nino.discord.core.listeners.applyGuildBanEvents +import sh.nino.discord.core.listeners.applyGuildEvents +import sh.nino.discord.core.listeners.applyGuildMemberEvents +import sh.nino.discord.core.listeners.applyUserEvents +import sh.nino.discord.core.listeners.applyVoiceStateEvents +import sh.nino.discord.core.localization.LocalizationManager +import sh.nino.discord.core.timers.TimerJob +import sh.nino.discord.core.timers.TimerManager +import sh.nino.discord.timeouts.Client +import java.lang.management.ManagementFactory +import java.util.concurrent.Executor +import java.util.concurrent.Executors + +class NinoBot { + private val logger by logging() + val bootTime = System.currentTimeMillis() + + @OptIn(KordUnsafe::class, KordExperimental::class, PrivilegedIntent::class) + suspend fun start() { + val runtime = Runtime.getRuntime() + val os = ManagementFactory.getOperatingSystemMXBean() + val threads = ManagementFactory.getThreadMXBean() + + val free = runtime.freeMemory() / 1024L / 1024L + val total = runtime.totalMemory() / 1024L / 1024L + val maxMem = runtime.maxMemory() / 1024L / 1024L + + logger.info("Displaying runtime information:") + logger.info("* Free / Total (Max) Memory: ${free}MiB/${total}MiB (${maxMem}MiB)") + logger.info("* Threads: ${threads.threadCount} (${threads.daemonThreadCount} daemon'd)") + logger.info("* JVM: v${System.getProperty("java.version")} (${System.getProperty("java.vendor", "")})") + logger.info("* Kotlin: v${KotlinVersion.CURRENT}") + logger.info("* Operating System: ${os.name} with ${os.availableProcessors} processors (${os.arch}; ${os.version})") + + if (DEDI_NODE != "none") + logger.info("* Dedi Node: $DEDI_NODE") + + val kord = GlobalContext.retrieve() + val config = GlobalContext.retrieve() + val gatewayInfo = kord.rest.unsafe(Route.GatewayBotGet) {} + + logger.info("Displaying gateway information:") + logger.info("* Shards to launch: ${gatewayInfo.shards}") + logger.info("* Session Limit: ${gatewayInfo.sessionStartLimit.remaining}/${gatewayInfo.sessionStartLimit.total}") + + // Initialize localization + logger.info("* Initializing localization manager...") + GlobalContext.retrieve() + + // Setup Sentry + if (config.sentryDsn != null) { + logger.info("* Installing Sentry...") + Sentry.init { + it.dsn = config.sentryDsn + it.release = "v${NinoInfo.VERSION} (${NinoInfo.COMMIT_SHA})" + } + + Sentry.configureScope { + it.tags += mutableMapOf( + "nino.environment" to config.environment.toString(), + "nino.build.date" to NinoInfo.BUILD_DATE, + "nino.commitSha" to NinoInfo.COMMIT_SHA, + "nino.version" to NinoInfo.VERSION, + "system.user" to System.getProperty("user.name"), + "system.os" to "${os.name} (${os.arch}; ${os.version})" + ) + } + } + + // Startup the timeouts client in a different coroutine scope + // since it will block this thread (and we don't want that.) + NinoScope.launch { + val timeouts = GlobalContext.retrieve() + timeouts.connect() + } + + // Schedule all timer jobs + val scheduler = GlobalContext.retrieve() + val jobs = GlobalContext.retrieveAll() + scheduler.bulkSchedule(*jobs.toTypedArray()) + + // Startup Kord + kord.applyGenericEvents() + kord.applyGuildEvents() + kord.applyGuildMemberEvents() + kord.applyUserEvents() + kord.applyVoiceStateEvents() + kord.applyGuildBanEvents() + + kord.login { + presence = DiscordPresence( + status = PresenceStatus.Idle, + game = DiscordBotActivity( + name = "server fans go whirr...", + type = ActivityType.Listening + ), + + afk = true, + since = System.currentTimeMillis() + ) + + intents = Intents { + +Intent.Guilds + +Intent.GuildMessages + +Intent.GuildBans + +Intent.GuildVoiceStates + +Intent.GuildMembers + } + } + } + + fun sentryReport(ex: Exception) { + if (Sentry.isEnabled()) { + Sentry.captureException(ex) + } + } + + companion object { + val executorPool: Executor = Executors.newCachedThreadPool(NinoThreadFactory) + } +} diff --git a/src/singletons/Prisma.ts b/bot/core/src/main/kotlin/sh/nino/discord/core/NinoScope.kt similarity index 73% rename from src/singletons/Prisma.ts rename to bot/core/src/main/kotlin/sh/nino/discord/core/NinoScope.kt index abbb7706..ca4f09b1 100644 --- a/src/singletons/Prisma.ts +++ b/bot/core/src/main/kotlin/sh/nino/discord/core/NinoScope.kt @@ -1,5 +1,5 @@ -/** - * Copyright (c) 2019-2021 Nino +/* + * Copyright (c) 2019-2022 Nino * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -20,15 +20,13 @@ * SOFTWARE. */ -import { PrismaClient } from '.prisma/client'; -import { Container } from '@augu/lilith'; -import { Logger } from 'tslog'; +package sh.nino.discord.core -export async function teardown(this: Container, $prisma: PrismaClient) { - const logger = this.get('logger'); - logger.warn('Disconnecting Prisma client...'); +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.asCoroutineDispatcher +import kotlin.coroutines.CoroutineContext - await $prisma.$disconnect(); +object NinoScope: CoroutineScope { + override val coroutineContext: CoroutineContext = SupervisorJob() + NinoBot.executorPool.asCoroutineDispatcher() } - -export default new PrismaClient(); diff --git a/bot/core/src/main/kotlin/sh/nino/discord/core/NinoThreadFactory.kt b/bot/core/src/main/kotlin/sh/nino/discord/core/NinoThreadFactory.kt new file mode 100644 index 00000000..90c208fc --- /dev/null +++ b/bot/core/src/main/kotlin/sh/nino/discord/core/NinoThreadFactory.kt @@ -0,0 +1,50 @@ +/* + * Copyright (c) 2019-2022 Nino + * + * 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. + */ + +package sh.nino.discord.core + +import kotlinx.atomicfu.atomic +import java.util.concurrent.ThreadFactory + +object NinoThreadFactory: ThreadFactory { + private val threadIdCounter = atomic(0) + private val threadGroup: ThreadGroup by lazy { + // TODO: move to Thread.currentThread().threadGroup + val security = System.getSecurityManager() + + if (security != null && security.threadGroup != null) { + security.threadGroup + } else { + Thread.currentThread().threadGroup + } + } + + override fun newThread(r: Runnable): Thread { + val name = "Nino-ExecutorThread[${threadIdCounter.incrementAndGet()}]" + val thread = Thread(threadGroup, r, name) + + if (thread.priority != Thread.NORM_PRIORITY) + thread.priority = Thread.NORM_PRIORITY + + return thread + } +} diff --git a/bot/core/src/main/kotlin/sh/nino/discord/core/annotations/NinoDslMarker.kt b/bot/core/src/main/kotlin/sh/nino/discord/core/annotations/NinoDslMarker.kt new file mode 100644 index 00000000..300b30c1 --- /dev/null +++ b/bot/core/src/main/kotlin/sh/nino/discord/core/annotations/NinoDslMarker.kt @@ -0,0 +1,26 @@ +/* + * Copyright (c) 2019-2022 Nino + * + * 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. + */ + +package sh.nino.discord.core.annotations + +@DslMarker +annotation class NinoDslMarker diff --git a/src/util/Stopwatch.ts b/bot/core/src/main/kotlin/sh/nino/discord/core/interceptors/LoggingInterceptor.kt similarity index 55% rename from src/util/Stopwatch.ts rename to bot/core/src/main/kotlin/sh/nino/discord/core/interceptors/LoggingInterceptor.kt index fefd9a21..9c5155ae 100644 --- a/src/util/Stopwatch.ts +++ b/bot/core/src/main/kotlin/sh/nino/discord/core/interceptors/LoggingInterceptor.kt @@ -1,5 +1,5 @@ -/** - * Copyright (c) 2019-2021 Nino +/* + * Copyright (c) 2019-2022 Nino * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -20,36 +20,26 @@ * SOFTWARE. */ -import { performance } from 'perf_hooks'; +package sh.nino.discord.core.interceptors -/** - * Utility stopwatch for calculating duration on asynchronous execution - */ -export default class Stopwatch { - private startTime?: number; - private endTime?: number; - - private symbolOf(type: number) { - if (type > 1000) return `${type.toFixed(1)}s`; - if (type > 1) return `${type.toFixed(1)}ms`; - return `${type.toFixed(1)}µs`; - } - - restart() { - this.startTime = performance.now(); - this.endTime = undefined; - } +import gay.floof.utils.slf4j.logging +import okhttp3.Interceptor +import okhttp3.Response +import org.apache.commons.lang3.time.StopWatch +import java.util.concurrent.TimeUnit - start() { - if (this.startTime !== undefined) throw new SyntaxError('Stopwatch has already started'); +class LoggingInterceptor: Interceptor { + private val log by logging() - this.startTime = performance.now(); - } + override fun intercept(chain: Interceptor.Chain): Response { + val request = chain.request() + val watch = StopWatch.createStarted() - end() { - if (!this.startTime) throw new TypeError('Stopwatch has not started'); + log.info("-> ${request.method.uppercase()} ${request.url}") + val res = chain.proceed(request) + watch.stop() - this.endTime = performance.now(); - return this.symbolOf(this.endTime - this.startTime); - } + log.info("<- [${res.code} ${res.message.ifEmpty { "OK" }} / ${res.protocol.toString().replace("h2", "http/2")}] ${request.method.uppercase()} ${request.url} [${watch.getTime(TimeUnit.MILLISECONDS)}ms]") + return res + } } diff --git a/bot/core/src/main/kotlin/sh/nino/discord/core/interceptors/SentryInterceptor.kt b/bot/core/src/main/kotlin/sh/nino/discord/core/interceptors/SentryInterceptor.kt new file mode 100644 index 00000000..37d7f5c1 --- /dev/null +++ b/bot/core/src/main/kotlin/sh/nino/discord/core/interceptors/SentryInterceptor.kt @@ -0,0 +1,68 @@ +/* + * Copyright (c) 2019-2022 Nino + * + * 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. + */ + +package sh.nino.discord.core.interceptors + +import io.sentry.* +import okhttp3.Interceptor +import okhttp3.Response +import java.io.IOException + +class SentryInterceptor: Interceptor { + private val hub: IHub = HubAdapter.getInstance() + + override fun intercept(chain: Interceptor.Chain): Response { + var request = chain.request() + val url = "${request.method} ${request.url.encodedPath}" + val span = hub.span?.startChild("nino.http.client", "Request $url") + var statusCode = 200 + var response: Response? = null + + return try { + span?.toSentryTrace()?.let { + request = request + .newBuilder() + .addHeader(it.name, it.value) + .build() + } + + response = chain.proceed(request) + statusCode = response.code + span?.status = SpanStatus.fromHttpStatusCode(statusCode) + + response + } catch (e: IOException) { + span?.apply { + this.throwable = e + this.status = SpanStatus.INTERNAL_ERROR + } + + throw e + } finally { + span?.finish() + + val breb = Breadcrumb.http(request.url.toString(), request.method, statusCode) + breb.level = if (response?.isSuccessful == true) SentryLevel.FATAL else SentryLevel.ERROR + hub.addBreadcrumb(breb) + } + } +} diff --git a/bot/core/src/main/kotlin/sh/nino/discord/core/jobs/BotlistJob.kt b/bot/core/src/main/kotlin/sh/nino/discord/core/jobs/BotlistJob.kt new file mode 100644 index 00000000..bd92d797 --- /dev/null +++ b/bot/core/src/main/kotlin/sh/nino/discord/core/jobs/BotlistJob.kt @@ -0,0 +1,263 @@ +/* + * Copyright (c) 2019-2022 Nino + * + * 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. + */ + +package sh.nino.discord.core.jobs + +import dev.kord.core.Kord +import gay.floof.utils.slf4j.logging +import io.ktor.client.* +import io.ktor.client.call.* +import io.ktor.client.request.* +import io.ktor.client.statement.* +import io.ktor.http.* +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.toList +import kotlinx.coroutines.withContext +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.JsonPrimitive +import org.apache.commons.lang3.time.StopWatch +import sh.nino.discord.common.data.Config +import sh.nino.discord.core.timers.TimerJob +import java.util.concurrent.TimeUnit + +private data class BotlistResult( + val name: String, + val success: Boolean, + val time: Long, + val data: JsonObject +) + +class BotlistJob( + private val config: Config, + private val httpClient: HttpClient, + private val kord: Kord +): TimerJob( + name = "botlists", + interval = 86400000 +) { + private val logger by logging() + + override suspend fun execute() { + if (config.botlists == null) return + + val guilds = kord.guilds.toList().size + val shardCount = kord.gateway.gateways.size + val data = mutableListOf() + val botlistWatch = StopWatch.createStarted() + + if (config.botlists!!.discordServicesToken != null) { + logger.info("* Found discordservices.net token!") + + val stopwatch = StopWatch.createStarted() + val res: HttpResponse = httpClient.post("https://api.discordservices.net/bot/${kord.selfId}/stats") { + body = JsonObject( + mapOf( + "server_count" to JsonPrimitive(guilds) + ) + ) + + header("Authorization", config.botlists!!.discordServicesToken) + } + + stopwatch.stop() + val success = res.status.isSuccess() + val json = withContext(Dispatchers.IO) { + res.receive() + } + + data.add( + BotlistResult( + "discordservices.net", + success, + stopwatch.time, + json + ) + ) + } + + if (config.botlists!!.discordBoatsToken != null) { + logger.info("* Found discord.boats token!") + + val stopwatch = StopWatch.createStarted() + val res: HttpResponse = httpClient.post("https://discord.boats/api/bot/${kord.selfId}") { + body = JsonObject( + mapOf( + "server_count" to JsonPrimitive(guilds) + ) + ) + + header("Authorization", config.botlists!!.discordBoatsToken) + } + + stopwatch.stop() + val success = res.status.isSuccess() + val json = withContext(Dispatchers.IO) { + res.receive() + } + + data.add( + BotlistResult( + "discord.boats", + success, + stopwatch.time, + json + ) + ) + } + + if (config.botlists!!.discordBotsToken != null) { + logger.info("* Found discord.bots.gg token!") + + val stopwatch = StopWatch.createStarted() + val res: HttpResponse = httpClient.post("https://discord.bots.gg/api/v1/bots/${kord.selfId}/stats") { + body = JsonObject( + mapOf( + "guildCount" to JsonPrimitive(guilds), + "shardCount" to JsonPrimitive(shardCount) + ) + ) + + header("Authorization", config.botlists!!.discordBotsToken) + } + + stopwatch.stop() + val success = res.status.isSuccess() + val json = withContext(Dispatchers.IO) { + res.receive() + } + + data.add( + BotlistResult( + "discord.bots.gg", + success, + stopwatch.time, + json + ) + ) + } + + if (config.botlists!!.discordsToken != null) { + logger.info("* Found discords.com token!") + + val stopwatch = StopWatch.createStarted() + val res: HttpResponse = httpClient.post("https://discords.com/bots/api/${kord.selfId}") { + body = JsonObject( + mapOf( + "server_count" to JsonPrimitive(guilds) + ) + ) + + header("Authorization", config.botlists!!.discordsToken) + } + + stopwatch.stop() + val success = res.status.isSuccess() + val json = withContext(Dispatchers.IO) { + res.receive() + } + + data.add( + BotlistResult( + "discords.com", + success, + stopwatch.time, + json + ) + ) + } + + if (config.botlists!!.topGGToken != null) { + logger.info("* Found top.gg token!") + + val stopwatch = StopWatch.createStarted() + val res: HttpResponse = httpClient.post("https://top.gg/api/bots/${kord.selfId}/stats") { + body = JsonObject( + mapOf( + "server_count" to JsonPrimitive(guilds), + "shard_count" to JsonPrimitive(shardCount) + ) + ) + + header("Authorization", config.botlists!!.topGGToken) + } + + stopwatch.stop() + val success = res.status.isSuccess() + val json = withContext(Dispatchers.IO) { + res.receive() + } + + data.add( + BotlistResult( + "top.gg", + success, + stopwatch.time, + json + ) + ) + } + + // botlist by a cute fox, a carrot, and a funny api blob + if (config.botlists!!.dellyToken != null) { + logger.info("* Found Delly (Discord Extreme List) token!") + + val stopwatch = StopWatch.createStarted() + val res: HttpResponse = httpClient.post("https://api.discordextremelist.xyz/v2/bot/${kord.selfId}/stats") { + body = JsonObject( + mapOf( + "guildCount" to JsonPrimitive(guilds), + "shardCount" to JsonPrimitive(shardCount) + ) + ) + + header("Authorization", config.botlists!!.dellyToken) + } + + stopwatch.stop() + val success = res.status.isSuccess() + val json = withContext(Dispatchers.IO) { + res.receive() + } + + data.add( + BotlistResult( + "Delly", + success, + stopwatch.time, + json + ) + ) + } + + botlistWatch.stop() + logger.info("Took ${botlistWatch.getTime(TimeUnit.MILLISECONDS)}ms to post to ${data.size} bot lists.") + + logger.info("----------") + for (list in data) { + logger.info("|- ${list.name}") + logger.info("\\- Took ${list.time}ms to post data.") + logger.info("\\- ${if (list.success) "and it was successful" else "was not successful"}") + logger.info(list.data.toString()) + } + logger.info("----------") + } +} diff --git a/bot/core/src/main/kotlin/sh/nino/discord/core/jobs/GatewayPingJob.kt b/bot/core/src/main/kotlin/sh/nino/discord/core/jobs/GatewayPingJob.kt new file mode 100644 index 00000000..839233a4 --- /dev/null +++ b/bot/core/src/main/kotlin/sh/nino/discord/core/jobs/GatewayPingJob.kt @@ -0,0 +1,83 @@ +/* + * Copyright (c) 2019-2022 Nino + * + * 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. + */ + +package sh.nino.discord.core.jobs + +import dev.kord.core.Kord +import gay.floof.utils.slf4j.logging +import io.ktor.client.* +import io.ktor.client.call.* +import io.ktor.client.request.* +import io.ktor.client.statement.* +import io.ktor.http.* +import kotlinx.serialization.Serializable +import sh.nino.discord.common.data.Config +import sh.nino.discord.core.timers.TimerJob +import sh.nino.discord.metrics.MetricsRegistry +import kotlin.time.Duration +import kotlin.time.DurationUnit + +@Serializable +data class InstatusPostMetricBody( + val timestamp: Long, + val value: Long +) + +class GatewayPingJob( + private val config: Config, + private val httpClient: HttpClient, + private val metrics: MetricsRegistry, + private val kord: Kord +): TimerJob( + "gateway.ping", + 5000 +) { + private val log by logging() + + override suspend fun execute() { + if (metrics.enabled) { + val averagePing = kord.gateway.averagePing ?: Duration.ZERO + metrics.gatewayPing?.set(averagePing.inWholeMilliseconds.toDouble()) + + // Log the duration for all shards + for ((shardId, shard) in kord.gateway.gateways) { + metrics.gatewayLatency?.labels("$shardId")?.set((shard.ping.value ?: Duration.ZERO).inWholeMilliseconds.toDouble()) + } + } + + if (config.instatus != null && config.instatus!!.gatewayMetricId != null) { + log.debug("Instatus configuration is available, now posting to Instatus...") + val res: HttpResponse = httpClient.post("") { + body = InstatusPostMetricBody( + timestamp = System.currentTimeMillis(), + value = (kord.gateway.averagePing ?: Duration.ZERO).toLong(DurationUnit.MILLISECONDS) + ) + + header("Authorization", config.instatus!!.token) + } + + if (!res.status.isSuccess()) { + log.warn("Unable to post to Instatus (${res.status.value} ${res.status.description}): ${res.receive()}") + } + } + } +} diff --git a/bot/core/src/main/kotlin/sh/nino/discord/core/jobs/JobModule.kt b/bot/core/src/main/kotlin/sh/nino/discord/core/jobs/JobModule.kt new file mode 100644 index 00000000..6e25248b --- /dev/null +++ b/bot/core/src/main/kotlin/sh/nino/discord/core/jobs/JobModule.kt @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2019-2022 Nino + * + * 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. + */ + +package sh.nino.discord.core.jobs + +import org.koin.dsl.bind +import org.koin.dsl.module +import sh.nino.discord.core.timers.TimerJob + +val jobsModule = module { + single { BotlistJob(get(), get(), get()) } bind TimerJob::class + single { GatewayPingJob(get(), get(), get(), get()) } bind TimerJob::class +} diff --git a/bot/core/src/main/kotlin/sh/nino/discord/core/koinModule.kt b/bot/core/src/main/kotlin/sh/nino/discord/core/koinModule.kt new file mode 100644 index 00000000..46940602 --- /dev/null +++ b/bot/core/src/main/kotlin/sh/nino/discord/core/koinModule.kt @@ -0,0 +1,104 @@ +/* + * Copyright (c) 2019-2022 Nino + * + * 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. + */ + +package sh.nino.discord.core + +import io.ktor.client.* +import io.ktor.client.engine.okhttp.* +import io.ktor.client.features.* +import io.ktor.client.features.json.* +import io.ktor.client.features.json.serializer.* +import io.ktor.client.features.websocket.* +import io.sentry.Sentry +import kotlinx.serialization.json.Json +import org.koin.dsl.module +import sh.nino.discord.common.NinoInfo +import sh.nino.discord.common.data.Config +import sh.nino.discord.core.interceptors.LoggingInterceptor +import sh.nino.discord.core.interceptors.SentryInterceptor +import sh.nino.discord.core.localization.LocalizationManager +import sh.nino.discord.core.timers.TimerManager +import sh.nino.discord.metrics.MetricsRegistry +import sh.nino.discord.timeouts.Client + +val globalModule = module { + single { + NinoBot() + } + + single { + Json { + ignoreUnknownKeys = true + isLenient = true + } + } + + single { + HttpClient(OkHttp) { + engine { + config { + followRedirects(true) + addInterceptor(LoggingInterceptor()) + + if (Sentry.isEnabled()) { + addInterceptor(SentryInterceptor()) + } + } + } + + install(WebSockets) + install(JsonFeature) { + serializer = KotlinxSerializer(get()) + } + + install(UserAgent) { + agent = "Nino/DiscordBot (+https://github.com/NinoDiscord/Nino; v${NinoInfo.VERSION})" + } + } + } + + single { + LocalizationManager(get()) + } + + single { + val config: Config = get() + Client { + coroutineScope = NinoScope + httpClient = get() + json = get() + uri = config.timeouts.uri + + if (config.timeouts.auth != null) { + auth = config.timeouts.auth as String + } + } + } + + single { + TimerManager() + } + + single { + MetricsRegistry(get()) + } +} diff --git a/bot/core/src/main/kotlin/sh/nino/discord/core/listeners/GenericListener.kt b/bot/core/src/main/kotlin/sh/nino/discord/core/listeners/GenericListener.kt new file mode 100644 index 00000000..cf6fce83 --- /dev/null +++ b/bot/core/src/main/kotlin/sh/nino/discord/core/listeners/GenericListener.kt @@ -0,0 +1,114 @@ +/* + * Copyright (c) 2019-2022 Nino + * + * 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. + */ + +package sh.nino.discord.core.listeners + +import dev.kord.common.entity.ActivityType +import dev.kord.core.Kord +import dev.kord.core.event.Event +import dev.kord.core.event.gateway.DisconnectEvent +import dev.kord.core.event.gateway.ReadyEvent +import dev.kord.core.on +import kotlinx.coroutines.flow.count +import org.koin.core.context.GlobalContext +import org.slf4j.LoggerFactory +import sh.nino.discord.common.data.Config +import sh.nino.discord.common.extensions.humanize +import sh.nino.discord.common.extensions.name +import sh.nino.discord.common.extensions.retrieve +import sh.nino.discord.core.NinoBot +import sh.nino.discord.metrics.MetricsRegistry + +fun Kord.applyGenericEvents() { + val logger = LoggerFactory.getLogger("sh.nino.discord.core.listeners.GenericListenerKt") + val nino = GlobalContext.retrieve() + val metrics = GlobalContext.retrieve() + + on { + logger.info("Successfully launched bot as ${this.self.tag} (${this.self.id}) on shard #${this.shard} in ${(System.currentTimeMillis() - nino.bootTime).humanize(true)}") + logger.info("Ready in ${this.guilds.size} guilds! | Using Discord Gateway v${this.gatewayVersion}") + + val config = GlobalContext.retrieve() + val guildCount = kord.guilds.count() + val currStatus = config.status.status + .replace("{shard_id}", "$shard") + .replace("{guilds}", "$guildCount") + + // Set guild count to whatever it is listed + if (metrics.enabled) { + metrics.guildCount?.set(guildCount.toDouble()) + } + + kord.editPresence { + status = config.status.presence + when (config.status.type) { + ActivityType.Listening -> listening(currStatus) + ActivityType.Game -> playing(currStatus) + ActivityType.Competing -> competing(currStatus) + ActivityType.Watching -> watching(currStatus) + else -> { + playing(currStatus) + } + } + } + } + + on { + val reason = buildString { + if (this@on is DisconnectEvent.DetachEvent) + append("Shard #${this@on.shard} has been detached.") + + if (this@on is DisconnectEvent.UserCloseEvent) + append("Closed by you.") + + if (this@on is DisconnectEvent.TimeoutEvent) + append("Possible internet connection loss; something was timed out. :<") + + if (this@on is DisconnectEvent.DiscordCloseEvent) { + val event = this@on + append("Discord closed off our connection (${event.closeCode.name} ~ ${event.closeCode.code}; recoverable=${if (event.recoverable) "yes" else "no"})") + } + + if (this@on is DisconnectEvent.RetryLimitReachedEvent) + append("Failed to established connection too many times.") + + if (this@on is DisconnectEvent.ReconnectingEvent) + append("Requested reconnect from Discord.") + + if (this@on is DisconnectEvent.SessionReset) + append("Gateway was closed; attempting to start new session.") + + if (this@on is DisconnectEvent.ZombieConnectionEvent) + append("Discord is no longer responding to gateway commands.") + } + + logger.warn("Shard #${this.shard} has disconnected from the world: $reason") + } + + on { + if (metrics.enabled) { + metrics.websocketEvents?.labels("$shard", this.name)?.inc() + } + } + + logger.info("✔ Registered all generic events!") +} diff --git a/src/entities/WarningsEntity.ts b/bot/core/src/main/kotlin/sh/nino/discord/core/listeners/GuildBansListener.kt similarity index 70% rename from src/entities/WarningsEntity.ts rename to bot/core/src/main/kotlin/sh/nino/discord/core/listeners/GuildBansListener.kt index a0c0e247..e676a1af 100644 --- a/src/entities/WarningsEntity.ts +++ b/bot/core/src/main/kotlin/sh/nino/discord/core/listeners/GuildBansListener.kt @@ -1,5 +1,5 @@ -/** - * Copyright (c) 2019-2021 Nino +/* + * Copyright (c) 2019-2022 Nino * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -20,22 +20,22 @@ * SOFTWARE. */ -import { Entity, Column, PrimaryColumn, PrimaryGeneratedColumn } from 'typeorm'; +package sh.nino.discord.core.listeners -@Entity({ name: 'warnings' }) -export default class WarningsEntity { - @PrimaryColumn({ name: 'guild_id' }) - public guildID!: string; +import dev.kord.core.Kord +import dev.kord.core.event.guild.BanAddEvent +import dev.kord.core.event.guild.BanRemoveEvent +import dev.kord.core.on +import org.slf4j.LoggerFactory - @Column({ default: undefined, nullable: true }) - public reason?: string; +fun Kord.applyGuildBanEvents() { + val log = LoggerFactory.getLogger("sh.nino.discord.core.listeners.GuildBansListenerKt") - @Column({ default: 0 }) - public amount!: number; + on { + } - @Column({ name: 'user_id' }) - public userID!: string; + on { + } - @PrimaryGeneratedColumn() - public id!: number; + log.info("✔ Registered all guild ban events!") } diff --git a/bot/core/src/main/kotlin/sh/nino/discord/core/listeners/GuildListener.kt b/bot/core/src/main/kotlin/sh/nino/discord/core/listeners/GuildListener.kt new file mode 100644 index 00000000..0fb07601 --- /dev/null +++ b/bot/core/src/main/kotlin/sh/nino/discord/core/listeners/GuildListener.kt @@ -0,0 +1,171 @@ +/* + * Copyright (c) 2019-2022 Nino + * + * 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. + */ + +package sh.nino.discord.core.listeners + +import dev.kord.common.entity.ActivityType +import dev.kord.core.Kord +import dev.kord.core.behavior.channel.createMessage +import dev.kord.core.entity.channel.TextChannel +import dev.kord.core.event.guild.GuildDeleteEvent +import dev.kord.core.on +import kotlinx.coroutines.flow.filter +import kotlinx.coroutines.flow.toList +import org.jetbrains.exposed.sql.deleteWhere +import org.koin.core.context.GlobalContext +import org.slf4j.LoggerFactory +import sh.nino.discord.common.data.Config +import sh.nino.discord.common.extensions.asSnowflake +import sh.nino.discord.common.extensions.runSuspended +import sh.nino.discord.database.asyncTransaction +import sh.nino.discord.database.tables.* +import sh.nino.discord.metrics.MetricsRegistry + +fun Kord.applyGuildEvents() { + val log = LoggerFactory.getLogger("sh.nino.discord.core.listeners.GuildListenerKt") + val koin = GlobalContext.get() + val metrics = koin.get() + val config = koin.get() + + // this is commented out due to: + // https://canary.discord.com/channels/556525343595298817/631147109311053844/936066300218835014 + +// on { +// log.info("New Guild Joined - ${guild.name} (${guild.id})") +// asyncTransaction { +// GuildSettingsEntity.new(guild.id.value.toLong()) {} +// AutomodEntity.new(guild.id.value.toLong()) {} +// LoggingEntity.new(guild.id.value.toLong()) {} +// } +// +// metrics.guildCount?.inc() +// kord.getChannelOf("844410521599737878".asSnowflake())?.runSuspended { +// val humans = this@on.guild.members.filter { +// !it.isBot +// }.toList() +// +// val bots = this@on.guild.members.filter { +// it.isBot +// }.toList() +// +// val ratio = ((humans.size * bots.size) / this@on.guild.members.toList().size).toDouble() +// val owner = this@on.guild.owner.asMember() +// +// createMessage( +// buildString { +// appendLine("```md") +// appendLine("# Joined ${this@on.guild.name} (${this@on.guild.id})") +// appendLine() +// appendLine("• Members [ Bots / Total ]: ${bots.size} / ${humans.size} ($ratio%)") +// appendLine("• Owner: ${owner.tag} (${owner.id})") +// appendLine("```") +// } +// ) +// +// val currStatus = config.status.status +// .replace("{shard_id}", shard.toString()) +// .replace("{guilds}", kord.guilds.toList().size.toString()) +// +// kord.editPresence { +// status = config.status.presence +// when (config.status.type) { +// ActivityType.Listening -> listening(currStatus) +// ActivityType.Game -> playing(currStatus) +// ActivityType.Competing -> competing(currStatus) +// ActivityType.Watching -> watching(currStatus) +// else -> { +// playing(currStatus) +// } +// } +// } +// } +// } + + on { + if (unavailable) { + log.warn("Guild ${guild?.name ?: "(unknown)"} (${guild?.id ?: "(unknown ID)"}) went unavailable, not doing anything.") + return@on + } + + if (guild == null) { + log.warn("Left uncached guild, cannot say anything about it. :<") + return@on + } + + log.info("Left Guild - ${guild!!.name} (${guild!!.id})") + asyncTransaction { + GuildSettings.deleteWhere { + GuildSettings.id eq guild!!.id.value.toLong() + } + + AutomodTable.deleteWhere { + AutomodTable.id eq guild!!.id.value.toLong() + } + + GuildLogging.deleteWhere { + GuildLogging.id eq guild!!.id.value.toLong() + } + } + + metrics.guildCount?.dec() + kord.getChannelOf("844410521599737878".asSnowflake())?.runSuspended { + val humans = this@on.guild!!.members.filter { + !it.isBot + }.toList() + + val bots = this@on.guild!!.members.filter { + it.isBot + }.toList() + + val ratio = ((humans.size * bots.size) / this@on.guild!!.members.toList().size).toDouble() + val owner = this@on.guild!!.owner.asMember() + + createMessage( + buildString { + appendLine("```md") + appendLine("# Left ${this@on.guild!!.name} (${this@on.guild!!.id})") + appendLine() + appendLine("• Members [ Bots / Total ]: ${bots.size} / ${humans.size} ($ratio%)") + appendLine("• Owner: ${owner.tag} (${owner.id})") + appendLine("```") + } + ) + + val currStatus = config.status.status + .replace("{shard_id}", shard.toString()) + .replace("{guilds}", kord.guilds.toList().size.toString()) + + kord.editPresence { + status = config.status.presence + when (config.status.type) { + ActivityType.Listening -> listening(currStatus) + ActivityType.Game -> playing(currStatus) + ActivityType.Competing -> competing(currStatus) + ActivityType.Watching -> watching(currStatus) + else -> { + playing(currStatus) + } + } + } + } + } +} diff --git a/bot/core/src/main/kotlin/sh/nino/discord/core/listeners/GuildMemberListener.kt b/bot/core/src/main/kotlin/sh/nino/discord/core/listeners/GuildMemberListener.kt new file mode 100644 index 00000000..3d94e5b6 --- /dev/null +++ b/bot/core/src/main/kotlin/sh/nino/discord/core/listeners/GuildMemberListener.kt @@ -0,0 +1,233 @@ +/* + * Copyright (c) 2019-2022 Nino + * + * 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. + */ + +package sh.nino.discord.core.listeners + +import dev.kord.common.entity.AuditLogEvent +import dev.kord.common.entity.DiscordAuditLogEntry +import dev.kord.common.entity.Permission +import dev.kord.common.entity.Snowflake +import dev.kord.core.Kord +import dev.kord.core.entity.Member +import dev.kord.core.event.guild.MemberJoinEvent +import dev.kord.core.event.guild.MemberLeaveEvent +import dev.kord.core.event.guild.MemberUpdateEvent +import dev.kord.core.firstOrNull +import dev.kord.core.on +import dev.kord.rest.json.request.AuditLogGetRequest +import org.jetbrains.exposed.sql.and +import org.jetbrains.exposed.sql.update +import org.koin.core.context.GlobalContext +import org.slf4j.LoggerFactory +import sh.nino.discord.automod.core.Container +import sh.nino.discord.common.extensions.contains +import sh.nino.discord.common.extensions.createdAt +import sh.nino.discord.database.asyncTransaction +import sh.nino.discord.database.tables.* +import sh.nino.discord.punishments.MemberLike +import sh.nino.discord.punishments.PunishmentModule +import sh.nino.discord.timeouts.Client +import sh.nino.discord.timeouts.RequestCommand +import sh.nino.discord.timeouts.Timeout + +private suspend fun getAuditLogEntriesOf( + kord: Kord, + self: Member, + guildId: Snowflake, + userId: Snowflake, + action: AuditLogEvent +): DiscordAuditLogEntry? { + val auditLogs = kord.rest.auditLog.getAuditLogs( + guildId, + AuditLogGetRequest( + userId, + limit = 3, + action = action + ) + ) + + return auditLogs.auditLogEntries.sortedWith { a, b -> + b.id.createdAt.toEpochMilliseconds().toInt() - a.id.createdAt.toEpochMilliseconds().toInt() + }.firstOrNull() +} + +fun Kord.applyGuildMemberEvents() { + val log = LoggerFactory.getLogger("sh.nino.discord.core.listeners.GuildMemberListenerKt") + val koin = GlobalContext.get() + val timeouts = koin.get() + val punishments = koin.get() + + on { + val guild = getGuild() + val user = member.asUser() + + log.info("User ${user.tag} (${user.id}) joined ${guild.name} (${guild.id}) - applying automod!") + val executed = Container.execute(this) + if (executed) return@on + + val cases = asyncTransaction { + GuildCasesEntity.find { + (GuildCases.id eq guild.id.value.toLong()) and (GuildCases.victimId eq user.id.value.toLong()) + } + } + + // Check if there were previous cases, + // if there is none, just skip. + if (cases.empty()) return@on + + // Check if the last case was a mute, assumed it's a mute evade + val last = ( + try { + cases.last() + } catch (e: Exception) { + null + } + ) ?: return@on + + if (last.type == PunishmentType.MUTE && last.time != null) { + timeouts.send( + RequestCommand( + Timeout( + guildId = "${guild.id}", + userId = "${user.id}", + issuedAt = System.currentTimeMillis(), + expiresIn = System.currentTimeMillis() - last.time!!, + moderatorId = last.moderatorId.toString(), + reason = last.reason ?: "[Automod] User was mute evading, added role back.", + type = PunishmentType.UNBAN.key + ) + ) + ) + } + } + + on { + val guild = getGuild() + log.info("User ${user.tag} (${user.id}) has left guild ${guild.name} (${guild.id}) - checking if user was kicked!") + + val member = guild.members.firstOrNull { it.id == kord.selfId } ?: return@on + val perms = member.getPermissions() + + if (!perms.contains(Permission.ViewAuditLog)) return@on + + // We found an audit log! Let's add it to the mod log! + val auditLog = getAuditLogEntriesOf(kord, member, guild.id, user.id, AuditLogEvent.MemberKick) ?: return@on + val moderator = guild.getMember(auditLog.userId) + + punishments.apply( + MemberLike(user.asMemberOrNull(guild.id), guild, user.id), + moderator, + PunishmentType.KICK + ) { + reason = auditLog.reason.value ?: "[Automod] User was kicked with no reason." + } + } + + on { + val guild = getGuild() + val settings = asyncTransaction { + GuildSettingsEntity.findById(guild.id.value.toLong())!! + } + + val automodSettings = asyncTransaction { + AutomodEntity.findById(guild.id.value.toLong())!! + } + + // Check if we cannot retrieve the old metadata + if (old == null) return@on + + // Check if their nickname was changed + if (old!!.nickname != null && member.nickname != old!!.nickname) { + // If the automod dehoisting feature is disabled, let's not do anything! + if (!automodSettings.dehoisting) return@on + + // Run the automod thingy + val ret = Container.execute(this) + if (ret) return@on + } + + // Check if the user is a bot + val user = member.asUser() + if (user.isBot) return@on + + // Check if the muted role exists in the database + if (settings.mutedRoleId == null) return@on + + // Check if the muted role was deleted, so we can act on it + // to delete it. + val mutedRole = guild.roles.firstOrNull { it.id.value.toLong() == settings.mutedRoleId } + if (mutedRole == null) { + asyncTransaction { + GuildSettings.update({ + GuildSettings.id eq guild.id.value.toLong() + }) { + it[mutedRoleId] = null + } + } + + return@on + } + + // Check if they were unmuted + if (!member.roles.contains(mutedRole) && old!!.roles.contains(mutedRole)) { + val self = guild.members.firstOrNull { it.id == kord.selfId } ?: return@on + val entry = getAuditLogEntriesOf( + kord, + self, + guild.id, + user.id, + AuditLogEvent.MemberRoleUpdate + ) ?: return@on + + val moderator = guild.getMember(entry.userId) + punishments.apply( + MemberLike(user.asMemberOrNull(guild.id), guild, user.id), + moderator, + PunishmentType.UNMUTE + ) { + reason = entry.reason.value ?: "[Automod] User was unmuted with no reason." + } + } + + if (member.roles.contains(mutedRole) && !old!!.roles.contains(mutedRole)) { + val self = guild.members.firstOrNull { it.id == kord.selfId } ?: return@on + val entry = getAuditLogEntriesOf( + kord, + self, + guild.id, + user.id, + AuditLogEvent.MemberRoleUpdate + ) ?: return@on + + val moderator = guild.getMember(entry.userId) + punishments.apply( + MemberLike(user.asMemberOrNull(guild.id), guild, user.id), + moderator, + PunishmentType.MUTE + ) { + reason = entry.reason.value ?: "[Automod] User was muted with no reason." + } + } + } + + log.info("✔ Registered all guild member events!") +} diff --git a/src/entities/BlacklistEntity.ts b/bot/core/src/main/kotlin/sh/nino/discord/core/listeners/UserListener.kt similarity index 73% rename from src/entities/BlacklistEntity.ts rename to bot/core/src/main/kotlin/sh/nino/discord/core/listeners/UserListener.kt index b9c179d9..d3b1a9af 100644 --- a/src/entities/BlacklistEntity.ts +++ b/bot/core/src/main/kotlin/sh/nino/discord/core/listeners/UserListener.kt @@ -1,5 +1,5 @@ -/** - * Copyright (c) 2019-2021 Nino +/* + * Copyright (c) 2019-2022 Nino * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -20,24 +20,18 @@ * SOFTWARE. */ -import { Entity, PrimaryColumn, Column } from 'typeorm'; +package sh.nino.discord.core.listeners -export enum BlacklistType { - Guild, - User, -} - -@Entity({ name: 'blacklists' }) -export default class BlacklistEntity { - @Column({ nullable: true }) - public reason?: string; +import dev.kord.core.Kord +import dev.kord.core.event.user.UserUpdateEvent +import dev.kord.core.on +import org.slf4j.LoggerFactory - @Column() - public issuer!: string; +fun Kord.applyUserEvents() { + val log = LoggerFactory.getLogger("sh.nino.discord.core.listeners.UserListenerKt") - @Column({ type: 'enum', enum: BlacklistType }) - public type!: BlacklistType; + on { + } - @PrimaryColumn() - public id!: string; + log.info("✔ Registered all user update events!") } diff --git a/bot/core/src/main/kotlin/sh/nino/discord/core/listeners/VoiceStateListener.kt b/bot/core/src/main/kotlin/sh/nino/discord/core/listeners/VoiceStateListener.kt new file mode 100644 index 00000000..5b9ca646 --- /dev/null +++ b/bot/core/src/main/kotlin/sh/nino/discord/core/listeners/VoiceStateListener.kt @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2019-2022 Nino + * + * 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. + */ + +package sh.nino.discord.core.listeners + +import dev.kord.core.Kord +import dev.kord.core.event.user.VoiceStateUpdateEvent +import dev.kord.core.on +import org.slf4j.LoggerFactory + +fun Kord.applyVoiceStateEvents() { + val log = LoggerFactory.getLogger("sh.nino.discord.core.listeners.VoiceStateListenerKt") + + on { + // work on implementation details here :lurk: + } + + log.info("✔ Registered all guild voice state events!") +} diff --git a/bot/core/src/main/kotlin/sh/nino/discord/core/localization/Locale.kt b/bot/core/src/main/kotlin/sh/nino/discord/core/localization/Locale.kt new file mode 100644 index 00000000..377b5f7a --- /dev/null +++ b/bot/core/src/main/kotlin/sh/nino/discord/core/localization/Locale.kt @@ -0,0 +1,75 @@ +/* + * Copyright (c) 2019-2022 Nino + * + * 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. + */ + +package sh.nino.discord.core.localization + +import kotlinx.serialization.Serializable +import kotlinx.serialization.json.Json +import org.koin.core.context.GlobalContext +import sh.nino.discord.common.StringOrArray +import sh.nino.discord.common.extensions.retrieve +import java.io.File +import java.util.regex.Pattern + +/** + * Represents the metadata of a [Locale] object. + * + * @param contributors A list of contributors by their ID that contributed to this language + * @param translator The translator's ID that translated this language. + * @param aliases A list of aliases when setting this [Locale]. + * @param code The IANA code that is used for this [Locale]. + * @param flag The flag emoji (i.e, `:flag_us:`) for presentation purposes. + * @param name The locale's full name. + */ +@Serializable +data class LocalizationMeta( + val contributors: List = listOf(), + val translator: String, + val aliases: List = listOf(), + val code: String, + val flag: String, + val name: String +) + +private val KEY_REGEX = Pattern.compile("[\$]\\{([\\w\\.]+)\\}").toRegex() + +@Serializable +data class Locale( + val meta: LocalizationMeta, + val strings: Map +) { + companion object { + fun fromFile(file: File): Locale { + val json = GlobalContext.retrieve() + return json.decodeFromString(serializer(), file.readText()) + } + } + + fun translate(key: String, args: Map = mapOf()): String { + val format = strings[key] ?: error("Key \"$key\" was not found.") + val stringsToTranslate = format.asListOrNull?.joinToString("\n") ?: format.asString + + return KEY_REGEX.replace(stringsToTranslate, transform = { + args[it.groups[1]!!.value].toString() + }) + } +} diff --git a/bot/core/src/main/kotlin/sh/nino/discord/core/localization/LocalizationManager.kt b/bot/core/src/main/kotlin/sh/nino/discord/core/localization/LocalizationManager.kt new file mode 100644 index 00000000..1cccda6f --- /dev/null +++ b/bot/core/src/main/kotlin/sh/nino/discord/core/localization/LocalizationManager.kt @@ -0,0 +1,81 @@ +/* + * Copyright (c) 2019-2022 Nino + * + * 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. + */ + +package sh.nino.discord.core.localization + +import gay.floof.utils.slf4j.logging +import sh.nino.discord.common.data.Config +import java.io.File + +class LocalizationManager(config: Config) { + private val localeDirectory = File("./locales") + private lateinit var defaultLocale: Locale + private val logger by logging() + + val locales: Map + init { + logger.info("Finding locales in ${localeDirectory.path}...") + if (!localeDirectory.exists()) + throw IllegalStateException("Locale path must be available in ${localeDirectory.path}!") + + val files = localeDirectory.listFiles { _, s -> s.endsWith(".json") } ?: arrayOf() + val foundLocales = mutableMapOf() + + for (file in files) { + val locale = Locale.fromFile(file) + + logger.info("Found locale ${locale.meta.code} by ${locale.meta.translator}!") + foundLocales[locale.meta.code] = locale + + if (locale.meta.code == config.defaultLocale) { + logger.info("Found default locale ${config.defaultLocale}!") + defaultLocale = locale + } + } + + if (!this::defaultLocale.isInitialized) { + logger.warn("No default locale was found, setting to English (US)!") + defaultLocale = foundLocales["en_US"]!! + } + + locales = foundLocales.toMap() + } + + fun getLocale(guild: String, user: String): Locale { + // This should never happen, but it could happen. + if (!locales.containsKey(guild) || !locales.containsKey(user)) return defaultLocale + + // If both parties use the default locale, return it. + if (guild == defaultLocale.meta.code && user == defaultLocale.meta.code) return defaultLocale + + // Users have more priority than guilds, so let's check if the guild locale + // is the default and the user's locale is completely different + if (user != defaultLocale.meta.code && guild == defaultLocale.meta.code) return locales[user]!! + + // If the user's locale is not the guild's locale, return it, + // so it can be translated properly. + if (guild !== defaultLocale.meta.code && user == defaultLocale.meta.code) return locales[guild]!! + + // We should never be here, but here we are. + error("Illegal unknown value (locale: guild->$guild;user->$user)") + } +} diff --git a/bot/core/src/main/kotlin/sh/nino/discord/core/messaging/PaginationEmbed.kt b/bot/core/src/main/kotlin/sh/nino/discord/core/messaging/PaginationEmbed.kt new file mode 100644 index 00000000..8cc51944 --- /dev/null +++ b/bot/core/src/main/kotlin/sh/nino/discord/core/messaging/PaginationEmbed.kt @@ -0,0 +1,400 @@ +/* + * Copyright (c) 2019-2022 Nino + * + * 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. + */ + +package sh.nino.discord.core.messaging + +import dev.kord.common.entity.ButtonStyle +import dev.kord.common.entity.ComponentType +import dev.kord.common.entity.DiscordPartialEmoji +import dev.kord.common.entity.InteractionType +import dev.kord.core.Kord +import dev.kord.core.behavior.channel.createMessage +import dev.kord.core.behavior.edit +import dev.kord.core.entity.Message +import dev.kord.core.entity.User +import dev.kord.core.entity.channel.TextChannel +import dev.kord.core.event.interaction.ComponentInteractionCreateEvent +import dev.kord.core.event.interaction.InteractionCreateEvent +import dev.kord.core.on +import dev.kord.rest.builder.message.EmbedBuilder +import dev.kord.rest.builder.message.create.actionRow +import dev.kord.rest.builder.message.modify.actionRow +import kotlinx.coroutines.Job +import kotlinx.coroutines.cancelAndJoin +import org.koin.core.context.GlobalContext +import sh.nino.discord.common.extensions.* +import sh.nino.discord.core.AutoSuspendCloseable +import java.util.* + +/** + * Represents an embed that can be paginated in a specific amount of time before + * the underlying [Job][kotlinx.coroutines.Job] is closed off and no more events + * will be coming in. + */ +class PaginationEmbed( + private val channel: TextChannel, + private val invoker: User, + private var embeds: List, +): AutoSuspendCloseable { + companion object { + val REACTIONS = mapOf( + "stop" to "\u23F9\uFE0F", + "right" to "\u27A1\uFE0F", + "left" to "\u2B05\uFE0F", + "first" to "\u23EE\uFE0F", + "last" to "\u23ED\uFE0F" + ) + } + + private val uniqueId = UUID.randomUUID().toString() + + // If this [PaginationEmbed] is listening to events. + private val listening: Boolean + get() = if (!this::job.isInitialized) { + false + } else { + this.job.isActive + } + + // Returns the [Message] that this [PaginationEmbed] has control over. + private lateinit var message: Message + + // Returns the current index in this [PaginationEmbed] tree. + private var currentIndex = 0 + + // Returns the coroutine job that this [PaginationEmbed] has control over. + private lateinit var job: Job + + override suspend fun close() { + if (!this.listening) throw IllegalStateException("This PaginationEmbed is already closed.") + + message.delete("[Pagination Embed for ${invoker.tag}] Embed was destroyed.") + job.cancelAndJoin() + } + + suspend fun create() { + if (this::job.isInitialized) throw IllegalStateException("PaginationEmbed is already running") + + message = channel.createMessage { + embeds += this@PaginationEmbed.embeds[currentIndex].apply { + footer { + text = "Page ${currentIndex + 1}/${this@PaginationEmbed.embeds.size}" + } + } + + actionRow { + interactionButton(ButtonStyle.Secondary, "nino:selection:$uniqueId:first") { + emoji = DiscordPartialEmoji( + id = null, + name = REACTIONS["first"]!! + ) + + disabled = currentIndex == 0 + } + + interactionButton(ButtonStyle.Secondary, "nino:selection:$uniqueId:left") { + emoji = DiscordPartialEmoji( + id = null, + name = REACTIONS["left"]!! + ) + } + + interactionButton(ButtonStyle.Secondary, "nino:selection:$uniqueId:stop") { + emoji = DiscordPartialEmoji( + id = null, + name = REACTIONS["stop"]!! + ) + } + + interactionButton(ButtonStyle.Secondary, "nino:selection:$uniqueId:right") { + emoji = DiscordPartialEmoji( + id = null, + name = REACTIONS["right"]!! + ) + } + + interactionButton(ButtonStyle.Secondary, "nino:selection:$uniqueId:last") { + emoji = DiscordPartialEmoji( + id = null, + name = REACTIONS["last"]!! + ) + + disabled = currentIndex == this@PaginationEmbed.embeds.size + } + } + } + + val kord = GlobalContext.retrieve() + job = kord.on { onInteractionReceive(this) } + } + + private suspend fun onInteractionReceive(event: InteractionCreateEvent) { + // do not do anything if the interaction type is not a component + if (event.interaction.type != InteractionType.Component) return + event as ComponentInteractionCreateEvent // cast it at compile time + + // Is it a button? If not, skip it. + if (event.interaction.componentType != ComponentType.Button) return + + // If the custom id doesn't start with `nino:selection:$uniqueId`, skip it. + if (!event.interaction.componentId.startsWith("nino:selection:$uniqueId")) return + + // Is the interaction member the user who invoked it? + // If not, do not do anything + if (event.interaction.data.member.value != null && event.interaction.data.member.value!!.userId != invoker.id) return + + event.interaction.acknowledgePublicDeferredMessageUpdate() + + // Get the action to use + when (event.interaction.componentId.split(":").last()) { + "stop" -> close() + "left" -> { + currentIndex -= 1 + if (currentIndex < 0) currentIndex = embeds.size - 1 + + message.edit { + this.embeds = mutableListOf( + this@PaginationEmbed.embeds[currentIndex].apply { + footer { + text = "Page ${currentIndex + 1}/${this@PaginationEmbed.embeds.size}" + } + } + ) + + actionRow { + interactionButton(ButtonStyle.Secondary, "nino:selection:$uniqueId:first") { + emoji = DiscordPartialEmoji( + id = null, + name = REACTIONS["first"]!! + ) + + disabled = currentIndex == 0 + } + + interactionButton(ButtonStyle.Secondary, "nino:selection:$uniqueId:left") { + emoji = DiscordPartialEmoji( + id = null, + name = REACTIONS["left"]!! + ) + } + + interactionButton(ButtonStyle.Secondary, "nino:selection:$uniqueId:stop") { + emoji = DiscordPartialEmoji( + id = null, + name = REACTIONS["stop"]!! + ) + } + + interactionButton(ButtonStyle.Secondary, "nino:selection:$uniqueId:right") { + emoji = DiscordPartialEmoji( + id = null, + name = REACTIONS["right"]!! + ) + } + + interactionButton(ButtonStyle.Secondary, "nino:selection:$uniqueId:last") { + emoji = DiscordPartialEmoji( + id = null, + name = REACTIONS["last"]!! + ) + + disabled = currentIndex == this@PaginationEmbed.embeds.size + } + } + } + } + + "right" -> { + currentIndex++ + if (currentIndex == embeds.size) currentIndex = 0 + + message.edit { + this.embeds = mutableListOf( + this@PaginationEmbed.embeds[currentIndex].apply { + footer { + text = "Page ${currentIndex + 1}/${this@PaginationEmbed.embeds.size}" + } + } + ) + + actionRow { + interactionButton(ButtonStyle.Secondary, "nino:selection:$uniqueId:first") { + emoji = DiscordPartialEmoji( + id = null, + name = REACTIONS["first"]!! + ) + + disabled = currentIndex == 0 + } + + interactionButton(ButtonStyle.Secondary, "nino:selection:$uniqueId:left") { + emoji = DiscordPartialEmoji( + id = null, + name = REACTIONS["left"]!! + ) + } + + interactionButton(ButtonStyle.Secondary, "nino:selection:$uniqueId:stop") { + emoji = DiscordPartialEmoji( + id = null, + name = REACTIONS["stop"]!! + ) + } + + interactionButton(ButtonStyle.Secondary, "nino:selection:$uniqueId:right") { + emoji = DiscordPartialEmoji( + id = null, + name = REACTIONS["right"]!! + ) + } + + interactionButton(ButtonStyle.Secondary, "nino:selection:$uniqueId:last") { + emoji = DiscordPartialEmoji( + id = null, + name = REACTIONS["last"]!! + ) + + disabled = currentIndex == this@PaginationEmbed.embeds.size + } + } + } + } + + "first" -> { + // We shouldn't get this if the currentIndex is zero since, + // it's automatically disabled if it is. But, this is just + // here to be safe and discord decides to commit a fucking woeme + if (currentIndex == 0) return + + currentIndex = 0 + message.edit { + this.embeds = mutableListOf( + this@PaginationEmbed.embeds[currentIndex].apply { + footer { + text = "Page ${currentIndex + 1}/${this@PaginationEmbed.embeds.size}" + } + } + ) + + actionRow { + interactionButton(ButtonStyle.Secondary, "nino:selection:$uniqueId:first") { + emoji = DiscordPartialEmoji( + id = null, + name = REACTIONS["first"]!! + ) + + disabled = currentIndex == 0 + } + + interactionButton(ButtonStyle.Secondary, "nino:selection:$uniqueId:left") { + emoji = DiscordPartialEmoji( + id = null, + name = REACTIONS["left"]!! + ) + } + + interactionButton(ButtonStyle.Secondary, "nino:selection:$uniqueId:stop") { + emoji = DiscordPartialEmoji( + id = null, + name = REACTIONS["stop"]!! + ) + } + + interactionButton(ButtonStyle.Secondary, "nino:selection:$uniqueId:right") { + emoji = DiscordPartialEmoji( + id = null, + name = REACTIONS["right"]!! + ) + } + + interactionButton(ButtonStyle.Secondary, "nino:selection:$uniqueId:last") { + emoji = DiscordPartialEmoji( + id = null, + name = REACTIONS["last"]!! + ) + + disabled = currentIndex == this@PaginationEmbed.embeds.size + } + } + } + } + + "last" -> { + // this is just here to be safe. + val lastIndex = embeds.size - 1 + if (currentIndex == lastIndex) return + + currentIndex = lastIndex + message.edit { + this.embeds = mutableListOf( + this@PaginationEmbed.embeds[currentIndex].apply { + footer { + text = "Page ${currentIndex + 1}/${this@PaginationEmbed.embeds.size}" + } + } + ) + + actionRow { + interactionButton(ButtonStyle.Secondary, "nino:selection:$uniqueId:first") { + emoji = DiscordPartialEmoji( + id = null, + name = REACTIONS["first"]!! + ) + + disabled = currentIndex == 0 + } + + interactionButton(ButtonStyle.Secondary, "nino:selection:$uniqueId:left") { + emoji = DiscordPartialEmoji( + id = null, + name = REACTIONS["left"]!! + ) + } + + interactionButton(ButtonStyle.Secondary, "nino:selection:$uniqueId:stop") { + emoji = DiscordPartialEmoji( + id = null, + name = REACTIONS["stop"]!! + ) + } + + interactionButton(ButtonStyle.Secondary, "nino:selection:$uniqueId:right") { + emoji = DiscordPartialEmoji( + id = null, + name = REACTIONS["right"]!! + ) + } + + interactionButton(ButtonStyle.Secondary, "nino:selection:$uniqueId:last") { + emoji = DiscordPartialEmoji( + id = null, + name = REACTIONS["last"]!! + ) + + disabled = currentIndex == this@PaginationEmbed.embeds.size + } + } + } + } + } + } +} diff --git a/bot/core/src/main/kotlin/sh/nino/discord/core/redis/RedisManager.kt b/bot/core/src/main/kotlin/sh/nino/discord/core/redis/RedisManager.kt new file mode 100644 index 00000000..e607e219 --- /dev/null +++ b/bot/core/src/main/kotlin/sh/nino/discord/core/redis/RedisManager.kt @@ -0,0 +1,149 @@ +/* + * Copyright (c) 2019-2022 Nino + * + * 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. + */ + +package sh.nino.discord.core.redis + +import gay.floof.utils.slf4j.logging +import io.lettuce.core.RedisClient +import io.lettuce.core.RedisURI +import io.lettuce.core.api.StatefulRedisConnection +import io.lettuce.core.api.async.RedisAsyncCommands +import kotlinx.coroutines.future.await +import org.apache.commons.lang3.time.StopWatch +import sh.nino.discord.common.data.Config +import sh.nino.discord.common.extensions.asMap +import kotlin.time.Duration +import kotlin.time.DurationUnit +import kotlin.time.toDuration + +data class RedisStats( + val serverStats: Map, + val stats: Map, + val ping: Duration +) + +class RedisManager(config: Config): AutoCloseable { + private lateinit var connection: StatefulRedisConnection + lateinit var commands: RedisAsyncCommands + private val logger by logging() + val client: RedisClient + + init { + logger.info("Creating Redis client...") + + val redisUri: RedisURI = if (config.redis.sentinels.isNotEmpty()) { + val builder = RedisURI.builder() + val sentinelRedisUri = RedisURI.builder() + .withSentinelMasterId(config.redis.master!!) + .withDatabase(config.redis.index) + + for (host in config.redis.sentinels) { + val (h, port) = host.split(":") + sentinelRedisUri.withSentinel(h, Integer.parseInt(port)) + } + + if (config.redis.password != null) + sentinelRedisUri.withPassword(config.redis.password!!.toCharArray()) + + builder + .withSentinel(sentinelRedisUri.build()) + .build() + } else { + val builder = RedisURI + .builder() + .withHost(config.redis.host) + .withPort(config.redis.port) + .withDatabase(config.redis.index) + + if (config.redis.password != null) + builder.withPassword(config.redis.password!!.toCharArray()) + + builder.build() + } + + client = RedisClient.create(redisUri) + } + + override fun close() { + // If the connection was never established, skip. + if (!::connection.isInitialized) return + + logger.warn("Closing Redis connection...") + connection.close() + client.shutdown() + } + + fun connect() { + // If it was already established, let's not skip. + if (::connection.isInitialized) return + + logger.info("Creating connection...") + connection = client.connect() + commands = connection.async() + + logger.info("Connected!") + } + + suspend fun getPing(): Duration { + // If the connection wasn't established, + // let's return Duration.ZERO + if (::connection.isInitialized) return Duration.ZERO + + val watch = StopWatch.createStarted() + commands.ping().await() + watch.stop() + + return watch.time.toDuration(DurationUnit.MILLISECONDS) + } + + suspend fun getStats(): RedisStats { + val ping = getPing() + + // get stats from connection + val serverStats = commands.info("server").await() + val stats = commands.info("stats").await() + + val mappedServerStats = serverStats!! + .split("\r\n?".toRegex()) + .drop(1) + .dropLast(1) + .map { + val (key, value) = it.split(":") + key to value + }.asMap() + + val mappedStats = stats!! + .split("\r\n?".toRegex()) + .drop(1) + .dropLast(1) + .map { + val (key, value) = it.split(":") + key to value + }.asMap() + + return RedisStats( + mappedServerStats, + mappedStats, + ping + ) + } +} diff --git a/scripts/util/getCaseType.js b/bot/core/src/main/kotlin/sh/nino/discord/core/timers/TimerJob.kt similarity index 60% rename from scripts/util/getCaseType.js rename to bot/core/src/main/kotlin/sh/nino/discord/core/timers/TimerJob.kt index 99188470..f5bc8573 100644 --- a/scripts/util/getCaseType.js +++ b/bot/core/src/main/kotlin/sh/nino/discord/core/timers/TimerJob.kt @@ -1,5 +1,5 @@ -/** - * Copyright (c) 2019-2021 Nino +/* + * Copyright (c) 2019-2022 Nino * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -20,31 +20,27 @@ * SOFTWARE. */ -const { PunishmentType } = require('../../build/entities/PunishmentsEntity'); +package sh.nino.discord.core.timers + +import kotlinx.coroutines.Job /** - * Determines the type from v0.x to v1.x - * @param {'warning remove' | 'warning add' | 'unmute' | 'kick' | 'mute' | 'ban'} type The type to serialize - * @returns {string} The punishment type + * Represents a base instance of a job that can be timed per basis. This abstract class + * takes in a [name], which is... self-explanatory and the [interval] to tick to call + * the [execute] function. */ -module.exports = (type) => { - switch (type) { - case 'warning remove': - return PunishmentType.WarningRemoved; - - case 'warning add': - return PunishmentType.WarningAdded; - - case 'unmute': - return PunishmentType.Unmute; - - case 'kick': - return PunishmentType.Kick; - - case 'mute': - return PunishmentType.Mute; +abstract class TimerJob( + val name: String, + val interval: Int +) { + /** + * Represents the current coroutine [job][Job] that is being executed. This + * can be `null` if the job was never scheduled or was unscheduled. + */ + var coroutineJob: Job? = null - case 'ban': - return PunishmentType.Ban; - } -}; + /** + * The executor function to call every tick of the [interval] specified. + */ + abstract suspend fun execute() +} diff --git a/src/controllers/UserSettingsController.ts b/bot/core/src/main/kotlin/sh/nino/discord/core/timers/TimerManager.kt similarity index 52% rename from src/controllers/UserSettingsController.ts rename to bot/core/src/main/kotlin/sh/nino/discord/core/timers/TimerManager.kt index c80b9d90..27b7a000 100644 --- a/src/controllers/UserSettingsController.ts +++ b/bot/core/src/main/kotlin/sh/nino/discord/core/timers/TimerManager.kt @@ -1,5 +1,5 @@ -/** - * Copyright (c) 2019-2021 Nino +/* + * Copyright (c) 2019-2022 Nino * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -20,38 +20,37 @@ * SOFTWARE. */ -import { QueryDeepPartialEntity } from 'typeorm/query-builder/QueryPartialEntity'; -import type Database from '../components/Database'; -import UserEntity from '../entities/UserEntity'; +package sh.nino.discord.core.timers + +import gay.floof.utils.slf4j.logging +import io.ktor.utils.io.* +import kotlin.time.Duration.Companion.seconds -export default class UserSettingsController { - constructor(private database: Database) {} +/** + * The timer manager is the main manager to schedule and unschedule all the timers + * that were registered. + */ +class TimerManager { + private val scope = TimerScope() + private val logger by logging() + private val jobs: MutableList = mutableListOf() - get repository() { - return this.database.connection.getRepository(UserEntity); - } + fun schedule(job: TimerJob) { + logger.info("Scheduled job ${job.name} for every ${job.interval.seconds} seconds!") + val coroutineJob = scope.launch(job) - async get(id: string) { - const settings = await this.repository.findOne({ id }); - if (settings === undefined) { - const entry = new UserEntity(); - entry.prefixes = []; - entry.language = 'en_US'; - entry.id = id; + job.coroutineJob = coroutineJob + coroutineJob.start() - await this.repository.save(entry); - return entry; + jobs.add(job) } - return settings; - } - - update(userID: string, values: QueryDeepPartialEntity) { - return this.database.connection - .createQueryBuilder() - .update(UserEntity) - .set(values) - .where('user_id = :id', { id: userID }) - .execute(); - } + fun bulkSchedule(vararg jobs: TimerJob) { + for (job in jobs) schedule(job) + } + + fun unschedule() { + logger.warn("Unscheduled all timer jobs...") + for (job in jobs) job.coroutineJob!!.cancel(CancellationException("Unscheduled by program")) + } } diff --git a/bot/core/src/main/kotlin/sh/nino/discord/core/timers/TimerScope.kt b/bot/core/src/main/kotlin/sh/nino/discord/core/timers/TimerScope.kt new file mode 100644 index 00000000..e209767c --- /dev/null +++ b/bot/core/src/main/kotlin/sh/nino/discord/core/timers/TimerScope.kt @@ -0,0 +1,55 @@ +/* + * Copyright (c) 2019-2022 Nino + * + * 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. + */ + +package sh.nino.discord.core.timers + +import gay.floof.utils.slf4j.logging +import kotlinx.coroutines.* +import sh.nino.discord.core.NinoThreadFactory +import java.util.concurrent.ExecutorService +import java.util.concurrent.Executors +import kotlin.coroutines.CoroutineContext + +/** + * The timer scope is a coroutine scope that uses a single-threaded executor pool, + * that it can be easily used with kotlinx.coroutines! + */ +internal class TimerScope: CoroutineScope { + private val executorPool: ExecutorService = Executors.newSingleThreadExecutor(NinoThreadFactory) + private val logger by logging() + + override val coroutineContext: CoroutineContext = SupervisorJob() + executorPool.asCoroutineDispatcher() + fun launch(job: TimerJob): Job { + return launch(start = CoroutineStart.LAZY) { + delay(job.interval.toLong()) + while (isActive) { + try { + job.execute() + } catch (e: Exception) { + logger.error("Unable to run job '${job.name}':", e) + } + + delay(job.interval.toLong()) + } + } + } +} diff --git a/src/@types/json.d.ts b/bot/database/build.gradle.kts similarity index 88% rename from src/@types/json.d.ts rename to bot/database/build.gradle.kts index 655c723b..042bee36 100644 --- a/src/@types/json.d.ts +++ b/bot/database/build.gradle.kts @@ -1,5 +1,5 @@ /** - * Copyright (c) 2019-2021 Nino + * Copyright (c) 2019-2022 Nino * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -20,7 +20,6 @@ * SOFTWARE. */ -/** */ -interface JSON { - parse(content: string, reviver?: (this: any, key: string, value: any) => any): T; +plugins { + `nino-module` } diff --git a/bot/database/src/main/kotlin/sh/nino/discord/database/AsyncTransaction.kt b/bot/database/src/main/kotlin/sh/nino/discord/database/AsyncTransaction.kt new file mode 100644 index 00000000..ef522bf1 --- /dev/null +++ b/bot/database/src/main/kotlin/sh/nino/discord/database/AsyncTransaction.kt @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2019-2022 Nino + * + * 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. + */ + +package sh.nino.discord.database + +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.DelicateCoroutinesApi +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.future.await +import kotlinx.coroutines.future.future +import org.jetbrains.exposed.sql.Transaction +import org.jetbrains.exposed.sql.transactions.transaction + +class AsyncTransaction(private val block: Transaction.() -> T) { + @OptIn(DelicateCoroutinesApi::class) + suspend fun execute(): T = CoroutineScope(GlobalScope.coroutineContext).future { + transaction { block() } + }.await() +} + +suspend fun asyncTransaction(block: Transaction.() -> T): T = AsyncTransaction(block).execute() diff --git a/src/commands/core/SourceCommand.ts b/bot/database/src/main/kotlin/sh/nino/discord/database/SnowflakeTable.kt similarity index 72% rename from src/commands/core/SourceCommand.ts rename to bot/database/src/main/kotlin/sh/nino/discord/database/SnowflakeTable.kt index b6476c5e..c97b027d 100644 --- a/src/commands/core/SourceCommand.ts +++ b/bot/database/src/main/kotlin/sh/nino/discord/database/SnowflakeTable.kt @@ -1,5 +1,5 @@ -/** - * Copyright (c) 2019-2021 Nino +/* + * Copyright (c) 2019-2022 Nino * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -20,18 +20,13 @@ * SOFTWARE. */ -import { Command, CommandMessage } from '../../structures'; +package sh.nino.discord.database -export default class SourceCommand extends Command { - constructor() { - super({ - description: 'descriptions.source', - aliases: ['git', 'github', 'sauce', 'oss'], - name: 'source', - }); - } +import org.jetbrains.exposed.dao.id.EntityID +import org.jetbrains.exposed.dao.id.IdTable +import org.jetbrains.exposed.sql.Column - run(msg: CommandMessage) { - return msg.reply(':eyes: https://github.com/NinoDiscord/Nino'); - } +open class SnowflakeTable(name: String = ""): IdTable(name) { + override val id: Column> = long("id").entityId() + override val primaryKey: PrimaryKey = PrimaryKey(id, name = "PK_$name") } diff --git a/bot/database/src/main/kotlin/sh/nino/discord/database/columns/ArrayColumnType.kt b/bot/database/src/main/kotlin/sh/nino/discord/database/columns/ArrayColumnType.kt new file mode 100644 index 00000000..3cb62677 --- /dev/null +++ b/bot/database/src/main/kotlin/sh/nino/discord/database/columns/ArrayColumnType.kt @@ -0,0 +1,110 @@ +/* + * Copyright (c) 2019-2022 Nino + * + * 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. + */ + +package sh.nino.discord.database.columns + +import org.jetbrains.exposed.sql.* +import org.jetbrains.exposed.sql.statements.jdbc.JdbcConnectionImpl +import org.jetbrains.exposed.sql.transactions.TransactionManager +import org.postgresql.jdbc.PgArray + +fun Table.array(name: String, type: ColumnType): Column> = registerColumn(name, ArrayColumnType(type)) + +class ArrayColumnType(private val type: ColumnType): ColumnType() { + override fun sqlType(): String = "${type.sqlType()} ARRAY" + + override fun valueToDB(value: Any?): Any? = + if (value is Array<*>) { + val columnType = type.sqlType().split("(")[0] + val connection = (TransactionManager.current().connection as JdbcConnectionImpl).connection + connection.createArrayOf(columnType, value) + } else { + super.valueToDB(value) + } + + @Suppress("UNCHECKED_CAST") + override fun valueFromDB(value: Any): Array<*> { + if (value is PgArray) { + return if (type.sqlType().endsWith("Enum")) { + (value.array as Array<*>).filterNotNull().map { + type.valueFromDB(it) + }.toTypedArray() + } else { + value.array as Array<*> + } + } + + if (value is java.sql.Array) { + return if (type.sqlType().endsWith("Enum")) { + (value.array as Array<*>).filterNotNull().map { + type.valueFromDB(it) + }.toTypedArray() + } else { + value.array as Array<*> + } + } + + if (value is Array<*>) return value + + error("Unable to return an Array from a non-array value. ($value, ${value::class})") + } + + override fun notNullValueToDB(value: Any): Any { + if (value is Array<*>) { + if (value.isEmpty()) return "'{}'" + + val columnType = type.sqlType().split("(")[0] + val connection = (TransactionManager.current().connection as JdbcConnectionImpl).connection + return connection.createArrayOf(columnType, value) + } else { + return super.notNullValueToDB(value) + } + } +} + +private class ContainsOp(expr1: Expression<*>, expr2: Expression<*>): ComparisonOp(expr1, expr2, "@>") +infix fun ExpressionWithColumnType.contains(array: Array): Op = ContainsOp(this, QueryParameter(array, columnType)) + +class AnyOp(val expr1: Expression<*>, val expr2: Expression<*>): Op() { + override fun toQueryBuilder(queryBuilder: QueryBuilder) { + if (expr2 is OrOp) { + queryBuilder.append("(").append(expr2).append(")") + } else { + queryBuilder.append(expr2) + } + + queryBuilder.append(" = ANY (") + if (expr1 is OrOp) { + queryBuilder.append("(").append(expr1).append(")") + } else { + queryBuilder.append(expr1) + } + + queryBuilder.append(")") + } +} + +infix fun ExpressionWithColumnType.any(v: S): Op = if (v == null) { + IsNullOp(this) +} else { + AnyOp(this, QueryParameter(v, columnType)) +} diff --git a/bot/database/src/main/kotlin/sh/nino/discord/database/columns/CustomEnumerationColumn.kt b/bot/database/src/main/kotlin/sh/nino/discord/database/columns/CustomEnumerationColumn.kt new file mode 100644 index 00000000..e5e7c665 --- /dev/null +++ b/bot/database/src/main/kotlin/sh/nino/discord/database/columns/CustomEnumerationColumn.kt @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2019-2022 Nino + * + * 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. + */ + +package sh.nino.discord.database.columns + +import org.jetbrains.exposed.sql.ColumnType +import kotlin.reflect.full.isSubclassOf + +@Suppress("UNCHECKED_CAST") +class CustomEnumerationColumn( + private val name: String, + private val sql: String? = null, + private val fromDb: (Any) -> T, + private val toDb: (T) -> Any +): ColumnType() { + override fun sqlType(): String = sql ?: error("Column $name should exists in database ") + override fun valueFromDB(value: Any): T = if (value::class.isSubclassOf(Enum::class)) value as T else fromDb(value) + override fun notNullValueToDB(value: Any): Any = toDb(value as T) +} diff --git a/src/entities/GuildEntity.ts b/bot/database/src/main/kotlin/sh/nino/discord/database/createEnums.kt similarity index 65% rename from src/entities/GuildEntity.ts rename to bot/database/src/main/kotlin/sh/nino/discord/database/createEnums.kt index 7140effe..63998ba8 100644 --- a/src/entities/GuildEntity.ts +++ b/bot/database/src/main/kotlin/sh/nino/discord/database/createEnums.kt @@ -1,5 +1,5 @@ -/** - * Copyright (c) 2019-2021 Nino +/* + * Copyright (c) 2019-2022 Nino * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -20,22 +20,21 @@ * SOFTWARE. */ -import { Entity, Column, PrimaryColumn } from 'typeorm'; +package sh.nino.discord.database -@Entity({ name: 'guilds' }) -export default class GuildEntity { - @Column({ default: null, nullable: true, name: 'modlog_channel_id' }) - public modlogChannelID?: string; +suspend fun createPgEnums(mapped: Map>) { + for ((typeName, meta) in mapped) { + val exists = asyncTransaction { + exec("SELECT * FROM pg_type WHERE typname='${typeName.lowercase()}';") { + it.next() + } + } - @Column({ default: null, nullable: true, name: 'muted_role_id' }) - public mutedRoleID?: string; - - @Column({ array: true, type: 'text' }) - public prefixes!: string[]; - - @Column({ default: 'en_US' }) - public language!: string; - - @PrimaryColumn({ name: 'guild_id' }) - public guildID!: string; + if (exists != null && !exists) { + asyncTransaction { + val enumKeys = meta.joinToString(", ") { "'$it'" } + exec("CREATE TYPE $typeName AS ENUM($enumKeys)") + } + } + } } diff --git a/bot/database/src/main/kotlin/sh/nino/discord/database/tables/Automod.kt b/bot/database/src/main/kotlin/sh/nino/discord/database/tables/Automod.kt new file mode 100644 index 00000000..39fba876 --- /dev/null +++ b/bot/database/src/main/kotlin/sh/nino/discord/database/tables/Automod.kt @@ -0,0 +1,71 @@ +/* + * Copyright (c) 2019-2022 Nino + * + * 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. + */ + +package sh.nino.discord.database.tables + +import org.jetbrains.exposed.dao.LongEntity +import org.jetbrains.exposed.dao.LongEntityClass +import org.jetbrains.exposed.dao.id.EntityID +import org.jetbrains.exposed.sql.LongColumnType +import org.jetbrains.exposed.sql.TextColumnType +import sh.nino.discord.database.SnowflakeTable +import sh.nino.discord.database.columns.array + +object AutomodTable: SnowflakeTable("automod") { + val accountAgeDayThreshold = integer("account_age_days_threshold").default(4) + val mentionsThreshold = integer("mention_threshold").default(4) + val blacklistedWords = array("blacklisted_words", TextColumnType()).default(arrayOf()) + val omittedChannels = array("omitted_channels", LongColumnType()).default(arrayOf()) + val omittedUsers = array("omitted_users", LongColumnType()).default(arrayOf()) + val messageLinks = bool("message_links").default(false) + val accountAge = bool("account_age").default(false) + val dehoisting = bool("dehoisting").default(false) + val shortlinks = bool("shortlinks").default(false) + val blacklist = bool("blacklist").default(false) + val toxicity = bool("toxicity").default(false) + val phishing = bool("phishing").default(false) + val mentions = bool("mentions").default(false) + val invites = bool("invites").default(false) + val spam = bool("spam").default(false) + val raid = bool("raid").default(false) +} + +class AutomodEntity(id: EntityID): LongEntity(id) { + companion object: LongEntityClass(AutomodTable) + + var accountAgeDayThreshold by AutomodTable.accountAgeDayThreshold + var mentionThreshold by AutomodTable.mentionsThreshold + var blacklistedWords by AutomodTable.blacklistedWords + var omittedChannels by AutomodTable.omittedChannels + var omittedUsers by AutomodTable.omittedUsers + var messageLinks by AutomodTable.messageLinks + val accountAge by AutomodTable.accountAge + var dehoisting by AutomodTable.dehoisting + var shortlinks by AutomodTable.shortlinks + var blacklist by AutomodTable.blacklist + var toxicity by AutomodTable.toxicity + var phishing by AutomodTable.phishing + var mentions by AutomodTable.mentions + var invites by AutomodTable.invites + var spam by AutomodTable.spam + var raid by AutomodTable.raid +} diff --git a/bot/database/src/main/kotlin/sh/nino/discord/database/tables/Cases.kt b/bot/database/src/main/kotlin/sh/nino/discord/database/tables/Cases.kt new file mode 100644 index 00000000..dfc01dc2 --- /dev/null +++ b/bot/database/src/main/kotlin/sh/nino/discord/database/tables/Cases.kt @@ -0,0 +1,137 @@ +/* + * Copyright (c) 2019-2022 Nino + * + * 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. + */ + +package sh.nino.discord.database.tables + +import dev.kord.common.entity.* +import kotlinx.datetime.Clock +import kotlinx.datetime.TimeZone +import kotlinx.datetime.toLocalDateTime +import org.jetbrains.exposed.dao.LongEntity +import org.jetbrains.exposed.dao.LongEntityClass +import org.jetbrains.exposed.dao.id.EntityID +import org.jetbrains.exposed.sql.TextColumnType +import org.jetbrains.exposed.sql.kotlin.datetime.datetime +import sh.nino.discord.database.SnowflakeTable +import sh.nino.discord.database.columns.array + +enum class PunishmentType(val key: String) { + THREAD_MESSAGES_REMOVED("thread message removed"), + THREAD_MESSAGES_ADDED("thread message added"), + WARNING_REMOVED("warning removed"), + VOICE_UNDEAFEN("voice undeafen"), + WARNING_ADDED("warning added"), + VOICE_DEAFEN("voice deafened"), + VOICE_UNMUTE("voice unmute"), + VOICE_MUTE("voice mute"), + ROLE_REMOVE("role remove"), + ROLE_ADD("role add"), + UNMUTE("unmute"), + UNBAN("unban"), + MUTE("mute"), + KICK("kick"), + BAN("ban"); + + companion object { + operator fun get(key: String): PunishmentType = values().find { it.key == key } ?: error(key) + } +} + +val PunishmentType.asEmoji: String + get() = when (this) { + PunishmentType.BAN -> "\uD83D\uDD28" + PunishmentType.KICK -> "\uD83D\uDC62" + PunishmentType.MUTE -> "\uD83D\uDD07" + PunishmentType.UNBAN -> "\uD83D\uDC64" + PunishmentType.UNMUTE -> "\uD83D\uDCE2" + PunishmentType.VOICE_MUTE -> "\uD83D\uDD07" + PunishmentType.VOICE_UNMUTE -> "\uD83D\uDCE2" + PunishmentType.VOICE_DEAFEN -> "\uD83D\uDD07" + PunishmentType.VOICE_UNDEAFEN -> "\uD83D\uDCE2" + PunishmentType.THREAD_MESSAGES_ADDED -> "\uD83E\uDDF5" + PunishmentType.THREAD_MESSAGES_REMOVED -> "\uD83E\uDDF5" + PunishmentType.ROLE_ADD -> "" + PunishmentType.ROLE_REMOVE -> "" + else -> error("Unknown punishment type: $this") + } + +val PunishmentType.permissions: Permissions + get() = when (this) { + PunishmentType.MUTE, PunishmentType.UNMUTE -> Permissions { + +Permission.ManageRoles + } + + PunishmentType.VOICE_UNDEAFEN, PunishmentType.VOICE_DEAFEN -> Permissions { + +Permission.DeafenMembers + } + + PunishmentType.VOICE_MUTE, PunishmentType.VOICE_UNMUTE -> Permissions { + +Permission.MuteMembers + } + + PunishmentType.UNBAN, PunishmentType.BAN -> Permissions { + +Permission.BanMembers + } + + PunishmentType.KICK -> Permissions { + +Permission.KickMembers + } + + else -> Permissions() + } + +object GuildCases: SnowflakeTable("guild_cases") { + val attachments = array("attachments", TextColumnType()).default(arrayOf()) + val moderatorId = long("moderator_id") + val messageId = long("message_id").nullable() + val createdAt = datetime("created_at").default(Clock.System.now().toLocalDateTime(TimeZone.currentSystemDefault())) + val updatedAt = datetime("updated_at").default(Clock.System.now().toLocalDateTime(TimeZone.currentSystemDefault())) + val victimId = long("victim_id") + val reason = text("reason").nullable() + val index = integer("index").autoIncrement() + val soft = bool("soft").default(false) + val time = long("time").nullable().default(null) + val type = customEnumeration( + "type", + "PunishmentTypeEnum", + { value -> PunishmentType[value as String] }, + { toDb -> toDb.key } + ) + + override val primaryKey: PrimaryKey = PrimaryKey(id, index, name = "PK_GuildCases_ID") +} + +class GuildCasesEntity(id: EntityID): LongEntity(id) { + companion object: LongEntityClass(GuildCases) + + var attachments by GuildCases.attachments + var moderatorId by GuildCases.moderatorId + var messageId by GuildCases.messageId + var createdAt by GuildCases.createdAt + var updatedAt by GuildCases.updatedAt + var victimId by GuildCases.victimId + var reason by GuildCases.reason + var index by GuildCases.index + var type by GuildCases.type + var soft by GuildCases.soft + var time by GuildCases.time +} diff --git a/bot/database/src/main/kotlin/sh/nino/discord/database/tables/GlobalBans.kt b/bot/database/src/main/kotlin/sh/nino/discord/database/tables/GlobalBans.kt new file mode 100644 index 00000000..be58b62d --- /dev/null +++ b/bot/database/src/main/kotlin/sh/nino/discord/database/tables/GlobalBans.kt @@ -0,0 +1,65 @@ +/* + * Copyright (c) 2019-2022 Nino + * + * 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. + */ + +package sh.nino.discord.database.tables + +import kotlinx.datetime.Clock +import kotlinx.datetime.TimeZone +import kotlinx.datetime.toLocalDateTime +import org.jetbrains.exposed.dao.LongEntity +import org.jetbrains.exposed.dao.LongEntityClass +import org.jetbrains.exposed.dao.id.EntityID +import org.jetbrains.exposed.sql.kotlin.datetime.datetime +import sh.nino.discord.database.SnowflakeTable + +enum class BanType(val key: String) { + GUILD("guild"), + USER("user"); + + companion object { + fun find(key: String): BanType = + values().find { it.key == key } ?: error("Unable to find '$key' -> BanType") + } +} + +object GlobalBansTable: SnowflakeTable("global_bans") { + val createdAt = datetime("created_at").default(Clock.System.now().toLocalDateTime(TimeZone.currentSystemDefault())) + val expireAt = long("expire_at").nullable() + val reason = varchar("reason", 256).nullable() + val issuer = long("issuer") + val type = customEnumeration( + "type", + "BanTypeEnum", + { value -> BanType.find(value as String) }, + { toDb -> toDb.key } + ) +} + +class GlobalBans(id: EntityID): LongEntity(id) { + companion object: LongEntityClass(GlobalBansTable) + + var createdAt by GlobalBansTable.createdAt + var expireAt by GlobalBansTable.expireAt + var reason by GlobalBansTable.reason + var issuer by GlobalBansTable.issuer + var type by GlobalBansTable.type +} diff --git a/bot/database/src/main/kotlin/sh/nino/discord/database/tables/Guilds.kt b/bot/database/src/main/kotlin/sh/nino/discord/database/tables/Guilds.kt new file mode 100644 index 00000000..791ca651 --- /dev/null +++ b/bot/database/src/main/kotlin/sh/nino/discord/database/tables/Guilds.kt @@ -0,0 +1,52 @@ +/* + * Copyright (c) 2019-2022 Nino + * + * 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. + */ + +package sh.nino.discord.database.tables + +import org.jetbrains.exposed.dao.LongEntity +import org.jetbrains.exposed.dao.LongEntityClass +import org.jetbrains.exposed.dao.id.EntityID +import org.jetbrains.exposed.sql.VarCharColumnType +import sh.nino.discord.database.SnowflakeTable +import sh.nino.discord.database.columns.array + +object GuildSettings: SnowflakeTable("guilds") { + val usePlainModlogMessage = bool("use_plain_modlog_message").default(false) + val modlogWebhookUri = text("modlog_webhook_uri").nullable().default(null) + val noThreadsRoleId = long("no_threads_role_id").nullable().default(null) + val modlogChannelId = long("modlog_channel_id").nullable().default(null) + val mutedRoleId = long("muted_role_id").nullable().default(null) + val language = text("language").default("en_US") + val prefixes = array("prefixes", VarCharColumnType(18)).default(arrayOf()) +} + +class GuildSettingsEntity(id: EntityID): LongEntity(id) { + companion object: LongEntityClass(GuildSettings) + + var usePlainModlogMessage by GuildSettings.usePlainModlogMessage + var modlogWebhookUri by GuildSettings.modlogWebhookUri + var noThreadsRoleId by GuildSettings.noThreadsRoleId + var modlogChannelId by GuildSettings.modlogChannelId + var mutedRoleId by GuildSettings.mutedRoleId + var language by GuildSettings.language + var prefixes by GuildSettings.prefixes +} diff --git a/bot/database/src/main/kotlin/sh/nino/discord/database/tables/Logging.kt b/bot/database/src/main/kotlin/sh/nino/discord/database/tables/Logging.kt new file mode 100644 index 00000000..639f7985 --- /dev/null +++ b/bot/database/src/main/kotlin/sh/nino/discord/database/tables/Logging.kt @@ -0,0 +1,68 @@ +/* + * Copyright (c) 2019-2022 Nino + * + * 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. + */ + +package sh.nino.discord.database.tables + +import org.jetbrains.exposed.dao.LongEntity +import org.jetbrains.exposed.dao.LongEntityClass +import org.jetbrains.exposed.dao.id.EntityID +import org.jetbrains.exposed.sql.LongColumnType +import org.jetbrains.exposed.sql.TextColumnType +import sh.nino.discord.database.SnowflakeTable +import sh.nino.discord.database.columns.array + +enum class LogEvent(val key: String, val pretty: String) { + VoiceMemberDeafen("voice member deafen", "Voice Member Deafened"), + VoiceChannelLeave("voice channel leave", "Voice Channel Leave"), + VoiceChannelSwitch("voice channel switch", "Voice Channel Switch"), + VoiceChannelJoin("voice channel join", "Voice Channel Join"), + VoiceMemberMuted("voice member muted", "Voice Member Muted"), + MessageUpdated("message updated", "Message Updates"), + MessageDeleted("message deleted", "Message Deletes"), + MemberUnboosted("member unboosted", "Member Unboosted"), + MemberBoosted("member boosted", "Member Boosted"), + ThreadArchived("thread archive", "Channel Thread Archived"), + ThreadCreated("thread created", "Channel Thread Created"), + ThreadDeleted("thread deleted", "Channel Thread Deleted"); + + companion object { + operator fun get(key: String): LogEvent = values().find { it.name == key } ?: error("Unable to find key '$key' -> LogEvent") + } +} + +object GuildLogging: SnowflakeTable("logging") { + val ignoreChannels = array("ignored_channels", LongColumnType()).default(arrayOf()) + val ignoredUsers = array("ignored_users", LongColumnType()).default(arrayOf()) + val channelId = long("channel_id").nullable().default(null) + val enabled = bool("enabled").default(false) + val events = array("events", TextColumnType()).default(arrayOf()) +} + +class LoggingEntity(id: EntityID): LongEntity(id) { + companion object: LongEntityClass(GuildLogging) + + var ignoreChannels by GuildLogging.ignoreChannels + var ignoredUsers by GuildLogging.ignoredUsers + var channelId by GuildLogging.channelId + var enabled by GuildLogging.enabled + var events by GuildLogging.events +} diff --git a/bot/database/src/main/kotlin/sh/nino/discord/database/tables/Punishments.kt b/bot/database/src/main/kotlin/sh/nino/discord/database/tables/Punishments.kt new file mode 100644 index 00000000..50ee0a6c --- /dev/null +++ b/bot/database/src/main/kotlin/sh/nino/discord/database/tables/Punishments.kt @@ -0,0 +1,53 @@ +/* + * Copyright (c) 2019-2022 Nino + * + * 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. + */ + +package sh.nino.discord.database.tables + +import org.jetbrains.exposed.dao.LongEntity +import org.jetbrains.exposed.dao.LongEntityClass +import org.jetbrains.exposed.dao.id.EntityID +import org.jetbrains.exposed.sql.LongColumnType +import sh.nino.discord.database.SnowflakeTable +import sh.nino.discord.database.columns.array + +object Punishments: SnowflakeTable("punishments") { + var warnings = integer("warnings").default(1) + var roleIds = array("roleIds", LongColumnType()) + var index = integer("index").autoIncrement() + var soft = bool("soft").nullable() + var time = long("time").nullable() + var type = customEnumeration("type", "PunishmentTypeEnum", { value -> + PunishmentType[value as String] + }, { t -> t.key }) + + override val primaryKey: PrimaryKey = PrimaryKey(id, index, name = "PK_GuildPunishments") +} + +class PunishmentsEntity(id: EntityID): LongEntity(id) { + companion object: LongEntityClass(Punishments) + + var warnings by Punishments.warnings + var roleIds by Punishments.roleIds + var soft by Punishments.soft + var time by Punishments.time + var type by Punishments.type +} diff --git a/src/structures/decorators/Subscribe.ts b/bot/database/src/main/kotlin/sh/nino/discord/database/tables/Users.kt similarity index 60% rename from src/structures/decorators/Subscribe.ts rename to bot/database/src/main/kotlin/sh/nino/discord/database/tables/Users.kt index 58ba3c32..6dba402a 100644 --- a/src/structures/decorators/Subscribe.ts +++ b/bot/database/src/main/kotlin/sh/nino/discord/database/tables/Users.kt @@ -1,5 +1,5 @@ -/** - * Copyright (c) 2019-2021 Nino +/* + * Copyright (c) 2019-2022 Nino * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -20,23 +20,23 @@ * SOFTWARE. */ -import { MetadataKeys } from '../../util/Constants'; +package sh.nino.discord.database.tables -interface Subscription { - run(...args: any[]): Promise; - event: string; +import org.jetbrains.exposed.dao.LongEntity +import org.jetbrains.exposed.dao.LongEntityClass +import org.jetbrains.exposed.dao.id.EntityID +import org.jetbrains.exposed.sql.VarCharColumnType +import sh.nino.discord.database.SnowflakeTable +import sh.nino.discord.database.columns.array + +object Users: SnowflakeTable("users") { + val prefixes = array("prefixes", VarCharColumnType(25)).default(arrayOf()) + val language = text("language").default("en_US") } -export const getSubscriptionsIn = (target: any) => - Reflect.getMetadata(MetadataKeys.Subscribe, target) ?? []; -export default function Subscribe(event: string): MethodDecorator { - return (target, _, descriptor: TypedPropertyDescriptor) => { - const subscriptions = getSubscriptionsIn(target); - subscriptions.push({ - event, - run: descriptor.value!, - }); +class UserEntity(id: EntityID): LongEntity(id) { + companion object: LongEntityClass(Users) - Reflect.defineMetadata(MetadataKeys.Subscribe, subscriptions, target); - }; + var prefixes by Users.prefixes + var language by Users.language } diff --git a/src/entities/PunishmentsEntity.ts b/bot/database/src/main/kotlin/sh/nino/discord/database/tables/Warnings.kt similarity index 52% rename from src/entities/PunishmentsEntity.ts rename to bot/database/src/main/kotlin/sh/nino/discord/database/tables/Warnings.kt index 81985cdc..f9687f01 100644 --- a/src/entities/PunishmentsEntity.ts +++ b/bot/database/src/main/kotlin/sh/nino/discord/database/tables/Warnings.kt @@ -1,5 +1,5 @@ -/** - * Copyright (c) 2019-2021 Nino +/* + * Copyright (c) 2019-2022 Nino * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -20,45 +20,30 @@ * SOFTWARE. */ -import { Entity, Column, PrimaryGeneratedColumn, PrimaryColumn } from 'typeorm'; +package sh.nino.discord.database.tables -export enum PunishmentType { - WarningRemoved = 'warning.removed', - VoiceUndeafen = 'voice.undeafen', - WarningAdded = 'warning.added', - VoiceUnmute = 'voice.unmute', - VoiceDeafen = 'voice.deafen', - VoiceMute = 'voice.mute', - Unmute = 'unmute', - Unban = 'unban', - Kick = 'kick', - Mute = 'mute', - Ban = 'ban', -} - -@Entity({ name: 'punishments' }) -export default class PunishmentEntity { - @Column({ default: 1 }) - public warnings!: number; - - @PrimaryColumn({ name: 'guild_id' }) - public guildID!: string; +import org.jetbrains.exposed.dao.LongEntity +import org.jetbrains.exposed.dao.LongEntityClass +import org.jetbrains.exposed.dao.id.EntityID +import org.jetbrains.exposed.sql.kotlin.datetime.datetime +import sh.nino.discord.database.SnowflakeTable - @PrimaryColumn() - public index!: number; +object Warnings: SnowflakeTable("warnings") { + var receivedAt = datetime("received_at") + var expiresIn = datetime("expires_in").nullable() + var reason = text("reason").nullable() + var guildId = long("guild_id") + var amount = integer("amount").default(0) - @Column({ default: false }) - public soft!: boolean; - - @Column({ default: undefined, nullable: true }) - public time?: number; + override val primaryKey: PrimaryKey = PrimaryKey(id, guildId, name = "PK_UserWarnings") +} - @Column({ default: undefined, nullable: true }) - public days?: number; +class WarningsEntity(id: EntityID): LongEntity(id) { + companion object: LongEntityClass(Warnings) - @Column({ - type: 'enum', - enum: PunishmentType, - }) - public type!: PunishmentType; + var receivedAt by Warnings.receivedAt + var expiresIn by Warnings.expiresIn + var reason by Warnings.reason + var guildId by Warnings.guildId + var amount by Warnings.amount } diff --git a/bot/markup/Cargo.toml b/bot/markup/Cargo.toml new file mode 100644 index 00000000..39094408 --- /dev/null +++ b/bot/markup/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "markup" +version = "0.1.0" +edition = "2021" +authors = ["Noel "] + +[lib] +crate-type = [ "cdylib" ] +path = "src/main/rust/lib.rs" + +[dependencies] +jni = "0.19.0" diff --git a/bot/markup/README.md b/bot/markup/README.md new file mode 100644 index 00000000..8bc42d27 --- /dev/null +++ b/bot/markup/README.md @@ -0,0 +1,72 @@ +# module sh.nino.discord.markup +> Markup language for constructing mod log and logging outputs. + +## Usage +There is two ways to create clean and precise outputs for customizibility. + +There is the simple approach, this is where you don't need anything complex, and want to use generic Mustache templates: + +``` +embed { + title = "Case {{ .CaseId }} | {{ .Victim | ToUserString }}" + + {{- if (.Reason != nil) }} + description = "{{ .Reason | PreserveMarkdown }}" + {{- end }} +} +``` + +And there is the "programmer" approach, where you have a bunch of standard library functions to use: + +``` +case = context.getCase(); +language = context.getCurrentLanguage(); + +create embed with { + title("Case $(case.id) | ${case.victim |> ToUserString} (${case.victim.id})") // => Case #1 | August#5820 (280158289667555328) + check if case.meta.reason is not nil { + description(case.meta.reason) + } or else { + description("owo.da.uwu" |> language.translate) // Use Nino's localization to customize this output. + } +} +``` + +### With Kotlin +```kotlin +fun main(args: Array) { + val markup = MarkupLanguage { + complexityType = ComplexityType.ROBUST + } + + val context = markup.createContext(mapOf( + "case" to MyCase(), + "currentLanguage" to SomeLanguage() + )) + + val node = markup.parse(""" + case = context.getCase(); + language = context.getCurrentLanguage(); + + create embed with { + title("Case $(case.id) | \$\{case.victim |> ToUserString} (\$\{case.victim.id})") // => Case #1 | August#5820 (280158289667555328) + check if case.meta.reason is not nil { + description(case.meta.reason) + } or else { + description("owo.da.uwu" |> language.translate) // Use Nino's localization to customize this output. + } + } + """, withContext = context) + + node.errors // => List + node.result // => EmbedDocument? + node.result?.toKordEmbed() // => EmbedBuilder +} +``` + +## Compile +To compile the Rust project to bring in the bindings, you can call the `compileRust` task: + +```sh +$ ./gradlew :bot:markup:compileRust +``` diff --git a/bot/markup/build.gradle.kts b/bot/markup/build.gradle.kts new file mode 100644 index 00000000..3c7a323c --- /dev/null +++ b/bot/markup/build.gradle.kts @@ -0,0 +1,40 @@ +/** + * Copyright (c) 2019-2022 Nino + * + * 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. + */ + +plugins { + `nino-module` +} + +val sourcesJar by tasks.registering(Jar::class) { + archiveClassifier.set("sources") + from(sourceSets["main"].allSource) +} + +tasks.create("compileRust") { + workingDir = file(".") + commandLine = listOf("cargo", "build", "--release") + + copy { + from("build/rust/release/libmarkup.so") + into("src/main/resources/native/linux-x86-64") + } +} diff --git a/bot/markup/src/main/kotlin/sh/nino/discord/markup/MarkupLanguage.kt b/bot/markup/src/main/kotlin/sh/nino/discord/markup/MarkupLanguage.kt new file mode 100644 index 00000000..f5083384 --- /dev/null +++ b/bot/markup/src/main/kotlin/sh/nino/discord/markup/MarkupLanguage.kt @@ -0,0 +1,23 @@ +/* + * Copyright (c) 2019-2022 Nino + * + * 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. + */ + +package sh.nino.discord.markup diff --git a/bot/markup/src/main/kotlin/sh/nino/discord/markup/MarkupLexer.kt b/bot/markup/src/main/kotlin/sh/nino/discord/markup/MarkupLexer.kt new file mode 100644 index 00000000..f5083384 --- /dev/null +++ b/bot/markup/src/main/kotlin/sh/nino/discord/markup/MarkupLexer.kt @@ -0,0 +1,23 @@ +/* + * Copyright (c) 2019-2022 Nino + * + * 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. + */ + +package sh.nino.discord.markup diff --git a/bot/markup/src/main/kotlin/sh/nino/discord/markup/MarkupParser.kt b/bot/markup/src/main/kotlin/sh/nino/discord/markup/MarkupParser.kt new file mode 100644 index 00000000..f5083384 --- /dev/null +++ b/bot/markup/src/main/kotlin/sh/nino/discord/markup/MarkupParser.kt @@ -0,0 +1,23 @@ +/* + * Copyright (c) 2019-2022 Nino + * + * 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. + */ + +package sh.nino.discord.markup diff --git a/bot/markup/src/main/kotlin/sh/nino/discord/markup/_Loader.kt b/bot/markup/src/main/kotlin/sh/nino/discord/markup/_Loader.kt new file mode 100644 index 00000000..f5083384 --- /dev/null +++ b/bot/markup/src/main/kotlin/sh/nino/discord/markup/_Loader.kt @@ -0,0 +1,23 @@ +/* + * Copyright (c) 2019-2022 Nino + * + * 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. + */ + +package sh.nino.discord.markup diff --git a/bot/markup/src/main/kotlin/sh/nino/discord/markup/impl/MarkupLanguageImpl.kt b/bot/markup/src/main/kotlin/sh/nino/discord/markup/impl/MarkupLanguageImpl.kt new file mode 100644 index 00000000..ef5e6817 --- /dev/null +++ b/bot/markup/src/main/kotlin/sh/nino/discord/markup/impl/MarkupLanguageImpl.kt @@ -0,0 +1,23 @@ +/* + * Copyright (c) 2019-2022 Nino + * + * 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. + */ + +package sh.nino.discord.markup.impl diff --git a/bot/markup/src/main/kotlin/sh/nino/discord/markup/impl/MarkupLexerImpl.kt b/bot/markup/src/main/kotlin/sh/nino/discord/markup/impl/MarkupLexerImpl.kt new file mode 100644 index 00000000..ef5e6817 --- /dev/null +++ b/bot/markup/src/main/kotlin/sh/nino/discord/markup/impl/MarkupLexerImpl.kt @@ -0,0 +1,23 @@ +/* + * Copyright (c) 2019-2022 Nino + * + * 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. + */ + +package sh.nino.discord.markup.impl diff --git a/bot/markup/src/main/kotlin/sh/nino/discord/markup/impl/MarkupParserImpl.kt b/bot/markup/src/main/kotlin/sh/nino/discord/markup/impl/MarkupParserImpl.kt new file mode 100644 index 00000000..ef5e6817 --- /dev/null +++ b/bot/markup/src/main/kotlin/sh/nino/discord/markup/impl/MarkupParserImpl.kt @@ -0,0 +1,23 @@ +/* + * Copyright (c) 2019-2022 Nino + * + * 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. + */ + +package sh.nino.discord.markup.impl diff --git a/bot/markup/src/main/kotlin/sh/nino/discord/markup/nodes/ASTNode.kt b/bot/markup/src/main/kotlin/sh/nino/discord/markup/nodes/ASTNode.kt new file mode 100644 index 00000000..e1583743 --- /dev/null +++ b/bot/markup/src/main/kotlin/sh/nino/discord/markup/nodes/ASTNode.kt @@ -0,0 +1,23 @@ +/* + * Copyright (c) 2019-2022 Nino + * + * 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. + */ + +package sh.nino.discord.markup.nodes diff --git a/bot/markup/src/main/kotlin/sh/nino/discord/markup/nodes/ASTWriter.kt b/bot/markup/src/main/kotlin/sh/nino/discord/markup/nodes/ASTWriter.kt new file mode 100644 index 00000000..e1583743 --- /dev/null +++ b/bot/markup/src/main/kotlin/sh/nino/discord/markup/nodes/ASTWriter.kt @@ -0,0 +1,23 @@ +/* + * Copyright (c) 2019-2022 Nino + * + * 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. + */ + +package sh.nino.discord.markup.nodes diff --git a/bot/markup/src/main/kotlin/sh/nino/discord/markup/nodes/_Nodes.kt b/bot/markup/src/main/kotlin/sh/nino/discord/markup/nodes/_Nodes.kt new file mode 100644 index 00000000..e1583743 --- /dev/null +++ b/bot/markup/src/main/kotlin/sh/nino/discord/markup/nodes/_Nodes.kt @@ -0,0 +1,23 @@ +/* + * Copyright (c) 2019-2022 Nino + * + * 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. + */ + +package sh.nino.discord.markup.nodes diff --git a/bot/markup/src/main/rust/ast.rs b/bot/markup/src/main/rust/ast.rs new file mode 100644 index 00000000..e69de29b diff --git a/bot/markup/src/main/rust/errors.rs b/bot/markup/src/main/rust/errors.rs new file mode 100644 index 00000000..e69de29b diff --git a/bot/markup/src/main/rust/lib.rs b/bot/markup/src/main/rust/lib.rs new file mode 100644 index 00000000..5f28574c --- /dev/null +++ b/bot/markup/src/main/rust/lib.rs @@ -0,0 +1,5 @@ +mod parser; +mod tokens; +mod errors; +mod ast; +mod util; diff --git a/bot/markup/src/main/rust/parser.rs b/bot/markup/src/main/rust/parser.rs new file mode 100644 index 00000000..e69de29b diff --git a/bot/markup/src/main/rust/tokens.rs b/bot/markup/src/main/rust/tokens.rs new file mode 100644 index 00000000..e69de29b diff --git a/bot/markup/src/main/rust/util.rs b/bot/markup/src/main/rust/util.rs new file mode 100644 index 00000000..364d2760 --- /dev/null +++ b/bot/markup/src/main/rust/util.rs @@ -0,0 +1,7 @@ +use jni::objects::JString; +use jni::JNIEnv; + +/// This function converts a JString into a Rust string. +fn jstring_to_string(jni: JNIEnv, js: JString) -> String { + jni.get_string(js).unwrap().into() +} diff --git a/bot/metrics/build.gradle.kts b/bot/metrics/build.gradle.kts new file mode 100644 index 00000000..042bee36 --- /dev/null +++ b/bot/metrics/build.gradle.kts @@ -0,0 +1,25 @@ +/** + * Copyright (c) 2019-2022 Nino + * + * 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. + */ + +plugins { + `nino-module` +} diff --git a/bot/metrics/src/main/kotlin/sh/nino/discord/metrics/MetricsRegistry.kt b/bot/metrics/src/main/kotlin/sh/nino/discord/metrics/MetricsRegistry.kt new file mode 100644 index 00000000..15960847 --- /dev/null +++ b/bot/metrics/src/main/kotlin/sh/nino/discord/metrics/MetricsRegistry.kt @@ -0,0 +1,132 @@ +/* + * Copyright (c) 2019-2022 Nino + * + * 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. + */ + +package sh.nino.discord.metrics + +import gay.floof.utils.slf4j.logging +import io.prometheus.client.CollectorRegistry +import io.prometheus.client.Counter +import io.prometheus.client.Gauge +import io.prometheus.client.Histogram +import io.prometheus.client.hotspot.DefaultExports +import sh.nino.discord.common.data.Config + +class MetricsRegistry(config: Config) { + private val logger by logging() + val enabled: Boolean = config.metrics + + val commandsExecutedGauge: Gauge? + val commandLatency: Histogram? + val gatewayPing: Gauge? + val messagesSeen: Counter? + val gatewayLatency: Gauge? + val apiRequestLatency: Histogram? + val apiRequests: Gauge? + val registry: CollectorRegistry? + val users: Gauge? + val guildCount: Gauge? + val websocketEvents: Counter? + + init { + if (enabled) { + logger.info("Metrics is enabled, you will be able to collect them from the API endpoint /metrics") + registry = CollectorRegistry() + + // Export JVM metrics cuz cool and good + DefaultExports.register(registry) + + // Export our own! + commandsExecutedGauge = Gauge.build() + .name("nino_commands_executed") + .help("Returns how many commands were executed during its lifetime.") + .register(registry) + + commandLatency = Histogram.build() + .name("nino_command_latency") + .help("Returns the latency in milliseconds of how long a command is executed.") + .labelNames("command") + .register(registry) + + gatewayLatency = Gauge.build() + .name("nino_gateway_latency") + .help("Returns the gateway latency per shard. Use the `gatewayPing` gauge for all shards combined.") + .labelNames("shard") + .register(registry) + + gatewayPing = Gauge.build() + .name("nino_gateway_ping") + .help("Returns the gateway latency for all shards.") + .register(registry) + + messagesSeen = Counter.build() + .name("nino_messages_seen") + .help("Returns how many messages Nino has seen.") + .register(registry) + + guildCount = Gauge.build() + .name("nino_guild_count") + .help("Returns how many guilds Nino is in") + .register(registry) + + users = Gauge.build() + .name("nino_user_count") + .help("Returns how many users Nino can see") + .register(registry) + + websocketEvents = Counter.build() + .name("nino_websocket_events") + .help("Returns how many events that are being emitted.") + .labelNames("shard", "event") + .register(registry) + + if (config.api != null) { + apiRequestLatency = Histogram.build() + .name("nino_api_request_latency") + .help("Returns the average latency on all API requests.") + .register(registry) + + apiRequests = Gauge.build() + .name("nino_api_request_count") + .help("Returns how many requests by endpoint + method have been executed.") + .labelNames("endpoint", "method") + .register(registry) + } else { + apiRequests = null + apiRequestLatency = null + } + } else { + logger.warn("Metrics is not available on this instance.") + + registry = null + commandsExecutedGauge = null + commandLatency = null + gatewayLatency = null + gatewayPing = null + messagesSeen = null + apiRequests = null + apiRequestLatency = null + users = null + guildCount = null + websocketEvents = null + } + } +} diff --git a/bot/punishments/build.gradle.kts b/bot/punishments/build.gradle.kts new file mode 100644 index 00000000..ec618a29 --- /dev/null +++ b/bot/punishments/build.gradle.kts @@ -0,0 +1,30 @@ +/** + * Copyright (c) 2019-2022 Nino + * + * 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. + */ + +plugins { + `nino-module` +} + +dependencies { + implementation(project(":bot:database")) + api(project(":bot:timeouts")) +} diff --git a/bot/punishments/src/main/kotlin/sh/nino/discord/punishments/MemberLike.kt b/bot/punishments/src/main/kotlin/sh/nino/discord/punishments/MemberLike.kt new file mode 100644 index 00000000..80c189e9 --- /dev/null +++ b/bot/punishments/src/main/kotlin/sh/nino/discord/punishments/MemberLike.kt @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2019-2022 Nino + * + * 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. + */ + +package sh.nino.discord.punishments + +import dev.kord.common.entity.Snowflake +import dev.kord.core.entity.Guild +import dev.kord.core.entity.Member + +class MemberLike( + val member: Member?, + val guild: Guild, + val id: Snowflake +) { + val partial: Boolean + get() = member == null +} diff --git a/bot/punishments/src/main/kotlin/sh/nino/discord/punishments/PunishmentModule.kt b/bot/punishments/src/main/kotlin/sh/nino/discord/punishments/PunishmentModule.kt new file mode 100644 index 00000000..eb29eb58 --- /dev/null +++ b/bot/punishments/src/main/kotlin/sh/nino/discord/punishments/PunishmentModule.kt @@ -0,0 +1,109 @@ +/* + * Copyright (c) 2019-2022 Nino + * + * 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. + */ + +package sh.nino.discord.punishments + +import dev.kord.core.entity.Member +import dev.kord.core.entity.Message +import kotlinx.datetime.LocalDateTime +import sh.nino.discord.database.tables.GuildCasesEntity +import sh.nino.discord.database.tables.PunishmentType +import sh.nino.discord.punishments.builder.ApplyPunishmentBuilder +import sh.nino.discord.punishments.builder.PublishModLogBuilder + +interface PunishmentModule { + /** + * Resolves the current [member] to get the actual member object IF the current + * [member] object is a partial member instance. + */ + suspend fun resolveMember(member: MemberLike, useRest: Boolean = false): Member + + /** + * Adds a warning to the [member]. + * @param member The member to add warnings towards. + * @param moderator The moderator who invoked this action. + * @param reason The reason why the [member] needs to be warned. + * @param amount The amount of warnings to add. If [amount] is set to `null`, + * it'll just add the amount of warnings from the [member] in the guild by 1. + */ + suspend fun addWarning( + member: Member, + moderator: Member, + reason: String? = null, + amount: Int = 1, + expiresIn: LocalDateTime? = null + ) + + /** + * Removes any warnings from the [member]. + * + * @param member The member that needs their warnings removed. + * @param moderator The moderator who invoked this action. + * @param reason The reason why the warnings were removed. + * @param amount The amount of warnings to add. If [amount] is set to `null`, + * it'll just clean their database entries for this specific guild, not globally. + * + * @throws IllegalStateException If the member doesn't need any warnings removed. + */ + suspend fun removeWarning( + member: Member, + moderator: Member, + reason: String? = null, + amount: Int? = null + ) + + /** + * Applies a new punishment to a user, if needed. + * @param member The [member][MemberLike] to execute this action. + * @param moderator The moderator who executed this action. + * @param type The punishment type that is being executed. + * @param builder DSL builder for any extra options. + */ + suspend fun apply( + member: MemberLike, + moderator: Member, + type: PunishmentType, + builder: ApplyPunishmentBuilder.() -> Unit = {} + ) + + /** + * Publishes the [case] towards the mod-log channel if specified + * in guild settings. + * + * @param case The case to use to send out the modlog embed. + * @param builder The builder DSL to use + */ + suspend fun publishModlog( + case: GuildCasesEntity, + builder: PublishModLogBuilder.() -> Unit = {} + ) + + /** + * Edits the mod log message with the edited [case] properties. + * @param case The case to use to send out the modlog embed. + * @param message The message itself. + */ + suspend fun editModlogMessage( + case: GuildCasesEntity, + message: Message + ) +} diff --git a/bot/punishments/src/main/kotlin/sh/nino/discord/punishments/_koinModule.kt b/bot/punishments/src/main/kotlin/sh/nino/discord/punishments/_koinModule.kt new file mode 100644 index 00000000..7bac279a --- /dev/null +++ b/bot/punishments/src/main/kotlin/sh/nino/discord/punishments/_koinModule.kt @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2019-2022 Nino + * + * 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. + */ + +package sh.nino.discord.punishments + +import org.koin.dsl.module +import sh.nino.discord.punishments.impl.PunishmentModuleImpl + +val punishmentsModule = module { + single { + PunishmentModuleImpl() + } +} diff --git a/bot/punishments/src/main/kotlin/sh/nino/discord/punishments/builder/ApplyPunishmentBuilder.kt b/bot/punishments/src/main/kotlin/sh/nino/discord/punishments/builder/ApplyPunishmentBuilder.kt new file mode 100644 index 00000000..679151fe --- /dev/null +++ b/bot/punishments/src/main/kotlin/sh/nino/discord/punishments/builder/ApplyPunishmentBuilder.kt @@ -0,0 +1,112 @@ +/* + * Copyright (c) 2019-2022 Nino + * + * 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. + */ + +package sh.nino.discord.punishments.builder + +import dev.kord.common.entity.Snowflake +import dev.kord.core.entity.Attachment +import dev.kord.core.entity.Guild +import dev.kord.core.entity.Member +import dev.kord.core.entity.channel.VoiceChannel +import sh.nino.discord.database.tables.PunishmentType +import sh.nino.discord.punishments.MemberLike + +/** + * The data when you fun the [ApplyPunishmentBuilder.build] method. + */ +data class ApplyPunishmentData( + /** + * Returns the [voice channel][VoiceChannel] that is applied to this punishment. + * + * This is only tied to the following punishment types: + * - [PunishmentType.VOICE_UNDEAFEN] + * - [PunishmentType.VOICE_DEAFEN] + * - [PunishmentType.VOICE_UNMUTE] + * - [PunishmentType.VOICE_MUTE] + */ + val voiceChannel: VoiceChannel? = null, + + /** + * Returns a list of attachments to use to provide more evidence within a certain case. + */ + val attachments: List = listOf(), + + /** + * If we should publish this case to the mod-log. + */ + val publish: Boolean = true, + + /** + * The reason why this action was taken care of. + */ + val reason: String? = null, + + /** + * The [MemberLike] object to use. This is available for partial member metadata + * or full metadata. + */ + val member: MemberLike, + + /** + * How much time in milliseconds this action should revert. + */ + val time: Int? = null, + + val roleId: Long? = null, + val soft: Boolean = false, + val days: Int = 7 +) + +class ApplyPunishmentBuilder { + private var _member: MemberLike? = null + var voiceChannel: VoiceChannel? = null + var attachments: List = listOf() + var publish: Boolean = true + var reason: String? = null + var roleId: Long? = null + var time: Int? = null + var soft: Boolean = false + var days: Int = 7 + + fun setMemberData(data: Member?, guild: Guild, id: Snowflake): ApplyPunishmentBuilder { + val member = MemberLike(data, guild, id) + _member = member + + return this + } + + fun build(): ApplyPunishmentData { + require(_member != null) { "Member cannot be null. Use `ApplyPunishmentBuilder#setMemberData`." } + + return ApplyPunishmentData( + voiceChannel, + attachments, + publish, + reason, + member = _member!!, + time, + roleId, + soft, + days + ) + } +} diff --git a/bot/punishments/src/main/kotlin/sh/nino/discord/punishments/builder/PublishModlogBuilder.kt b/bot/punishments/src/main/kotlin/sh/nino/discord/punishments/builder/PublishModlogBuilder.kt new file mode 100644 index 00000000..abbe0004 --- /dev/null +++ b/bot/punishments/src/main/kotlin/sh/nino/discord/punishments/builder/PublishModlogBuilder.kt @@ -0,0 +1,82 @@ +/* + * Copyright (c) 2019-2022 Nino + * + * 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. + */ + +package sh.nino.discord.punishments.builder + +import dev.kord.core.entity.Attachment +import dev.kord.core.entity.Guild +import dev.kord.core.entity.User +import dev.kord.core.entity.channel.VoiceChannel +import sh.nino.discord.database.tables.PunishmentType + +data class PublishModLogData( + val warningsRemoved: Int? = null, + val warningsAdded: Int? = null, + val attachments: List = listOf(), + val moderator: User, + val voiceChannel: VoiceChannel? = null, + val reason: String? = null, + val victim: User, + val guild: Guild, + val time: Int? = null, + val type: PunishmentType +) + +class PublishModLogBuilder { + private val attachments: MutableList = mutableListOf() + + lateinit var moderator: User + lateinit var victim: User + lateinit var guild: Guild + lateinit var type: PunishmentType + + var warningsRemoved: Int? = null + var warningsAdded: Int? = null + var voiceChannel: VoiceChannel? = null + var reason: String? = null + var time: Int? = null + + fun addAttachments(list: List): PublishModLogBuilder { + attachments.addAll(list) + return this + } + + fun build(): PublishModLogData { + require(this::moderator.isInitialized) { "Moderator is a required property to initialize." } + require(this::victim.isInitialized) { "Victim is a required property to initialize." } + require(this::guild.isInitialized) { "Guild is a required property to be initialized." } + require(this::type.isInitialized) { "Punishment type is a required property to be initialized." } + + return PublishModLogData( + warningsRemoved, + warningsAdded, + attachments, + moderator, + voiceChannel, + reason, + victim, + guild, + time, + type + ) + } +} diff --git a/bot/punishments/src/main/kotlin/sh/nino/discord/punishments/extensions.kt b/bot/punishments/src/main/kotlin/sh/nino/discord/punishments/extensions.kt new file mode 100644 index 00000000..cb13fb72 --- /dev/null +++ b/bot/punishments/src/main/kotlin/sh/nino/discord/punishments/extensions.kt @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2019-2022 Nino + * + * 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. + */ + +@file:JvmName("PunishmentExtensionsKt") +package sh.nino.discord.punishments + +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.toList + +fun Flow.sortWith(comparator: (T, T) -> Int): Flow = flow { + for (entity in toList().sortedWith(Comparator(comparator))) emit(entity) +} diff --git a/bot/punishments/src/main/kotlin/sh/nino/discord/punishments/impl/PunishmentModuleImpl.kt b/bot/punishments/src/main/kotlin/sh/nino/discord/punishments/impl/PunishmentModuleImpl.kt new file mode 100644 index 00000000..77750cb2 --- /dev/null +++ b/bot/punishments/src/main/kotlin/sh/nino/discord/punishments/impl/PunishmentModuleImpl.kt @@ -0,0 +1,943 @@ +/* + * Copyright (c) 2019-2022 Nino + * + * 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. + */ + +package sh.nino.discord.punishments.impl + +import dev.kord.common.entity.Permission +import dev.kord.common.entity.Permissions +import dev.kord.common.entity.Snowflake +import dev.kord.core.Kord +import dev.kord.core.behavior.ban +import dev.kord.core.behavior.channel.createMessage +import dev.kord.core.behavior.channel.editRolePermission +import dev.kord.core.behavior.edit +import dev.kord.core.behavior.getChannelOf +import dev.kord.core.cache.data.AttachmentData +import dev.kord.core.cache.data.MemberData +import dev.kord.core.cache.data.toData +import dev.kord.core.entity.* +import dev.kord.core.entity.channel.TextChannel +import dev.kord.core.firstOrNull +import dev.kord.rest.builder.message.EmbedBuilder +import gay.floof.utils.slf4j.logging +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.firstOrNull +import kotlinx.coroutines.flow.toList +import kotlinx.datetime.Clock +import kotlinx.datetime.LocalDateTime +import kotlinx.datetime.TimeZone +import kotlinx.datetime.toLocalDateTime +import org.jetbrains.exposed.sql.and +import org.jetbrains.exposed.sql.deleteWhere +import org.jetbrains.exposed.sql.update +import sh.nino.discord.common.COLOR +import sh.nino.discord.common.extensions.asSnowflake +import sh.nino.discord.common.extensions.inject +import sh.nino.discord.common.isMemberAbove +import sh.nino.discord.common.ms +import sh.nino.discord.database.asyncTransaction +import sh.nino.discord.database.tables.* +import sh.nino.discord.punishments.MemberLike +import sh.nino.discord.punishments.PunishmentModule +import sh.nino.discord.punishments.builder.ApplyPunishmentBuilder +import sh.nino.discord.punishments.builder.PublishModLogBuilder +import sh.nino.discord.punishments.builder.PublishModLogData +import sh.nino.discord.punishments.sortWith +import sh.nino.discord.timeouts.Client +import sh.nino.discord.timeouts.RequestCommand +import sh.nino.discord.timeouts.Timeout + +class PunishmentModuleImpl: PunishmentModule { + private val logger by logging() + private val timeouts: Client by inject() + private val kord: Kord by inject() + + /** + * Resolves the current [member] to get the actual member object IF the current + * [member] object is a partial member instance. + */ + override suspend fun resolveMember(member: MemberLike, useRest: Boolean): Member { + if (!member.partial) return member.member!! + + // If it is cached in Kord, let's return it + val cachedMember = kord.defaultSupplier.getMemberOrNull(member.guild.id, member.id) + if (cachedMember != null) return cachedMember + + // If not, let's retrieve it from REST + // the parameter is a bit misleading though... + return if (useRest) { + val rawMember = kord.rest.guild.getGuildMember(member.guild.id, member.id) + Member(rawMember.toData(member.guild.id, member.id), rawMember.user.value!!.toData(), kord) + } else { + val user = kord.rest.user.getUser(member.id) + Member( + // we're mocking this because we have no information + // about the member, so. + MemberData( + member.id, + member.guild.id, + joinedAt = Clock.System.now().toString(), + roles = listOf() + ), + + user.toData(), + kord + ) + } + } + + /** + * Adds a warning to the [member]. + * @param member The member to add warnings towards. + * @param moderator The moderator who invoked this action. + * @param reason The reason why the [member] needs to be warned. + * @param amount The amount of warnings to add. If [amount] is set to `null`, + * it'll just add the amount of warnings from the [member] in the guild by 1. + */ + override suspend fun addWarning(member: Member, moderator: Member, reason: String?, amount: Int, expiresIn: LocalDateTime?) { + logger.info("Adding $amount warning$${if (amount == 0 || amount > 1) "s" else ""} to ${member.tag} by moderator ${moderator.tag}${if (reason != null) " for $reason" else ""}") + val warnings = asyncTransaction { + WarningsEntity.find { + Warnings.id eq member.id.value.toLong() + } + } + + val combined = warnings.fold(0) { acc, curr -> + acc + curr.amount + } + + val attach = combined + amount + if (attach < 0) throw IllegalStateException("attached warnings = out of bounds (<0; gotten $attach)") + + val guildPunishments = asyncTransaction { + PunishmentsEntity.find { + Punishments.id eq member.guild.id.value.toLong() + } + } + + val punishmentsToExecute = guildPunishments.filter { it.warnings == attach } + for (punishment in punishmentsToExecute) { + // TODO + } + + // add the warning + val guild = member.guild.asGuild() + asyncTransaction { + WarningsEntity.new(member.id.value.toLong()) { + receivedAt = Clock.System.now().toLocalDateTime(TimeZone.currentSystemDefault()) + + this.expiresIn = expiresIn + this.guildId = guild.id.value.toLong() + this.amount = amount + this.reason = reason + } + } + + // create a new case + val case = asyncTransaction { + GuildCasesEntity.new(member.guild.id.value.toLong()) { + moderatorId = moderator.id.value.toLong() + createdAt = LocalDateTime.parse(Clock.System.now().toString()) + victimId = member.id.value.toLong() + type = PunishmentType.WARNING_ADDED + + this.reason = "Moderator added **$attach** warnings.${if (reason != null) " ($reason)" else ""}" + } + } + + return if (guildPunishments.toList().isEmpty()) { + publishModlog(case) { + this.moderator = moderator + this.guild = guild + + warningsAdded = amount + victim = member + } + } else { + // something here + } + } + + /** + * Removes any warnings from the [member]. + * + * @param member The member that needs their warnings removed. + * @param moderator The moderator who invoked this action. + * @param reason The reason why the warnings were removed. + * @param amount The amount of warnings to add. If [amount] is set to `null`, + * it'll just clean their database entries for this specific guild, not globally. + * + * @throws IllegalStateException If the member doesn't need any warnings removed. + */ + override suspend fun removeWarning(member: Member, moderator: Member, reason: String?, amount: Int?) { + logger.info("Removing ${amount ?: "all"} warnings to ${member.tag} by ${moderator.tag}${if (reason != null) " ($reason)" else ""}") + val warnings = asyncTransaction { + WarningsEntity.find { + (Warnings.id eq member.id.value.toLong()) and (Warnings.guildId eq member.guild.id.value.toLong()) + } + } + + val ifZero = warnings.fold(0) { acc, curr -> acc + curr.amount } + if (warnings.toList().isEmpty() || (ifZero < 0 || ifZero == 0)) + throw IllegalStateException("Member ${member.tag} doesn't have any warnings to be removed.") + + if (amount == null) { + asyncTransaction { + Warnings.deleteWhere { + (Warnings.id eq member.id.value.toLong()) and (Warnings.guildId eq member.guild.id.value.toLong()) + } + } + + // create a new case + val case = asyncTransaction { + GuildCasesEntity.new(member.guild.id.value.toLong()) { + moderatorId = moderator.id.value.toLong() + createdAt = LocalDateTime.parse(Clock.System.now().toString()) + victimId = member.id.value.toLong() + type = PunishmentType.WARNING_ADDED + + this.reason = "Moderator removed all warnings.${if (reason != null) " ($reason)" else ""}" + } + } + + val guild = member.guild.asGuild() + publishModlog(case) { + this.moderator = moderator + this.guild = guild + + warningsRemoved = -1 + victim = member + } + } else { + // Create a warning transaction + asyncTransaction { + WarningsEntity.new(member.id.value.toLong()) { + this.guildId = member.guild.id.value.toLong() + this.amount = -amount + this.reason = reason + } + } + + val guild = member.guild.asGuild() + val case = asyncTransaction { + GuildCasesEntity.new(member.guild.id.value.toLong()) { + moderatorId = moderator.id.value.toLong() + createdAt = LocalDateTime.parse(Clock.System.now().toString()) + victimId = member.id.value.toLong() + type = PunishmentType.WARNING_ADDED + + this.reason = "Moderator removed **$amount** warnings.${if (reason != null) " ($reason)" else ""}" + } + } + + publishModlog(case) { + this.moderator = moderator + this.guild = guild + + warningsRemoved = amount + victim = member + } + } + } + + /** + * Applies a new punishment to a user, if needed. + * @param member The [member][MemberLike] to execute this action. + * @param moderator The moderator who executed this action. + * @param type The punishment type that is being executed. + * @param builder DSL builder for any extra options. + */ + override suspend fun apply( + member: MemberLike, + moderator: Member, + type: PunishmentType, + builder: ApplyPunishmentBuilder.() -> Unit + ) { + val options = ApplyPunishmentBuilder().apply(builder).build() + logger.info("Applying punishment ${type.key} on member ${member.id}${if (options.reason != null) " for ${options.reason}" else ""}") + + val guildSettings = asyncTransaction { + GuildSettingsEntity.findById(member.guild.id.value.toLong())!! + } + + val self = member.guild.getMember(kord.selfId) + if ( + (!member.partial && isMemberAbove(self, member.member!!)) || + (self.getPermissions().code.value.toLong() and type.permissions.code.value.toLong() == 0L) + ) return + + val mem = resolveMember(member, type != PunishmentType.UNBAN) + when (type) { + PunishmentType.VOICE_UNMUTE -> applyVoiceUnmute(mem, options.reason) + PunishmentType.VOICE_UNDEAFEN -> applyVoiceUndeafen(mem, options.reason) + PunishmentType.KICK -> mem.kick(options.reason) + PunishmentType.UNBAN -> mem.guild.unban(member.id, options.reason) + PunishmentType.VOICE_DEAFEN -> applyVoiceDeafen(moderator, options.reason, mem, member.guild, options.time) + PunishmentType.THREAD_MESSAGES_ADDED -> applyAddThreadMessagesBack(guildSettings, mem, options.reason, member.guild) + + PunishmentType.ROLE_ADD -> { + mem.addRole(options.roleId!!.asSnowflake(), options.reason) + } + + PunishmentType.ROLE_REMOVE -> { + mem.removeRole(options.roleId!!.asSnowflake(), options.reason) + } + + PunishmentType.BAN -> applyBan( + mem, + options.reason, + moderator, + member.guild, + options.days, + options.soft, + options.time + ) + + PunishmentType.MUTE -> applyMute( + guildSettings, + mem, + moderator, + options.reason, + member.guild, + options.time + ) + + PunishmentType.UNMUTE -> applyUnmute( + guildSettings, + mem, + options.reason, + member.guild + ) + + PunishmentType.VOICE_MUTE -> applyVoiceMute( + mem, + options.reason, + member.guild, + moderator, + options.time + ) + + PunishmentType.THREAD_MESSAGES_REMOVED -> applyRemoveThreadMessagePerms( + guildSettings, + mem, + moderator, + options.reason, + member.guild, + options.time + ) + + else -> { + // do nothing owo + } + } + + val case = asyncTransaction { + GuildCasesEntity.new(member.guild.id.value.toLong()) { + attachments = options.attachments.toTypedArray().map { it.url }.toTypedArray() + moderatorId = moderator.id.value.toLong() + victimId = member.id.value.toLong() + soft = options.soft + time = options.time?.toLong() + + this.type = type + this.reason = options.reason + } + } + + if (options.publish) { + publishModlog(case) { + this.moderator = moderator + + voiceChannel = options.voiceChannel + reason = options.reason + victim = mem + guild = member.guild + time = options.time + + if (options.attachments.isNotEmpty()) addAttachments( + options.attachments.map { + Attachment( + // we don't store the id, size, proxyUrl, or filename, + // so it's fine to make it mocked. + AttachmentData( + id = Snowflake(0L), + size = 0, + url = it.url, + proxyUrl = it.proxyUrl, + filename = "unknown.png" + ), + + kord + ) + } + ) + } + } + } + + /** + * Publishes the [case] towards the mod-log channel if specified + * in guild settings. + */ + override suspend fun publishModlog(case: GuildCasesEntity, builder: PublishModLogBuilder.() -> Unit) { + val data = PublishModLogBuilder().apply(builder).build() + val settings = asyncTransaction { + GuildSettingsEntity[data.guild.id.value.toLong()] + } + + val modlogChannel = try { + data.guild.getChannelOf(Snowflake(settings.modlogChannelId!!)) + } catch (e: Exception) { + null + } ?: return + + val permissions = modlogChannel.getEffectivePermissions(kord.selfId) + if (!permissions.contains(Permission.SendMessages) || !permissions.contains(Permission.EmbedLinks)) + return + + val message = if (settings.usePlainModlogMessage) { + modlogChannel.createMessage { + content = getModlogPlainText(case.id.value.toInt(), data) + } + } else { + modlogChannel.createMessage { + embeds += getModlogMessage(case.id.value.toInt(), data) + } + } + + asyncTransaction { + GuildCases.update({ + (GuildCases.index eq case.index) and (GuildCases.id eq data.guild.id.value.toLong()) + }) { + it[messageId] = message.id.value.toLong() + } + } + } + + override suspend fun editModlogMessage(case: GuildCasesEntity, message: Message) { + // Check if it was with plan text + val settings = asyncTransaction { + GuildSettingsEntity[case.id.value] + } + + val guild = message.getGuild() + val data = PublishModLogBuilder().apply { + moderator = guild.members.first { it.id == case.moderatorId.asSnowflake() } + reason = case.reason + victim = guild.members.first { it.id == case.victimId.asSnowflake() } + type = case.type + + this.guild = guild + if (case.attachments.isNotEmpty()) { + addAttachments( + case.attachments.map { + Attachment( + // we don't store the id, size, proxyUrl, or filename, + // so it's fine to make it mocked. + AttachmentData( + id = Snowflake(0L), + size = 0, + url = it, + proxyUrl = it, + filename = "unknown.png" + ), + + kord + ) + } + ) + } + } + + if (settings.usePlainModlogMessage) { + // this looks fucking horrendous but it works LOL + val warningsRegex = "> \\*\\*Warnings (Added|Removed)\\*\\*: ([A-Za-z]|\\d+)".toRegex() + val matcher = warningsRegex.toPattern().matcher(message.content) + + // if we find any matches, let's grab em all + if (matcher.matches()) { + val addOrRemove = matcher.group(1) + val allOrInt = matcher.group(2) + + when (addOrRemove) { + "Added" -> { + val intValue = try { + Integer.parseInt(allOrInt) + } catch (e: Exception) { + null + } ?: throw IllegalStateException("Unable to cast \"$allOrInt\" into a number.") + + data.warningsAdded = intValue + } + + "Removed" -> { + if (allOrInt == "All") { + data.warningsRemoved = -1 + } else { + val intValue = try { + Integer.parseInt(allOrInt) + } catch (e: Exception) { + null + } ?: throw IllegalStateException("Unable to cast \"$allOrInt\" into a number.") + + data.warningsRemoved = intValue + } + } + } + } + + message.edit { + content = getModlogPlainText(case.id.value.toInt(), data.build()) + } + } else { + val embed = message.embeds.first() + val warningsRemovedField = embed.fields.firstOrNull { + it.name.lowercase().contains("warnings removed") + } + + val warningsAddedField = embed.fields.firstOrNull { + it.name.lowercase().contains("warnings added") + } + + if (warningsRemovedField != null) + data.warningsRemoved = Integer.parseInt(warningsRemovedField.value) + + if (warningsAddedField != null) + data.warningsAdded = Integer.parseInt(warningsAddedField.value) + + message.edit { + embeds?.plusAssign(getModlogMessage(case.id.value.toInt(), data.build())) + } + } + } + + private suspend fun getOrCreateMutedRole(settings: GuildSettingsEntity, guild: Guild): Snowflake { + if (settings.mutedRoleId != null) return Snowflake(settings.mutedRoleId!!) + + val muteRole: Long + val role = guild.roles.firstOrNull { + it.name.lowercase() == "muted" + } + + if (role == null) { + val newRole = kord.rest.guild.createGuildRole(guild.id) { + hoist = false + reason = "Missing muted role in database and in guild" + name = "Muted" + mentionable = false + permissions = Permissions() + } + + muteRole = newRole.id.value.toLong() + val topRole = guild.members.first { it.id == kord.selfId } + .roles + .sortWith { a, b -> b.rawPosition - a.rawPosition } + .firstOrNull() + + if (topRole != null) { + kord.rest.guild.modifyGuildRolePosition(guild.id) { + move(topRole.id to topRole.rawPosition - 1) + } + + for (channel in guild.channels.toList()) { + val perms = channel.getEffectivePermissions(kord.selfId) + if (perms.contains(Permission.ManageChannels)) { + channel.editRolePermission(newRole.id) { + allowed = Permissions() + denied = Permissions { + -Permission.SendMessages + } + + reason = "Overrided permissions for role ${newRole.name} (${newRole.id})" + } + } + } + } + } else { + muteRole = role.id.value.toLong() + } + + if (muteRole == 0L) throw IllegalStateException("Unable to create or find a mute role, manually add it.") + asyncTransaction { + GuildSettings.update({ GuildSettings.id eq guild.id.value.toLong() }) { + it[mutedRoleId] = muteRole + } + } + + return Snowflake(muteRole) + } + + private suspend fun getOrCreateNoThreadsRole(settings: GuildSettingsEntity, guild: Guild): Snowflake { + if (settings.noThreadsRoleId != null) return Snowflake(settings.noThreadsRoleId!!) + + val muteRole: Long + val role = guild.roles.firstOrNull { + it.name.lowercase() == "no threads" + } + + if (role == null) { + val newRole = kord.rest.guild.createGuildRole(guild.id) { + hoist = false + reason = "Missing no threads role in database and in guild" + name = "No Threads" + mentionable = false + permissions = Permissions() + } + + muteRole = newRole.id.value.toLong() + val topRole = guild.members.first { it.id == kord.selfId } + .roles + .sortWith { a, b -> b.rawPosition - a.rawPosition } + .firstOrNull() + + if (topRole != null) { + kord.rest.guild.modifyGuildRolePosition(guild.id) { + move(topRole.id to topRole.rawPosition - 1) + } + + for (channel in guild.channels.toList()) { + val perms = channel.getEffectivePermissions(kord.selfId) + if (perms.contains(Permission.ManageChannels)) { + channel.editRolePermission(newRole.id) { + allowed = Permissions() + denied = Permissions { + -Permission.SendMessagesInThreads + } + + reason = "Overrided permissions for role ${newRole.name} (${newRole.id})" + } + } + } + } + } else { + muteRole = role.id.value.toLong() + } + + asyncTransaction { + GuildSettings.update({ GuildSettings.id eq guild.id.value.toLong() }) { + it[mutedRoleId] = muteRole + } + } + + return Snowflake(muteRole) + } + + private suspend fun applyBan( + member: Member, + reason: String? = null, + moderator: Member, + guild: Guild, + days: Int = 7, + soft: Boolean = false, + time: Int? = null + ) { + logger.info("Banning ${member.tag} for ${reason ?: "no reason"} by ${moderator.tag} in guild ${guild.name} (${guild.id})") + guild.ban(member.id) { + this.deleteMessagesDays = days + this.reason = reason + } + + if (soft) { + logger.info("Unbanning ${member.tag} (executed softban cmd).") + guild.unban(member.id, reason) + } + + if (!soft && time != null) { + if (timeouts.closed) { + logger.warn("Timeouts microservice has not been established (or not connected)") + return + } + + timeouts.send( + RequestCommand( + Timeout( + guildId = guild.id.toString(), + userId = member.id.toString(), + issuedAt = System.currentTimeMillis(), + expiresIn = time.toLong(), + moderatorId = moderator.id.toString(), + reason = reason, + type = PunishmentType.UNBAN.key + ) + ) + ) + } + } + + private suspend fun applyUnmute(settings: GuildSettingsEntity, member: Member, reason: String?, guild: Guild) { + val muteRoleId = getOrCreateMutedRole(settings, guild) + member.removeRole(muteRoleId, reason) + } + + private suspend fun applyAddThreadMessagesBack(settings: GuildSettingsEntity, member: Member, reason: String?, guild: Guild) { + val threadsRoleId = getOrCreateNoThreadsRole(settings, guild) + member.removeRole(threadsRoleId, reason) + } + + private suspend fun applyMute( + settings: GuildSettingsEntity, + member: Member, + moderator: Member, + reason: String?, + guild: Guild, + time: Int? + ) { + val roleId = getOrCreateMutedRole(settings, guild) + member.addRole(roleId, reason) + + if (time != null) { + if (timeouts.closed) { + logger.warn("Timeouts microservice has not been established (or not connected)") + return + } + + timeouts.send( + RequestCommand( + Timeout( + guildId = guild.id.toString(), + userId = member.id.toString(), + issuedAt = System.currentTimeMillis(), + expiresIn = time.toLong(), + moderatorId = moderator.id.toString(), + reason = reason, + type = PunishmentType.UNMUTE.key + ) + ) + ) + } + } + + private suspend fun applyRemoveThreadMessagePerms( + settings: GuildSettingsEntity, + member: Member, + moderator: Member, + reason: String?, + guild: Guild, + time: Int? + ) { + val roleId = getOrCreateNoThreadsRole(settings, guild) + member.addRole(roleId, reason) + + if (time != null) { + if (timeouts.closed) { + logger.warn("Timeouts microservice has not been established (or not connected)") + return + } + + timeouts.send( + RequestCommand( + Timeout( + guildId = guild.id.toString(), + userId = member.id.toString(), + issuedAt = System.currentTimeMillis(), + expiresIn = time.toLong(), + moderatorId = moderator.id.toString(), + reason = reason, + type = PunishmentType.THREAD_MESSAGES_ADDED.key + ) + ) + ) + } + } + + private suspend fun applyVoiceMute( + member: Member, + reason: String?, + guild: Guild, + moderator: Member, + time: Int? + ) { + val voiceState = member.getVoiceState() + if (voiceState.channelId != null && !voiceState.isMuted) { + member.edit { + muted = true + this.reason = reason + } + } + + if (time != null) { + if (timeouts.closed) { + logger.warn("Timeouts microservice has not been established (or not connected)") + return + } + + timeouts.send( + RequestCommand( + Timeout( + guildId = guild.id.toString(), + userId = member.id.toString(), + issuedAt = System.currentTimeMillis(), + expiresIn = time.toLong(), + moderatorId = moderator.id.toString(), + reason = reason, + type = PunishmentType.VOICE_UNMUTE.key + ) + ) + ) + } + } + + private suspend fun applyVoiceDeafen( + moderator: User, + reason: String?, + member: Member, + guild: Guild, + time: Int? = null + ) { + val voiceState = member.getVoiceState() + if (voiceState.channelId != null && !voiceState.isDeafened) { + member.edit { + deafened = true + this.reason = reason + } + } + + if (time != null) { + if (timeouts.closed) { + logger.warn("Timeouts microservice has not been established (or not connected)") + return + } + + timeouts.send( + RequestCommand( + Timeout( + guildId = guild.id.toString(), + userId = member.id.toString(), + issuedAt = System.currentTimeMillis(), + expiresIn = time.toLong(), + moderatorId = moderator.id.toString(), + reason = reason, + type = PunishmentType.VOICE_UNMUTE.key + ) + ) + ) + } + } + + private suspend fun applyVoiceUnmute( + member: Member, + reason: String? + ) { + val voiceState = member.getVoiceState() + if (voiceState.channelId != null && !voiceState.isDeafened) { + member.edit { + muted = false + this.reason = reason + } + } + } + + private suspend fun applyVoiceUndeafen( + member: Member, + reason: String? + ) { + val voiceState = member.getVoiceState() + if (voiceState.channelId != null && !voiceState.isDeafened) { + member.edit { + deafened = false + this.reason = reason + } + } + } + + private fun getModlogMessage(caseId: Int, data: PublishModLogData): EmbedBuilder = EmbedBuilder().apply { + color = COLOR + author { + name = "[ Case #$caseId | ${data.type.asEmoji} ${data.type.key} ]" + icon = data.victim.avatar?.url + } + + description = buildString { + if (data.reason != null) { + appendLine("• ${data.reason}") + } else { + appendLine("• No reason was specified, edit it using `reason $caseId `") + } + + if (data.attachments.isNotEmpty()) { + appendLine() + for ((i, attachment) in data.attachments.withIndex()) { + appendLine("• [**#$i**](${attachment.url})") + } + } + } + + field { + name = "• Victim" + value = "${data.victim.tag} (**${data.victim.id}**)" + } + + field { + name = "• Moderator" + value = "${data.moderator.tag} (**${data.moderator.id}**)" + } + + if (data.time != null) { + val verboseTime = ms.fromLong(data.time.toLong(), true) + field { + name = "• Time" + value = verboseTime + inline = true + } + } + + if (data.warningsRemoved != null) { + field { + name = "• Warnings Removed" + inline = true + value = if (data.warningsRemoved == 1) + "All" + else + "${data.warningsRemoved}" + } + } + + if (data.warningsAdded != null) { + field { + name = "• Warnings Added" + inline = true + value = "${data.warningsAdded}" + } + } + } + + private fun getModlogPlainText(caseId: Int, data: PublishModLogData): String = buildString { + appendLine("**[** Case #**$caseId** | ${data.type.asEmoji} **${data.type.key}** **]**") + appendLine() + appendLine("> **Victim**: ${data.victim.tag} (**${data.victim.id}**)") + appendLine("> **Moderator**: ${data.moderator.tag} (**${data.moderator.id}**)") + appendLine("> **Reason**: ${data.reason ?: "No reason was specified, edit it using `reason $caseId `"}") + + if (data.time != null) { + val verboseTime = ms.fromLong(data.time.toLong()) + appendLine("> :watch: **Time**: $verboseTime") + } + + if (data.warningsAdded != null) { + appendLine("> **Warnings Added**: ${data.warningsAdded}") + } + + if (data.warningsRemoved != null) { + appendLine("> **Warnings Removed**: ${if (data.warningsRemoved == -1) "All" else data.warningsAdded}") + } + } +} diff --git a/bot/slash-commands/build.gradle.kts b/bot/slash-commands/build.gradle.kts new file mode 100644 index 00000000..f7037bc8 --- /dev/null +++ b/bot/slash-commands/build.gradle.kts @@ -0,0 +1,31 @@ +/** + * Copyright (c) 2019-2022 Nino + * + * 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. + */ + +plugins { + `nino-module` +} + +dependencies { + implementation(project(":bot:automod")) + implementation(project(":bot:database")) + implementation(project(":bot:core")) +} diff --git a/bot/slash-commands/src/main/kotlin/sh/nino/discord/slash/commands/AbstractSlashCommand.kt b/bot/slash-commands/src/main/kotlin/sh/nino/discord/slash/commands/AbstractSlashCommand.kt new file mode 100644 index 00000000..4baa9a28 --- /dev/null +++ b/bot/slash-commands/src/main/kotlin/sh/nino/discord/slash/commands/AbstractSlashCommand.kt @@ -0,0 +1,25 @@ +/* + * Copyright (c) 2019-2022 Nino + * + * 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. + */ + +package sh.nino.discord.slash.commands + +abstract class AbstractSlashCommand diff --git a/bot/slash-commands/src/main/kotlin/sh/nino/discord/slash/commands/SlashCommand.kt b/bot/slash-commands/src/main/kotlin/sh/nino/discord/slash/commands/SlashCommand.kt new file mode 100644 index 00000000..7c5ba941 --- /dev/null +++ b/bot/slash-commands/src/main/kotlin/sh/nino/discord/slash/commands/SlashCommand.kt @@ -0,0 +1,25 @@ +/* + * Copyright (c) 2019-2022 Nino + * + * 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. + */ + +package sh.nino.discord.slash.commands + +class SlashCommand diff --git a/bot/slash-commands/src/main/kotlin/sh/nino/discord/slash/commands/SlashCommandHandler.kt b/bot/slash-commands/src/main/kotlin/sh/nino/discord/slash/commands/SlashCommandHandler.kt new file mode 100644 index 00000000..fdbe0c61 --- /dev/null +++ b/bot/slash-commands/src/main/kotlin/sh/nino/discord/slash/commands/SlashCommandHandler.kt @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2019-2022 Nino + * + * 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. + */ + +package sh.nino.discord.slash.commands + +import dev.kord.core.Kord +import sh.nino.discord.common.data.Config + +class SlashCommandHandler( + private val config: Config, + private val kord: Kord +) diff --git a/bot/slash-commands/src/main/kotlin/sh/nino/discord/slash/commands/SlashCommandMessage.kt b/bot/slash-commands/src/main/kotlin/sh/nino/discord/slash/commands/SlashCommandMessage.kt new file mode 100644 index 00000000..0e61c7d7 --- /dev/null +++ b/bot/slash-commands/src/main/kotlin/sh/nino/discord/slash/commands/SlashCommandMessage.kt @@ -0,0 +1,162 @@ +/* + * Copyright (c) 2019-2022 Nino + * + * 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. + */ + +package sh.nino.discord.slash.commands + +import dev.kord.common.entity.AllowedMentions +import dev.kord.common.entity.InteractionResponseType +import dev.kord.common.entity.MessageFlag +import dev.kord.common.entity.MessageFlags +import dev.kord.common.entity.optional.Optional +import dev.kord.common.entity.optional.OptionalBoolean +import dev.kord.common.entity.optional.optional +import dev.kord.core.Kord +import dev.kord.core.entity.Guild +import dev.kord.core.entity.User +import dev.kord.core.entity.channel.TextChannel +import dev.kord.core.event.interaction.InteractionCreateEvent +import dev.kord.rest.builder.message.EmbedBuilder +import dev.kord.rest.json.request.FollowupMessageCreateRequest +import dev.kord.rest.json.request.InteractionApplicationCommandCallbackData +import dev.kord.rest.json.request.InteractionResponseCreateRequest +import dev.kord.rest.json.request.MultipartFollowupMessageCreateRequest +import sh.nino.discord.common.COLOR +import sh.nino.discord.core.localization.Locale +import sh.nino.discord.core.messaging.PaginationEmbed +import sh.nino.discord.database.tables.GuildSettingsEntity +import sh.nino.discord.database.tables.UserEntity +import java.lang.IllegalArgumentException + +class SlashCommandArguments(private val args: Map, Any>) { + operator fun get(key: CommandOption<*>): T { + if (!args.containsKey(key) || key.type is CommandOptionType.Nullable) + throw IllegalArgumentException("Missing key in args: ${key.name} or is null.") + + return args[key] as T + } + + fun getNull(key: CommandOption<*>): T? { + if (!args.containsKey(key)) + return null + + return args[key] as T + } +} + +class SlashCommandMessage( + private val event: InteractionCreateEvent, + private val kord: Kord, + val args: SlashCommandArguments, + val settings: GuildSettingsEntity, + val userSettings: UserEntity, + val locale: Locale, + val author: User, + val guild: Guild +) { + /** + * Creates a new [PaginationEmbed] for showing off more than one embed to the user. + * @param embeds A list of embeds to show. + */ + suspend fun createPaginationEmbed(embeds: List): PaginationEmbed { + val channel = event.interaction.channel.asChannel() as TextChannel + return PaginationEmbed(channel, author, embeds) + } + + suspend fun defer() { + kord.rest.interaction.createInteractionResponse( + event.interaction.id, event.interaction.token, + InteractionResponseCreateRequest( + InteractionResponseType.ChannelMessageWithSource, + InteractionApplicationCommandCallbackData().optional() + ) + ) + } + + suspend fun deferEphermerally() { + kord.rest.interaction.createInteractionResponse( + event.interaction.id, event.interaction.token, + InteractionResponseCreateRequest( + InteractionResponseType.ChannelMessageWithSource, + InteractionApplicationCommandCallbackData( + flags = Optional.invoke( + MessageFlags { + +MessageFlag.Ephemeral + } + ) + ).optional() + ) + ) + } + + suspend fun reply(content: String, embed: EmbedBuilder.() -> Unit) { + val builder = EmbedBuilder().apply(embed) + builder.color = COLOR + + kord.rest.interaction.createFollowupMessage( + kord.selfId, event.interaction.token, + MultipartFollowupMessageCreateRequest( + FollowupMessageCreateRequest( + content = Optional.invoke(content), + embeds = Optional.invoke(listOf(builder.toRequest())) + ) + ) + ) + } + + suspend fun reply(embed: EmbedBuilder.() -> Unit) { + val builder = EmbedBuilder().apply(embed) + builder.color = COLOR + + kord.rest.interaction.createFollowupMessage( + kord.selfId, event.interaction.token, + MultipartFollowupMessageCreateRequest( + FollowupMessageCreateRequest( + embeds = Optional.invoke(listOf(builder.toRequest())) + ) + ) + ) + } + + suspend fun reply(content: String, ephemeral: Boolean = false) { + kord.rest.interaction.createFollowupMessage( + kord.selfId, event.interaction.token, + MultipartFollowupMessageCreateRequest( + FollowupMessageCreateRequest( + content = Optional.invoke(content), + flags = if (ephemeral) Optional.invoke( + MessageFlags { + +MessageFlag.Ephemeral + } + ) else Optional.Missing(), + allowedMentions = Optional.invoke( + AllowedMentions( + listOf(), + listOf(), + listOf(), + OptionalBoolean.Value(false) + ) + ) + ) + ) + ) + } +} diff --git a/bot/slash-commands/src/main/kotlin/sh/nino/discord/slash/commands/SlashSubcommand.kt b/bot/slash-commands/src/main/kotlin/sh/nino/discord/slash/commands/SlashSubcommand.kt new file mode 100644 index 00000000..0426fa62 --- /dev/null +++ b/bot/slash-commands/src/main/kotlin/sh/nino/discord/slash/commands/SlashSubcommand.kt @@ -0,0 +1,25 @@ +/* + * Copyright (c) 2019-2022 Nino + * + * 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. + */ + +package sh.nino.discord.slash.commands + +class SlashSubcommand diff --git a/bot/slash-commands/src/main/kotlin/sh/nino/discord/slash/commands/SlashSubcommandGroup.kt b/bot/slash-commands/src/main/kotlin/sh/nino/discord/slash/commands/SlashSubcommandGroup.kt new file mode 100644 index 00000000..c727ca3a --- /dev/null +++ b/bot/slash-commands/src/main/kotlin/sh/nino/discord/slash/commands/SlashSubcommandGroup.kt @@ -0,0 +1,25 @@ +/* + * Copyright (c) 2019-2022 Nino + * + * 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. + */ + +package sh.nino.discord.slash.commands + +class SlashSubcommandGroup diff --git a/bot/slash-commands/src/main/kotlin/sh/nino/discord/slash/commands/_Module.kt b/bot/slash-commands/src/main/kotlin/sh/nino/discord/slash/commands/_Module.kt new file mode 100644 index 00000000..0c240069 --- /dev/null +++ b/bot/slash-commands/src/main/kotlin/sh/nino/discord/slash/commands/_Module.kt @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2019-2022 Nino + * + * 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. + */ + +package sh.nino.discord.slash.commands + +import org.koin.dsl.module +import sh.nino.discord.slash.commands.admin.adminSlashCommandsModule +import sh.nino.discord.slash.commands.core.coreSlashCommandsModule +import sh.nino.discord.slash.commands.moderation.moderationSlashCommandsModule +import sh.nino.discord.slash.commands.threads.threadsSlashCommandModule +import sh.nino.discord.slash.commands.util.utilSlashCommandsModule +import sh.nino.discord.slash.commands.voice.voiceSlashCommandsModule + +val slashCommandsModule = adminSlashCommandsModule + + coreSlashCommandsModule + + moderationSlashCommandsModule + + threadsSlashCommandModule + + utilSlashCommandsModule + + voiceSlashCommandsModule + + module { + } diff --git a/bot/slash-commands/src/main/kotlin/sh/nino/discord/slash/commands/_Options.kt b/bot/slash-commands/src/main/kotlin/sh/nino/discord/slash/commands/_Options.kt new file mode 100644 index 00000000..13525169 --- /dev/null +++ b/bot/slash-commands/src/main/kotlin/sh/nino/discord/slash/commands/_Options.kt @@ -0,0 +1,263 @@ +/* + * Copyright (c) 2019-2022 Nino + * + * 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. + */ + +@file:JvmName("NinoSlashCommandOptionsKt") +package sh.nino.discord.slash.commands + +import dev.kord.common.entity.ApplicationCommandOptionType +import kotlin.reflect.KClass + +interface CommandOptionType { + val nullable: Boolean + + abstract class Nullable: CommandOptionType { + override val nullable: Boolean = true + } + + interface NullableObject { + fun toNull(): Nullable + } + + object String: CommandOptionType, NullableObject { + override val nullable: Boolean = false + override fun toNull(): Nullable = OptionalString + } + + object OptionalString: Nullable() + + object Integer: CommandOptionType, NullableObject { + override val nullable: Boolean = false + override fun toNull(): Nullable = OptionalInt + } + + object OptionalInt: Nullable() + + object Number: CommandOptionType, NullableObject { + override val nullable: Boolean = false + override fun toNull(): Nullable = OptionalNumber + } + + object OptionalNumber: Nullable() + + object Bool: CommandOptionType, NullableObject { + override val nullable: Boolean = true + override fun toNull(): Nullable = OptionalBool + } + + object OptionalBool: Nullable() + + object User: CommandOptionType, NullableObject { + override val nullable: Boolean = false + override fun toNull(): Nullable = OptionalUser + } + + object OptionalUser: Nullable() + + object Channel: CommandOptionType, NullableObject { + override val nullable: Boolean = false + override fun toNull(): Nullable = OptionalChannel + } + + object OptionalChannel: Nullable() + + object Mentionable: CommandOptionType, NullableObject { + override val nullable: Boolean = false + override fun toNull(): Nullable = OptionalMentionable + } + + object OptionalMentionable: Nullable() + + object Role: CommandOptionType, NullableObject { + override val nullable: Boolean = false + override fun toNull(): Nullable = OptionalRole + } + + object OptionalRole: Nullable() +} + +fun CommandOptionType.asKordType(): ApplicationCommandOptionType = when (this) { + is CommandOptionType.String, CommandOptionType.OptionalString -> ApplicationCommandOptionType.String + is CommandOptionType.Integer, CommandOptionType.OptionalInt -> ApplicationCommandOptionType.Integer + is CommandOptionType.Number, CommandOptionType.OptionalNumber -> ApplicationCommandOptionType.Number + is CommandOptionType.Bool, CommandOptionType.OptionalBool -> ApplicationCommandOptionType.Boolean + is CommandOptionType.User, CommandOptionType.OptionalUser -> ApplicationCommandOptionType.User + is CommandOptionType.Channel, CommandOptionType.OptionalChannel -> ApplicationCommandOptionType.Channel + is CommandOptionType.Mentionable, CommandOptionType.OptionalMentionable -> ApplicationCommandOptionType.Mentionable + is CommandOptionType.Role, CommandOptionType.OptionalRole -> ApplicationCommandOptionType.Role + else -> error("Unknown option type ${this::class}") +} + +class CommandOption( + val name: String, + val description: String, + val type: CommandOptionType, + val typeClass: KClass<*>, + val choices: List>? = null, + val required: Boolean = true +) + +class CommandOptionBuilder( + val name: String, + val description: String, + val type: CommandOptionType, + var choices: MutableList>? = null, + var required: Boolean = true +) { + fun optional(): CommandOptionBuilder { + required = false + return this + } + + fun choice(name: String, value: T): CommandOptionBuilder { + if (choices == null) + choices = mutableListOf() + + choices!!.add(Pair(name, value)) + return this + } +} + +class CommandOptions { + companion object { + val None: CommandOptions = CommandOptions() + } + + val args = mutableListOf>() + private inline fun asBuilder(name: String, description: String, type: CommandOptionType): CommandOptionBuilder = CommandOptionBuilder( + name, + description, + type + ) + + inline fun CommandOptionBuilder.register(): CommandOption { + if (args.any { it.name == this.name }) + throw IllegalStateException("Command option $name already exists.") + + val option = CommandOption( + this.name, + this.description, + this.type, + T::class, + this.choices ?: listOf(), + this.required + ) + + args.add(option) + return option + } + + fun string(name: String, description: String): CommandOptionBuilder = asBuilder( + name, + description, + CommandOptionType.String + ) + + fun bool(name: String, description: String): CommandOptionBuilder = asBuilder( + name, + description, + CommandOptionType.Bool + ) + + fun number(name: String, description: String): CommandOptionBuilder = asBuilder( + name, + description, + CommandOptionType.Number + ) + + fun integer(name: String, description: String): CommandOptionBuilder = asBuilder( + name, + description, + CommandOptionType.Integer + ) + + fun user(name: String, description: String): CommandOptionBuilder = asBuilder( + name, + description, + CommandOptionType.User + ) + + fun role(name: String, description: String): CommandOptionBuilder = asBuilder( + name, + description, + CommandOptionType.Role + ) + + fun channel(name: String, description: String): CommandOptionBuilder = asBuilder( + name, + description, + CommandOptionType.Channel + ) + + fun mentionable(name: String, description: String): CommandOptionBuilder = asBuilder( + name, + description, + CommandOptionType.Mentionable + ) + + fun optionalString(name: String, description: String): CommandOptionBuilder = asBuilder( + name, + description, + CommandOptionType.OptionalString + ) + + fun optionalBool(name: String, description: String): CommandOptionBuilder = asBuilder( + name, + description, + CommandOptionType.OptionalBool + ) + + fun optionalNumber(name: String, description: String): CommandOptionBuilder = asBuilder( + name, + description, + CommandOptionType.OptionalNumber + ) + + fun optionalInt(name: String, description: String): CommandOptionBuilder = asBuilder( + name, + description, + CommandOptionType.OptionalInt + ) + + fun optionalUser(name: String, description: String): CommandOptionBuilder = asBuilder( + name, + description, + CommandOptionType.OptionalUser + ) + + fun optionalRole(name: String, description: String): CommandOptionBuilder = asBuilder( + name, + description, + CommandOptionType.OptionalRole + ) + + fun optionalChannel(name: String, description: String): CommandOptionBuilder = asBuilder( + name, + description, + CommandOptionType.OptionalChannel + ) + + fun optionalMentionable(name: String, description: String): CommandOptionBuilder = asBuilder( + name, + description, + CommandOptionType.OptionalMentionable + ) +} diff --git a/bot/slash-commands/src/main/kotlin/sh/nino/discord/slash/commands/admin/AutomodCommand.kt b/bot/slash-commands/src/main/kotlin/sh/nino/discord/slash/commands/admin/AutomodCommand.kt new file mode 100644 index 00000000..58e3cc2a --- /dev/null +++ b/bot/slash-commands/src/main/kotlin/sh/nino/discord/slash/commands/admin/AutomodCommand.kt @@ -0,0 +1,23 @@ +/* + * Copyright (c) 2019-2022 Nino + * + * 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. + */ + +package sh.nino.discord.slash.commands.admin diff --git a/bot/slash-commands/src/main/kotlin/sh/nino/discord/slash/commands/admin/ExportCommand.kt b/bot/slash-commands/src/main/kotlin/sh/nino/discord/slash/commands/admin/ExportCommand.kt new file mode 100644 index 00000000..58e3cc2a --- /dev/null +++ b/bot/slash-commands/src/main/kotlin/sh/nino/discord/slash/commands/admin/ExportCommand.kt @@ -0,0 +1,23 @@ +/* + * Copyright (c) 2019-2022 Nino + * + * 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. + */ + +package sh.nino.discord.slash.commands.admin diff --git a/bot/slash-commands/src/main/kotlin/sh/nino/discord/slash/commands/admin/ImportCommand.kt b/bot/slash-commands/src/main/kotlin/sh/nino/discord/slash/commands/admin/ImportCommand.kt new file mode 100644 index 00000000..58e3cc2a --- /dev/null +++ b/bot/slash-commands/src/main/kotlin/sh/nino/discord/slash/commands/admin/ImportCommand.kt @@ -0,0 +1,23 @@ +/* + * Copyright (c) 2019-2022 Nino + * + * 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. + */ + +package sh.nino.discord.slash.commands.admin diff --git a/bot/slash-commands/src/main/kotlin/sh/nino/discord/slash/commands/admin/LoggingCommand.kt b/bot/slash-commands/src/main/kotlin/sh/nino/discord/slash/commands/admin/LoggingCommand.kt new file mode 100644 index 00000000..58e3cc2a --- /dev/null +++ b/bot/slash-commands/src/main/kotlin/sh/nino/discord/slash/commands/admin/LoggingCommand.kt @@ -0,0 +1,23 @@ +/* + * Copyright (c) 2019-2022 Nino + * + * 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. + */ + +package sh.nino.discord.slash.commands.admin diff --git a/bot/slash-commands/src/main/kotlin/sh/nino/discord/slash/commands/admin/RoleConfigCommand.kt b/bot/slash-commands/src/main/kotlin/sh/nino/discord/slash/commands/admin/RoleConfigCommand.kt new file mode 100644 index 00000000..58e3cc2a --- /dev/null +++ b/bot/slash-commands/src/main/kotlin/sh/nino/discord/slash/commands/admin/RoleConfigCommand.kt @@ -0,0 +1,23 @@ +/* + * Copyright (c) 2019-2022 Nino + * + * 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. + */ + +package sh.nino.discord.slash.commands.admin diff --git a/bot/slash-commands/src/main/kotlin/sh/nino/discord/slash/commands/admin/_Module.kt b/bot/slash-commands/src/main/kotlin/sh/nino/discord/slash/commands/admin/_Module.kt new file mode 100644 index 00000000..d66cdbe3 --- /dev/null +++ b/bot/slash-commands/src/main/kotlin/sh/nino/discord/slash/commands/admin/_Module.kt @@ -0,0 +1,27 @@ +/* + * Copyright (c) 2019-2022 Nino + * + * 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. + */ + +package sh.nino.discord.slash.commands.admin + +import org.koin.dsl.module + +val adminSlashCommandsModule = module {} diff --git a/bot/slash-commands/src/main/kotlin/sh/nino/discord/slash/commands/annotations/SlashCommandInfo.kt b/bot/slash-commands/src/main/kotlin/sh/nino/discord/slash/commands/annotations/SlashCommandInfo.kt new file mode 100644 index 00000000..ce8ce013 --- /dev/null +++ b/bot/slash-commands/src/main/kotlin/sh/nino/discord/slash/commands/annotations/SlashCommandInfo.kt @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2019-2022 Nino + * + * 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. + */ + +package sh.nino.discord.slash.commands.annotations + +/** + * Represents the base information of a slash command that will be + * registered. + * + * @param name The name of the slash command. Must be 1-32 characters. + * @param description The slash command description when the pop out window appears. + * @param onlyIn A list of guild IDs that this slash command will be registered in. If this + * array is empty, it will be a global slash command, not a guild command. + * @param defaultPermission whether the command is enabled by default when the app is added to a guild + */ +annotation class SlashCommandInfo( + val name: String, + val description: String, + val onlyIn: LongArray = [], + val defaultPermission: Boolean = true +) diff --git a/bot/slash-commands/src/main/kotlin/sh/nino/discord/slash/commands/annotations/Subcommand.kt b/bot/slash-commands/src/main/kotlin/sh/nino/discord/slash/commands/annotations/Subcommand.kt new file mode 100644 index 00000000..9ceec6cb --- /dev/null +++ b/bot/slash-commands/src/main/kotlin/sh/nino/discord/slash/commands/annotations/Subcommand.kt @@ -0,0 +1,53 @@ +/* + * Copyright (c) 2019-2022 Nino + * + * 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. + */ + +package sh.nino.discord.slash.commands.annotations + +/** + * Represents a slash subcommand that is registered to an [AbstractSlashCommand][sh.nino.discord.slash.commands.AbstractSlashCommand]. + * If this subcommand should belong in a group, refer the [groupId] to chain it to that slash command. + * + * @param name The subcommand's name. Must be 1-32 characters. + * @param description The subcommand's description. Must be 1-100 characters. + * @param groupId An optional subcommand group to chain this subcommand to that group. + * + * ## Example + * ```kt + * @SlashCommandInfo("uwu", "uwu command!!!!") + * class MySlashCommand: AbstractSlashCommand() { + * @Subcommand("owo", "Owos x amount of times.") + * suspend fun owo( + * msg: SlashSubcommandMessage, + * @Option("amount", "how many times to owo", type = Int::class) amount: Int + * ) { + * msg.reply("owo ".repeat(amount)) + * } + * + * // /uwu owo will be registered to Discord. + * } + * ``` + */ +annotation class Subcommand( + val name: String, + val description: String, + val groupId: String = "" +) diff --git a/bot/slash-commands/src/main/kotlin/sh/nino/discord/slash/commands/core/AboutCommand.kt b/bot/slash-commands/src/main/kotlin/sh/nino/discord/slash/commands/core/AboutCommand.kt new file mode 100644 index 00000000..ba84ae46 --- /dev/null +++ b/bot/slash-commands/src/main/kotlin/sh/nino/discord/slash/commands/core/AboutCommand.kt @@ -0,0 +1,23 @@ +/* + * Copyright (c) 2019-2022 Nino + * + * 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. + */ + +package sh.nino.discord.slash.commands.core diff --git a/bot/slash-commands/src/main/kotlin/sh/nino/discord/slash/commands/core/HelpCommand.kt b/bot/slash-commands/src/main/kotlin/sh/nino/discord/slash/commands/core/HelpCommand.kt new file mode 100644 index 00000000..ba84ae46 --- /dev/null +++ b/bot/slash-commands/src/main/kotlin/sh/nino/discord/slash/commands/core/HelpCommand.kt @@ -0,0 +1,23 @@ +/* + * Copyright (c) 2019-2022 Nino + * + * 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. + */ + +package sh.nino.discord.slash.commands.core diff --git a/bot/slash-commands/src/main/kotlin/sh/nino/discord/slash/commands/core/InviteMeCommand.kt b/bot/slash-commands/src/main/kotlin/sh/nino/discord/slash/commands/core/InviteMeCommand.kt new file mode 100644 index 00000000..ba84ae46 --- /dev/null +++ b/bot/slash-commands/src/main/kotlin/sh/nino/discord/slash/commands/core/InviteMeCommand.kt @@ -0,0 +1,23 @@ +/* + * Copyright (c) 2019-2022 Nino + * + * 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. + */ + +package sh.nino.discord.slash.commands.core diff --git a/bot/slash-commands/src/main/kotlin/sh/nino/discord/slash/commands/core/PingCommand.kt b/bot/slash-commands/src/main/kotlin/sh/nino/discord/slash/commands/core/PingCommand.kt new file mode 100644 index 00000000..ba84ae46 --- /dev/null +++ b/bot/slash-commands/src/main/kotlin/sh/nino/discord/slash/commands/core/PingCommand.kt @@ -0,0 +1,23 @@ +/* + * Copyright (c) 2019-2022 Nino + * + * 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. + */ + +package sh.nino.discord.slash.commands.core diff --git a/bot/slash-commands/src/main/kotlin/sh/nino/discord/slash/commands/core/ShardInfoCommand.kt b/bot/slash-commands/src/main/kotlin/sh/nino/discord/slash/commands/core/ShardInfoCommand.kt new file mode 100644 index 00000000..ba84ae46 --- /dev/null +++ b/bot/slash-commands/src/main/kotlin/sh/nino/discord/slash/commands/core/ShardInfoCommand.kt @@ -0,0 +1,23 @@ +/* + * Copyright (c) 2019-2022 Nino + * + * 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. + */ + +package sh.nino.discord.slash.commands.core diff --git a/bot/slash-commands/src/main/kotlin/sh/nino/discord/slash/commands/core/StatisticsCommand.kt b/bot/slash-commands/src/main/kotlin/sh/nino/discord/slash/commands/core/StatisticsCommand.kt new file mode 100644 index 00000000..ba84ae46 --- /dev/null +++ b/bot/slash-commands/src/main/kotlin/sh/nino/discord/slash/commands/core/StatisticsCommand.kt @@ -0,0 +1,23 @@ +/* + * Copyright (c) 2019-2022 Nino + * + * 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. + */ + +package sh.nino.discord.slash.commands.core diff --git a/bot/slash-commands/src/main/kotlin/sh/nino/discord/slash/commands/core/UptimeCommand.kt b/bot/slash-commands/src/main/kotlin/sh/nino/discord/slash/commands/core/UptimeCommand.kt new file mode 100644 index 00000000..ba84ae46 --- /dev/null +++ b/bot/slash-commands/src/main/kotlin/sh/nino/discord/slash/commands/core/UptimeCommand.kt @@ -0,0 +1,23 @@ +/* + * Copyright (c) 2019-2022 Nino + * + * 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. + */ + +package sh.nino.discord.slash.commands.core diff --git a/bot/slash-commands/src/main/kotlin/sh/nino/discord/slash/commands/core/_Module.kt b/bot/slash-commands/src/main/kotlin/sh/nino/discord/slash/commands/core/_Module.kt new file mode 100644 index 00000000..61cee9a4 --- /dev/null +++ b/bot/slash-commands/src/main/kotlin/sh/nino/discord/slash/commands/core/_Module.kt @@ -0,0 +1,27 @@ +/* + * Copyright (c) 2019-2022 Nino + * + * 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. + */ + +package sh.nino.discord.slash.commands.core + +import org.koin.dsl.module + +val coreSlashCommandsModule = module {} diff --git a/bot/slash-commands/src/main/kotlin/sh/nino/discord/slash/commands/easter_egg/TestCommand.kt b/bot/slash-commands/src/main/kotlin/sh/nino/discord/slash/commands/easter_egg/TestCommand.kt new file mode 100644 index 00000000..fcf374b6 --- /dev/null +++ b/bot/slash-commands/src/main/kotlin/sh/nino/discord/slash/commands/easter_egg/TestCommand.kt @@ -0,0 +1,23 @@ +/* + * Copyright (c) 2019-2022 Nino + * + * 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. + */ + +package sh.nino.discord.slash.commands.easter_egg diff --git a/bot/slash-commands/src/main/kotlin/sh/nino/discord/slash/commands/easter_egg/WahCommand.kt b/bot/slash-commands/src/main/kotlin/sh/nino/discord/slash/commands/easter_egg/WahCommand.kt new file mode 100644 index 00000000..fcf374b6 --- /dev/null +++ b/bot/slash-commands/src/main/kotlin/sh/nino/discord/slash/commands/easter_egg/WahCommand.kt @@ -0,0 +1,23 @@ +/* + * Copyright (c) 2019-2022 Nino + * + * 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. + */ + +package sh.nino.discord.slash.commands.easter_egg diff --git a/bot/slash-commands/src/main/kotlin/sh/nino/discord/slash/commands/easter_egg/_Module.kt b/bot/slash-commands/src/main/kotlin/sh/nino/discord/slash/commands/easter_egg/_Module.kt new file mode 100644 index 00000000..d5e035d1 --- /dev/null +++ b/bot/slash-commands/src/main/kotlin/sh/nino/discord/slash/commands/easter_egg/_Module.kt @@ -0,0 +1,27 @@ +/* + * Copyright (c) 2019-2022 Nino + * + * 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. + */ + +package sh.nino.discord.slash.commands.easter_egg + +import org.koin.dsl.module + +val easterEggSlashCommandModule = module {} diff --git a/bot/slash-commands/src/main/kotlin/sh/nino/discord/slash/commands/moderation/BanCommand.kt b/bot/slash-commands/src/main/kotlin/sh/nino/discord/slash/commands/moderation/BanCommand.kt new file mode 100644 index 00000000..382b6ac8 --- /dev/null +++ b/bot/slash-commands/src/main/kotlin/sh/nino/discord/slash/commands/moderation/BanCommand.kt @@ -0,0 +1,23 @@ +/* + * Copyright (c) 2019-2022 Nino + * + * 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. + */ + +package sh.nino.discord.slash.commands.moderation diff --git a/bot/slash-commands/src/main/kotlin/sh/nino/discord/slash/commands/moderation/CaseCommand.kt b/bot/slash-commands/src/main/kotlin/sh/nino/discord/slash/commands/moderation/CaseCommand.kt new file mode 100644 index 00000000..382b6ac8 --- /dev/null +++ b/bot/slash-commands/src/main/kotlin/sh/nino/discord/slash/commands/moderation/CaseCommand.kt @@ -0,0 +1,23 @@ +/* + * Copyright (c) 2019-2022 Nino + * + * 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. + */ + +package sh.nino.discord.slash.commands.moderation diff --git a/bot/slash-commands/src/main/kotlin/sh/nino/discord/slash/commands/moderation/HistoryCommand.kt b/bot/slash-commands/src/main/kotlin/sh/nino/discord/slash/commands/moderation/HistoryCommand.kt new file mode 100644 index 00000000..382b6ac8 --- /dev/null +++ b/bot/slash-commands/src/main/kotlin/sh/nino/discord/slash/commands/moderation/HistoryCommand.kt @@ -0,0 +1,23 @@ +/* + * Copyright (c) 2019-2022 Nino + * + * 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. + */ + +package sh.nino.discord.slash.commands.moderation diff --git a/bot/slash-commands/src/main/kotlin/sh/nino/discord/slash/commands/moderation/KickCommand.kt b/bot/slash-commands/src/main/kotlin/sh/nino/discord/slash/commands/moderation/KickCommand.kt new file mode 100644 index 00000000..382b6ac8 --- /dev/null +++ b/bot/slash-commands/src/main/kotlin/sh/nino/discord/slash/commands/moderation/KickCommand.kt @@ -0,0 +1,23 @@ +/* + * Copyright (c) 2019-2022 Nino + * + * 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. + */ + +package sh.nino.discord.slash.commands.moderation diff --git a/bot/slash-commands/src/main/kotlin/sh/nino/discord/slash/commands/moderation/MuteCommand.kt b/bot/slash-commands/src/main/kotlin/sh/nino/discord/slash/commands/moderation/MuteCommand.kt new file mode 100644 index 00000000..382b6ac8 --- /dev/null +++ b/bot/slash-commands/src/main/kotlin/sh/nino/discord/slash/commands/moderation/MuteCommand.kt @@ -0,0 +1,23 @@ +/* + * Copyright (c) 2019-2022 Nino + * + * 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. + */ + +package sh.nino.discord.slash.commands.moderation diff --git a/bot/slash-commands/src/main/kotlin/sh/nino/discord/slash/commands/moderation/PardonCommand.kt b/bot/slash-commands/src/main/kotlin/sh/nino/discord/slash/commands/moderation/PardonCommand.kt new file mode 100644 index 00000000..382b6ac8 --- /dev/null +++ b/bot/slash-commands/src/main/kotlin/sh/nino/discord/slash/commands/moderation/PardonCommand.kt @@ -0,0 +1,23 @@ +/* + * Copyright (c) 2019-2022 Nino + * + * 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. + */ + +package sh.nino.discord.slash.commands.moderation diff --git a/bot/slash-commands/src/main/kotlin/sh/nino/discord/slash/commands/moderation/UnmuteCommand.kt b/bot/slash-commands/src/main/kotlin/sh/nino/discord/slash/commands/moderation/UnmuteCommand.kt new file mode 100644 index 00000000..382b6ac8 --- /dev/null +++ b/bot/slash-commands/src/main/kotlin/sh/nino/discord/slash/commands/moderation/UnmuteCommand.kt @@ -0,0 +1,23 @@ +/* + * Copyright (c) 2019-2022 Nino + * + * 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. + */ + +package sh.nino.discord.slash.commands.moderation diff --git a/bot/slash-commands/src/main/kotlin/sh/nino/discord/slash/commands/moderation/WarnCommand.kt b/bot/slash-commands/src/main/kotlin/sh/nino/discord/slash/commands/moderation/WarnCommand.kt new file mode 100644 index 00000000..382b6ac8 --- /dev/null +++ b/bot/slash-commands/src/main/kotlin/sh/nino/discord/slash/commands/moderation/WarnCommand.kt @@ -0,0 +1,23 @@ +/* + * Copyright (c) 2019-2022 Nino + * + * 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. + */ + +package sh.nino.discord.slash.commands.moderation diff --git a/bot/slash-commands/src/main/kotlin/sh/nino/discord/slash/commands/moderation/_Module.kt b/bot/slash-commands/src/main/kotlin/sh/nino/discord/slash/commands/moderation/_Module.kt new file mode 100644 index 00000000..6327e93a --- /dev/null +++ b/bot/slash-commands/src/main/kotlin/sh/nino/discord/slash/commands/moderation/_Module.kt @@ -0,0 +1,27 @@ +/* + * Copyright (c) 2019-2022 Nino + * + * 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. + */ + +package sh.nino.discord.slash.commands.moderation + +import org.koin.dsl.module + +val moderationSlashCommandsModule = module {} diff --git a/bot/slash-commands/src/main/kotlin/sh/nino/discord/slash/commands/threads/AddThreadMessagePermsCommand.kt b/bot/slash-commands/src/main/kotlin/sh/nino/discord/slash/commands/threads/AddThreadMessagePermsCommand.kt new file mode 100644 index 00000000..67dbed2e --- /dev/null +++ b/bot/slash-commands/src/main/kotlin/sh/nino/discord/slash/commands/threads/AddThreadMessagePermsCommand.kt @@ -0,0 +1,23 @@ +/* + * Copyright (c) 2019-2022 Nino + * + * 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. + */ + +package sh.nino.discord.slash.commands.threads diff --git a/bot/slash-commands/src/main/kotlin/sh/nino/discord/slash/commands/threads/NoThreadMessagePermsCommand.kt b/bot/slash-commands/src/main/kotlin/sh/nino/discord/slash/commands/threads/NoThreadMessagePermsCommand.kt new file mode 100644 index 00000000..67dbed2e --- /dev/null +++ b/bot/slash-commands/src/main/kotlin/sh/nino/discord/slash/commands/threads/NoThreadMessagePermsCommand.kt @@ -0,0 +1,23 @@ +/* + * Copyright (c) 2019-2022 Nino + * + * 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. + */ + +package sh.nino.discord.slash.commands.threads diff --git a/bot/slash-commands/src/main/kotlin/sh/nino/discord/slash/commands/threads/_Module.kt b/bot/slash-commands/src/main/kotlin/sh/nino/discord/slash/commands/threads/_Module.kt new file mode 100644 index 00000000..2ac9639d --- /dev/null +++ b/bot/slash-commands/src/main/kotlin/sh/nino/discord/slash/commands/threads/_Module.kt @@ -0,0 +1,27 @@ +/* + * Copyright (c) 2019-2022 Nino + * + * 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. + */ + +package sh.nino.discord.slash.commands.threads + +import org.koin.dsl.module + +val threadsSlashCommandModule = module {} diff --git a/bot/slash-commands/src/main/kotlin/sh/nino/discord/slash/commands/util/ChannelInfoCommand.kt b/bot/slash-commands/src/main/kotlin/sh/nino/discord/slash/commands/util/ChannelInfoCommand.kt new file mode 100644 index 00000000..ea2f44aa --- /dev/null +++ b/bot/slash-commands/src/main/kotlin/sh/nino/discord/slash/commands/util/ChannelInfoCommand.kt @@ -0,0 +1,23 @@ +/* + * Copyright (c) 2019-2022 Nino + * + * 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. + */ + +package sh.nino.discord.slash.commands.util diff --git a/bot/slash-commands/src/main/kotlin/sh/nino/discord/slash/commands/util/ServerInfoCommand.kt b/bot/slash-commands/src/main/kotlin/sh/nino/discord/slash/commands/util/ServerInfoCommand.kt new file mode 100644 index 00000000..ea2f44aa --- /dev/null +++ b/bot/slash-commands/src/main/kotlin/sh/nino/discord/slash/commands/util/ServerInfoCommand.kt @@ -0,0 +1,23 @@ +/* + * Copyright (c) 2019-2022 Nino + * + * 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. + */ + +package sh.nino.discord.slash.commands.util diff --git a/bot/slash-commands/src/main/kotlin/sh/nino/discord/slash/commands/util/UserOrRoleInfoCommand.kt b/bot/slash-commands/src/main/kotlin/sh/nino/discord/slash/commands/util/UserOrRoleInfoCommand.kt new file mode 100644 index 00000000..ea2f44aa --- /dev/null +++ b/bot/slash-commands/src/main/kotlin/sh/nino/discord/slash/commands/util/UserOrRoleInfoCommand.kt @@ -0,0 +1,23 @@ +/* + * Copyright (c) 2019-2022 Nino + * + * 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. + */ + +package sh.nino.discord.slash.commands.util diff --git a/bot/slash-commands/src/main/kotlin/sh/nino/discord/slash/commands/util/_Module.kt b/bot/slash-commands/src/main/kotlin/sh/nino/discord/slash/commands/util/_Module.kt new file mode 100644 index 00000000..c5d978ba --- /dev/null +++ b/bot/slash-commands/src/main/kotlin/sh/nino/discord/slash/commands/util/_Module.kt @@ -0,0 +1,27 @@ +/* + * Copyright (c) 2019-2022 Nino + * + * 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. + */ + +package sh.nino.discord.slash.commands.util + +import org.koin.dsl.module + +val utilSlashCommandsModule = module {} diff --git a/bot/slash-commands/src/main/kotlin/sh/nino/discord/slash/commands/voice/VoiceDeafenCommand.kt b/bot/slash-commands/src/main/kotlin/sh/nino/discord/slash/commands/voice/VoiceDeafenCommand.kt new file mode 100644 index 00000000..0a356e2d --- /dev/null +++ b/bot/slash-commands/src/main/kotlin/sh/nino/discord/slash/commands/voice/VoiceDeafenCommand.kt @@ -0,0 +1,23 @@ +/* + * Copyright (c) 2019-2022 Nino + * + * 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. + */ + +package sh.nino.discord.slash.commands.voice diff --git a/bot/slash-commands/src/main/kotlin/sh/nino/discord/slash/commands/voice/VoiceKickBotsCommand.kt b/bot/slash-commands/src/main/kotlin/sh/nino/discord/slash/commands/voice/VoiceKickBotsCommand.kt new file mode 100644 index 00000000..0a356e2d --- /dev/null +++ b/bot/slash-commands/src/main/kotlin/sh/nino/discord/slash/commands/voice/VoiceKickBotsCommand.kt @@ -0,0 +1,23 @@ +/* + * Copyright (c) 2019-2022 Nino + * + * 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. + */ + +package sh.nino.discord.slash.commands.voice diff --git a/bot/slash-commands/src/main/kotlin/sh/nino/discord/slash/commands/voice/VoiceKickCommand.kt b/bot/slash-commands/src/main/kotlin/sh/nino/discord/slash/commands/voice/VoiceKickCommand.kt new file mode 100644 index 00000000..0a356e2d --- /dev/null +++ b/bot/slash-commands/src/main/kotlin/sh/nino/discord/slash/commands/voice/VoiceKickCommand.kt @@ -0,0 +1,23 @@ +/* + * Copyright (c) 2019-2022 Nino + * + * 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. + */ + +package sh.nino.discord.slash.commands.voice diff --git a/bot/slash-commands/src/main/kotlin/sh/nino/discord/slash/commands/voice/VoiceMuteCommand.kt b/bot/slash-commands/src/main/kotlin/sh/nino/discord/slash/commands/voice/VoiceMuteCommand.kt new file mode 100644 index 00000000..0a356e2d --- /dev/null +++ b/bot/slash-commands/src/main/kotlin/sh/nino/discord/slash/commands/voice/VoiceMuteCommand.kt @@ -0,0 +1,23 @@ +/* + * Copyright (c) 2019-2022 Nino + * + * 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. + */ + +package sh.nino.discord.slash.commands.voice diff --git a/bot/slash-commands/src/main/kotlin/sh/nino/discord/slash/commands/voice/VoiceUndeafenCommand.kt b/bot/slash-commands/src/main/kotlin/sh/nino/discord/slash/commands/voice/VoiceUndeafenCommand.kt new file mode 100644 index 00000000..0a356e2d --- /dev/null +++ b/bot/slash-commands/src/main/kotlin/sh/nino/discord/slash/commands/voice/VoiceUndeafenCommand.kt @@ -0,0 +1,23 @@ +/* + * Copyright (c) 2019-2022 Nino + * + * 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. + */ + +package sh.nino.discord.slash.commands.voice diff --git a/bot/slash-commands/src/main/kotlin/sh/nino/discord/slash/commands/voice/_Module.kt b/bot/slash-commands/src/main/kotlin/sh/nino/discord/slash/commands/voice/_Module.kt new file mode 100644 index 00000000..f255b216 --- /dev/null +++ b/bot/slash-commands/src/main/kotlin/sh/nino/discord/slash/commands/voice/_Module.kt @@ -0,0 +1,27 @@ +/* + * Copyright (c) 2019-2022 Nino + * + * 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. + */ + +package sh.nino.discord.slash.commands.voice + +import org.koin.dsl.module + +val voiceSlashCommandsModule = module {} diff --git a/bot/src/main/kotlin/sh/nino/discord/Bootstrap.kt b/bot/src/main/kotlin/sh/nino/discord/Bootstrap.kt new file mode 100644 index 00000000..7a1695df --- /dev/null +++ b/bot/src/main/kotlin/sh/nino/discord/Bootstrap.kt @@ -0,0 +1,242 @@ +/* + * Copyright (c) 2019-2022 Nino + * + * 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. + */ + +package sh.nino.discord + +import com.charleskorn.kaml.Yaml +import com.zaxxer.hikari.HikariConfig +import com.zaxxer.hikari.HikariDataSource +import com.zaxxer.hikari.util.IsolationLevel +import dev.kord.cache.map.MapLikeCollection +import dev.kord.cache.map.internal.MapEntryCache +import dev.kord.core.Kord +import dev.kord.core.event.message.MessageCreateEvent +import dev.kord.core.on +import gay.floof.utils.slf4j.logging +import kotlinx.coroutines.cancel +import kotlinx.coroutines.launch +import kotlinx.coroutines.runBlocking +import org.jetbrains.exposed.sql.* +import org.jetbrains.exposed.sql.transactions.transaction +import org.koin.core.context.GlobalContext +import org.koin.core.context.startKoin +import org.koin.dsl.module +import sh.nino.discord.api.ApiServer +import sh.nino.discord.api.apiModule +import sh.nino.discord.commands.CommandHandler +import sh.nino.discord.commands.commandsModule +import sh.nino.discord.common.NinoInfo +import sh.nino.discord.common.data.Config +import sh.nino.discord.common.data.Environment +import sh.nino.discord.common.extensions.retrieve +import sh.nino.discord.core.NinoBot +import sh.nino.discord.core.NinoScope +import sh.nino.discord.core.globalModule +import sh.nino.discord.core.jobs.jobsModule +import sh.nino.discord.core.redis.RedisManager +import sh.nino.discord.database.createPgEnums +import sh.nino.discord.database.tables.* +import sh.nino.discord.punishments.punishmentsModule +import sh.nino.discord.timeouts.Client +import java.io.File +import kotlin.concurrent.thread +import kotlin.system.exitProcess + +object Bootstrap { + private val logger by logging() + + init { + addShutdownHook() + } + + @JvmStatic + fun main(args: Array) { + Thread.currentThread().name = "Nino-MainThread" + + val bannerFile = File("./assets/banner.txt").readText(Charsets.UTF_8) + for (line in bannerFile.split("\n")) { + val l = line + .replace("{{.Version}}", NinoInfo.VERSION) + .replace("{{.CommitSha}}", NinoInfo.COMMIT_SHA) + .replace("{{.BuildDate}}", NinoInfo.BUILD_DATE) + + println(l) + } + + val configFile = File("./config.yml") + val config = Yaml.default.decodeFromString(Config.serializer(), configFile.readText()) + + logger.info("* Connecting to PostgreSQL...") + val dataSource = HikariDataSource( + HikariConfig().apply { + jdbcUrl = "jdbc:postgresql://${config.database.host}:${config.database.port}/${config.database.name}" + username = config.database.username + password = config.database.password + schema = config.database.schema + driverClassName = "org.postgresql.Driver" + isAutoCommit = false + transactionIsolation = IsolationLevel.TRANSACTION_REPEATABLE_READ.name + leakDetectionThreshold = 30L * 1000 + poolName = "Nino-HikariPool" + } + ) + + Database.connect( + dataSource, + databaseConfig = DatabaseConfig { + defaultRepetitionAttempts = 5 + defaultIsolationLevel = IsolationLevel.TRANSACTION_REPEATABLE_READ.levelId + sqlLogger = if (config.environment == Environment.Development) { + Slf4jSqlDebugLogger + } else { + null + } + } + ) + + runBlocking { + createPgEnums( + mapOf( + "BanTypeEnum" to BanType.values().map { it.name }, + "PunishmentTypeEnum" to PunishmentType.values().map { it.name } + ) + ) + } + + transaction { + SchemaUtils.createMissingTablesAndColumns( + AutomodTable, + GlobalBansTable, + GuildCases, + GuildSettings, + GuildLogging, + Users, + Warnings + ) + } + + logger.info("* Connecting to Redis...") + val redis = RedisManager(config) + redis.connect() + + val kord = runBlocking { + Kord(config.token) { + enableShutdownHook = false + + cache { + // cache members + members { cache, description -> + MapEntryCache(cache, description, MapLikeCollection.concurrentHashMap()) + } + + // cache users + users { cache, description -> + MapEntryCache(cache, description, MapLikeCollection.concurrentHashMap()) + } + } + } + } + + logger.info("* Initializing Koin...") + val koin = startKoin { + modules( + globalModule, + *apiModule.toTypedArray(), + *commandsModule.toTypedArray(), + jobsModule, + module { + single { + config + } + + single { + kord + } + + single { + dataSource + } + + single { + redis + } + }, + + punishmentsModule + ) + } + + // implement kord events here + kord.on { + val handler = koin.koin.get() + handler.onCommand(this) + } + + // run api here + if (config.api != null) { + NinoScope.launch { + GlobalContext.retrieve().launch() + } + } + + val bot = koin.koin.get() + runBlocking { + try { + bot.start() + } catch (e: Exception) { + logger.error("Unable to initialize Nino:", e) + exitProcess(1) + } + } + } + + private fun addShutdownHook() { + logger.info("Adding shutdown hook...") + + val runtime = Runtime.getRuntime() + runtime.addShutdownHook( + thread(false, name = "Nino-ShutdownThread") { + logger.warn("Shutting down...") + + val kord = GlobalContext.retrieve() + val dataSource = GlobalContext.retrieve() + val apiServer = GlobalContext.retrieve() + val timeouts = GlobalContext.retrieve() + val redis = GlobalContext.retrieve() + + // Close off the Nino scope and detach all shards + runBlocking { + kord.gateway.detachAll() + apiServer.shutdown() + NinoScope.cancel() + } + + // Close off the database connection + dataSource.close() + timeouts.close() + redis.close() + + logger.info("Successfully shut down! Goodbye.") + } + ) + } +} diff --git a/bot/src/main/resources/build-info.json b/bot/src/main/resources/build-info.json new file mode 100644 index 00000000..cc1adb96 --- /dev/null +++ b/bot/src/main/resources/build-info.json @@ -0,0 +1,5 @@ +{ + "version": "${version}", + "commit_sha": "${commitSha}", + "build_date": "${buildDate}" +} diff --git a/bot/src/main/resources/config/logging.example.properties b/bot/src/main/resources/config/logging.example.properties new file mode 100644 index 00000000..cbedd33d --- /dev/null +++ b/bot/src/main/resources/config/logging.example.properties @@ -0,0 +1,48 @@ +# Copyright (c) 2019-2022 Nino +# +# 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. + +# This file customizes Logback however you wish! +# Rename this file to `logging.properties` to be fully loaded, thanks! + +# Add additional appenders towards your logging output. +# Available outputs: +# - Sentry: +# - Description: Allows you to output errors onto Sentry. +# - Required Configuration: +# - "nino.sentryDsn" +# +# - ElasticSearch +# - Description: Allows you to output errors with ElasticSearch + Logstash, while letting you visualize it +# with Kibana. This isn't needed for most instances. +# +# - File: +# - Description: Allows you to output a file +# - Required Configuration: +# - "nino.logging.filename" +# nino.logging.appenders=sentry,elasticsearch,file + +# Uncomment this out to add an aditional file name if using the File appender. +# nino.logging.filename="/var/log/nino/Nino.out.log" + +# Uncomment this out to use a Sentry DSN if using the Sentry appender +# nino.sentryDSN=... + +# Uncomment this out to get debug logging +# nino.debug=true diff --git a/bot/src/main/resources/logback.xml b/bot/src/main/resources/logback.xml new file mode 100644 index 00000000..cc7b05da --- /dev/null +++ b/bot/src/main/resources/logback.xml @@ -0,0 +1,149 @@ + + + + + + + + + + [%d{yyyy-MM-dd | HH:mm:ss, +10}] %boldCyan([%thread]) %highlight([%logger{36}]) %boldMagenta(%-5level) :: %msg%n + + + + + + + + ${nino.logging.filename:-logs/Nino.out.log} + + + [%d{yyyy-MM-dd | HH:mm:ss, +10}] [%thread] [%logger{36}] %-5level :: %msg%n + + + + + ${nino.logging.file.rollingPolicy.pattern:-./logs/Nino.%d{yyyy-MM-dd}.log} + ${nino.logging.file.rollingPolicy.maxHistory:-7} + + + + + + + + + + + ${nino.sentryDsn} + + + + + + + + + + ${nino.logging.logstash.urls} + + 5 minutes + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/bot/timeouts/build.gradle.kts b/bot/timeouts/build.gradle.kts new file mode 100644 index 00000000..b5405d3e --- /dev/null +++ b/bot/timeouts/build.gradle.kts @@ -0,0 +1,29 @@ +/** + * Copyright (c) 2019-2022 Nino + * + * 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. + */ + +plugins { + `nino-module` +} + +dependencies { + testImplementation("org.slf4j:slf4j-simple:1.7.35") +} diff --git a/bot/timeouts/src/main/kotlin/sh/nino/discord/timeouts/Client.kt b/bot/timeouts/src/main/kotlin/sh/nino/discord/timeouts/Client.kt new file mode 100644 index 00000000..ef3838ab --- /dev/null +++ b/bot/timeouts/src/main/kotlin/sh/nino/discord/timeouts/Client.kt @@ -0,0 +1,93 @@ +/* + * Copyright (c) 2019-2022 Nino + * + * 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. + */ + +package sh.nino.discord.timeouts + +import gay.floof.utils.slf4j.logging +import io.ktor.client.* +import io.ktor.client.engine.okhttp.* +import io.ktor.client.features.json.* +import io.ktor.client.features.json.serializer.* +import io.ktor.client.features.websocket.* +import kotlin.contracts.ExperimentalContracts +import kotlin.contracts.InvocationKind +import kotlin.contracts.contract + +@OptIn(ExperimentalContracts::class) +fun Client(builder: ClientBuilder.() -> Unit): Client { + contract { callsInPlace(builder, InvocationKind.EXACTLY_ONCE) } + + val resources = ClientBuilder().apply(builder).build() + return Client(resources) +} + +class Client(val resources: ClientResources): AutoCloseable { + private lateinit var connection: Connection + private val logger by logging() + + val closed: Boolean + get() = if (::connection.isInitialized) connection.closed else true + + override fun close() { + if (!::connection.isInitialized) return + if (connection.closed) return + + return connection.close() + } + + suspend fun connect() { + if (this::connection.isInitialized) return + + logger.info("Connecting to WebSocket...") + val httpClient = resources.httpClient?.config { + install(WebSockets) + } ?: HttpClient(OkHttp) { + engine { + config { + followRedirects(true) + } + } + + install(WebSockets) + install(JsonFeature) { + serializer = KotlinxSerializer(resources.json) + } + } + + connection = Connection( + resources.uri, + resources.auth, + httpClient, + resources.coroutineScope, + resources.eventFlow, + resources.json, + this + ) + + return connection.connect() + } + + suspend fun send(command: Command) { + if (!::connection.isInitialized) return + return connection.send(command) + } +} diff --git a/bot/timeouts/src/main/kotlin/sh/nino/discord/timeouts/ClientBuilder.kt b/bot/timeouts/src/main/kotlin/sh/nino/discord/timeouts/ClientBuilder.kt new file mode 100644 index 00000000..1d1fdcc4 --- /dev/null +++ b/bot/timeouts/src/main/kotlin/sh/nino/discord/timeouts/ClientBuilder.kt @@ -0,0 +1,70 @@ +/* + * Copyright (c) 2019-2022 Nino + * + * 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. + */ + +package sh.nino.discord.timeouts + +import io.ktor.client.* +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.DelicateCoroutinesApi +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.serialization.json.Json + +data class ClientResources( + val shutdownAfterSuccess: Boolean = false, + val coroutineScope: CoroutineScope, + val httpClient: HttpClient?, + val eventFlow: MutableSharedFlow, + val auth: String, + val json: Json, + val uri: String +) + +class ClientBuilder { + lateinit var uri: String + + // exposed for testing, should not be used in prod + var shutdownAfterSuccess: Boolean = false + var coroutineScope: CoroutineScope? = null + var httpClient: HttpClient? = null + var eventFlow: MutableSharedFlow = MutableSharedFlow(extraBufferCapacity = Int.MAX_VALUE) + var auth: String = "" + var json: Json = Json { + ignoreUnknownKeys = true + isLenient = true + } + + @OptIn(DelicateCoroutinesApi::class) + fun build(): ClientResources { + check(::uri.isInitialized) { "URI to the timeouts service must be specified." } + + return ClientResources( + shutdownAfterSuccess, + coroutineScope ?: GlobalScope, + httpClient, + eventFlow, + auth, + json, + uri + ) + } +} diff --git a/bot/timeouts/src/main/kotlin/sh/nino/discord/timeouts/Connection.kt b/bot/timeouts/src/main/kotlin/sh/nino/discord/timeouts/Connection.kt new file mode 100644 index 00000000..dff5a99d --- /dev/null +++ b/bot/timeouts/src/main/kotlin/sh/nino/discord/timeouts/Connection.kt @@ -0,0 +1,156 @@ +/* + * Copyright (c) 2019-2022 Nino + * + * 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. + */ + +package sh.nino.discord.timeouts + +import gay.floof.utils.slf4j.logging +import io.ktor.client.* +import io.ktor.client.features.websocket.* +import io.ktor.client.request.* +import io.ktor.http.cio.websocket.* +import kotlinx.coroutines.* +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.flow.receiveAsFlow +import kotlinx.serialization.json.* +import java.net.ConnectException +import kotlin.properties.Delegates + +/** + * Represents the current [client][Client] connection. + */ +internal class Connection( + private val uri: String, + private val auth: String = "", + private val httpClient: HttpClient, + private val coroutineScope: CoroutineScope, + private val eventFlow: MutableSharedFlow, + private val json: Json, + private val client: Client +): CoroutineScope by coroutineScope, AutoCloseable { + private val closeSessionDeferred = CompletableDeferred() + private var incomingMessageJob: Job? = null + private val logger by logging() + private var session by Delegates.notNull() + + private val coroutineExceptionHandler = CoroutineExceptionHandler { ctx, t -> + logger.error("Exception in coroutine context $ctx:", t) + } + + var closed = false + + private suspend fun internalMessageLoop() { + logger.debug("Starting message event loop...") + session.incoming.receiveAsFlow().collect { + val raw = (it as Frame.Text).readText() + val decoded = json.decodeFromString(JsonObject.serializer(), raw) + + onMessage(decoded, raw) + } + } + + private suspend fun onMessage(data: JsonObject, raw: String) { + val op = data["op"]?.jsonPrimitive?.intOrNull + logger.trace("raw data:", raw) + + if (op == null) { + logger.warn("Missing op code in data structure...") + return + } + + val actualOp = try { OPCode[op] } catch (e: Exception) { null } + if (actualOp == null) { + logger.warn("Unknown op code: $op") + return + } + + when (actualOp) { + is OPCode.Apply -> { + val timeout = Timeout.fromJsonObject(data["d"]!!.jsonObject) + eventFlow.emit( + ApplyEvent( + client, + timeout + ) + ) + } + } + } + + suspend fun send(command: Command) { + val data = json.encodeToString(Command.Companion, command) + logger.trace("Sending command >> ", data) + session.send(Frame.Text(data)) + } + + private suspend fun connectionCreate(sess: DefaultClientWebSocketSession) { + logger.info("Connected to WebSocket using URI - 'ws://$uri'") + session = sess + + val message = try { + sess.incoming.receive().readBytes().decodeToString() + } catch (e: Exception) { + null + } ?: throw ConnectException("Connection was closed by server.") + + if (client.resources.shutdownAfterSuccess) { + client.close() + return + } + + val obj = json.decodeFromString(JsonObject.serializer(), message) + if (obj["op"]?.jsonPrimitive?.int == 0) { + logger.debug("Hello world!") + + eventFlow.emit(ReadyEvent(client)) + incomingMessageJob = coroutineScope.launch(coroutineExceptionHandler) { + internalMessageLoop() + } + + closeSessionDeferred.await() + + logger.warn("Destroying connection...") + incomingMessageJob?.cancelAndJoin() + sess.close( + reason = CloseReason( + CloseReason.Codes.GOING_AWAY, + "told to disconnect" + ) + ) + } + } + + suspend fun connect() { + logger.debug("Connecting to microservice using URI - 'ws://$uri'") + httpClient.ws("ws://$uri", { + if (auth.isNotEmpty()) header("Authorization", auth) + }) { + connectionCreate(this) + } + } + + override fun close() { + if (closed) return + + closeSessionDeferred.complete(Unit) + } +} diff --git a/bot/timeouts/src/main/kotlin/sh/nino/discord/timeouts/Timeout.kt b/bot/timeouts/src/main/kotlin/sh/nino/discord/timeouts/Timeout.kt new file mode 100644 index 00000000..acd04000 --- /dev/null +++ b/bot/timeouts/src/main/kotlin/sh/nino/discord/timeouts/Timeout.kt @@ -0,0 +1,62 @@ +/* + * Copyright (c) 2019-2022 Nino + * + * 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. + */ + +package sh.nino.discord.timeouts + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.jsonPrimitive +import kotlinx.serialization.json.long + +/** + * Represents the timeout as a serializable object. ([source](https://github.com/NinoDiscord/timeouts/blob/master/pkg/types.go#L18-L26)) + */ +@Serializable +data class Timeout( + @SerialName("guild_id") + val guildId: String, + + @SerialName("user_id") + val userId: String, + + @SerialName("issued_at") + val issuedAt: Long, + + @SerialName("expires_at") + val expiresIn: Long, + + @SerialName("moderator_id") + val moderatorId: String, + val reason: String? = null, + val type: String +) + +fun Timeout.Companion.fromJsonObject(data: JsonObject): Timeout = Timeout( + guildId = data["guild_id"]!!.jsonPrimitive.content, + userId = data["user_id"]!!.jsonPrimitive.content, + issuedAt = data["issued_at"]!!.jsonPrimitive.long, + expiresIn = data["expires_in"]!!.jsonPrimitive.long, + moderatorId = data["moderator_id"]!!.jsonPrimitive.content, + reason = data["reason"]?.jsonPrimitive?.content, + type = data["type"]!!.jsonPrimitive.content +) diff --git a/bot/timeouts/src/main/kotlin/sh/nino/discord/timeouts/_Commands.kt b/bot/timeouts/src/main/kotlin/sh/nino/discord/timeouts/_Commands.kt new file mode 100644 index 00000000..01bb68fb --- /dev/null +++ b/bot/timeouts/src/main/kotlin/sh/nino/discord/timeouts/_Commands.kt @@ -0,0 +1,126 @@ +/* + * Copyright (c) 2019-2022 Nino + * + * 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. + */ + +package sh.nino.discord.timeouts + +import kotlinx.serialization.KSerializer +import kotlinx.serialization.Serializable +import kotlinx.serialization.SerializationStrategy +import kotlinx.serialization.descriptors.PrimitiveKind +import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor +import kotlinx.serialization.descriptors.SerialDescriptor +import kotlinx.serialization.descriptors.buildClassSerialDescriptor +import kotlinx.serialization.encoding.Decoder +import kotlinx.serialization.encoding.Encoder +import kotlinx.serialization.json.JsonObject + +/** + * Represents the operation type of command or payload. + */ +@Serializable(with = OPCode.Companion.Serializer::class) +open class OPCode(val code: Int) { + /** + * This is a **server -> client** operation code. + * + * This indicates that the connection was successful. You will be emitted a [ReadyEvent] + * event. + */ + object Ready: OPCode(0) + + /** + * This is a **server -> client** operation code. + * + * This indicates that a timeout packet has fulfilled its lifetime, and we need to do a + * reverse operation. You will be emitted a [ApplyEvent] event. + */ + object Apply: OPCode(1) + + /** + * This is a **client -> server** operation code. + * + * Requests all the timeouts that are being handled by the server. + */ + object RequestAll: OPCode(2) + + /** + * This is a **client -> server** operation code. + * + * This returns statistics about the microservice including the runtime, the ping from client -> server (for Instatus), + * and more. + */ + object Stats: OPCode(3) + + companion object { + object Serializer: KSerializer { + override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("sh.nino.timeouts.OPCode", PrimitiveKind.INT) + override fun deserialize(decoder: Decoder): OPCode = get(decoder.decodeInt()) + override fun serialize(encoder: Encoder, value: OPCode) { + encoder.encodeInt(value.code) + } + } + + private val _values = setOf(Ready, Apply, RequestAll, Stats) + operator fun get(code: Int): OPCode = _values.find { it.code == code } ?: error("Unknown OPCode: $code") + } +} + +/** + * Represents a base command to send. Use [RequestCommand], [StatsCommand], or [RequestAllCommand] + * to send out a command in a [Client]. + */ +sealed class Command { + companion object: SerializationStrategy { + override val descriptor: SerialDescriptor = buildClassSerialDescriptor("sh.nino.timeouts.Command") { + element("op", OPCode.Companion.Serializer.descriptor) + element("d", JsonObject.serializer().descriptor) + } + + override fun serialize(encoder: Encoder, value: Command) { + val composite = encoder.beginStructure(descriptor) + when (value) { + is RequestCommand -> { + composite.encodeSerializableElement(descriptor, 0, OPCode.serializer(), OPCode.Ready) + composite.encodeSerializableElement(descriptor, 1, RequestCommand.serializer(), value) + } + + is RequestAllCommand -> { + composite.encodeSerializableElement(descriptor, 0, OPCode.serializer(), OPCode.RequestAll) + composite.encodeSerializableElement(descriptor, 1, RequestAllCommand.serializer(), value) + } + } + + composite.endStructure(descriptor) + } + } +} + +/** + * Requests a [timeout] to be executed at a specific time. + */ +@Serializable +class RequestCommand(val timeout: Timeout): Command() + +/** + * Command to request all the concurrent [timeouts] that are being handled. + */ +@Serializable +class RequestAllCommand(val timeouts: List): Command() diff --git a/src/util/StringBuilder.ts b/bot/timeouts/src/main/kotlin/sh/nino/discord/timeouts/_Events.kt similarity index 67% rename from src/util/StringBuilder.ts rename to bot/timeouts/src/main/kotlin/sh/nino/discord/timeouts/_Events.kt index eb3c308d..9d54e20c 100644 --- a/src/util/StringBuilder.ts +++ b/bot/timeouts/src/main/kotlin/sh/nino/discord/timeouts/_Events.kt @@ -1,5 +1,5 @@ -/** - * Copyright (c) 2019-2021 Nino +/* + * Copyright (c) 2019-2022 Nino * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -20,23 +20,25 @@ * SOFTWARE. */ +package sh.nino.discord.timeouts + /** - * Represent a builder for a lot of strings encapsulated into one string. + * Represents a base event which includes the [client]. */ -export default class StringBuilder { - #strings: string[] = []; - - append(line?: string) { - this.#strings.push(line ?? ''); - return this; - } +interface Event { + /** + * The client that this event was emitted from. + */ + val client: Client +} - appendLine(line?: string) { - this.#strings.push(line ? `${line}\n` : '\n'); - return this; - } +/** + * This indicates that the connection was successful. + */ +class ReadyEvent(override val client: Client): Event - build(spacing: string = ' ') { - return this.#strings.join(spacing); - } -} +/** + * This indicates that a timeout packet has fulfilled its lifetime, and we need to do a + * reverse operation. + */ +class ApplyEvent(override val client: Client, val timeout: Timeout): Event diff --git a/src/entities/AutomodEntity.ts b/bot/timeouts/src/test/kotlin/sh/nino/tests/timeouts/ClientTests.kt similarity index 51% rename from src/entities/AutomodEntity.ts rename to bot/timeouts/src/test/kotlin/sh/nino/tests/timeouts/ClientTests.kt index 3d601117..0211b4ef 100644 --- a/src/entities/AutomodEntity.ts +++ b/bot/timeouts/src/test/kotlin/sh/nino/tests/timeouts/ClientTests.kt @@ -1,5 +1,5 @@ -/** - * Copyright (c) 2019-2021 Nino +/* + * Copyright (c) 2019-2022 Nino * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -20,42 +20,36 @@ * SOFTWARE. */ -import { Entity, Column, PrimaryColumn } from 'typeorm'; - -@Entity({ name: 'automod' }) -export default class AutomodEntity { - @Column({ - array: true, - type: 'text', - name: 'whitelist_channels_during_raid', - default: '{}', - }) - public whitelistChannelsDuringRaid!: string[]; - - @Column({ array: true, type: 'text', name: 'blacklist_words', default: '{}' }) - public blacklistWords!: string[]; - - @Column({ default: false, name: 'short_links' }) - public shortLinks!: boolean; - - @Column({ default: false }) - public blacklist!: boolean; - - @Column({ default: false }) - public mentions!: boolean; - - @Column({ default: false }) - public invites!: boolean; - - @Column({ default: false }) - public dehoist!: boolean; - - @PrimaryColumn({ name: 'guild_id' }) - public guildID!: string; - - @Column({ default: false }) - public spam!: boolean; - - @Column({ default: false }) - public raid!: boolean; -} +package sh.nino.tests.timeouts + +import io.kotest.assertions.throwables.shouldNotThrow +import io.kotest.assertions.throwables.shouldThrow +import io.kotest.core.spec.style.DescribeSpec +import io.kotest.matchers.should +import io.kotest.matchers.string.startWith +import sh.nino.discord.timeouts.ClientBuilder + +class ClientTests: DescribeSpec({ + it("should throw an illegal state exception on ClientBuilder#build without a URI.") { + val builder = ClientBuilder().apply { + auth = "jsssosjsbnsaskjdssdkds" + } + + val ex = shouldThrow { + builder.build() + } + + ex.message should startWith("URI to the timeouts service") + } + + it("should not throw an illegal exception on Client#build with a URI") { + val builder = ClientBuilder().apply { + uri = "localhost:4025" + auth = "owo" + } + + shouldNotThrow { + builder.build() + } + } +}) diff --git a/bot/timeouts/src/test/kotlin/sh/nino/tests/timeouts/ConnectionTest.kt b/bot/timeouts/src/test/kotlin/sh/nino/tests/timeouts/ConnectionTest.kt new file mode 100644 index 00000000..95e98205 --- /dev/null +++ b/bot/timeouts/src/test/kotlin/sh/nino/tests/timeouts/ConnectionTest.kt @@ -0,0 +1,68 @@ +/* + * Copyright (c) 2019-2022 Nino + * + * 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. + */ + +package sh.nino.tests.timeouts + +import io.kotest.assertions.throwables.shouldThrow +import io.kotest.core.spec.style.DescribeSpec +import io.kotest.matchers.string.shouldStartWith +import sh.nino.discord.timeouts.Client +import java.net.ConnectException + +class ConnectionTest: DescribeSpec({ + it("should not connect due to not finding it.") { + val client = Client { + uri = "localhost:6666" + auth = "owodauwu" + } + + val exception = shouldThrow { + client.connect() + } + + exception.message shouldStartWith "Failed to connect" + } + + // Commented out due to not knowing how to do this with GitHub actions +// it("should connect with valid auth") { +// val isCI = System.getenv("GITHUB_ACTIONS") != null +// val client = Client { +// uri = if (isCI) "timeouts:4025" else "localhost:4025" +// auth = "owodauwu" +// shutdownAfterSuccess = true +// } +// +// shouldNotThrow { +// client.connect() +// } +// } +// +// it("should error with bad auth") { +// val client = Client { +// uri = "localhost:4025" +// auth = "fuck" +// } +// +// val exception = shouldThrow { client.connect() } +// exception.message shouldBe "Connection was closed by server." +// } +}) diff --git a/src/entities/UserEntity.ts b/build.gradle.kts similarity index 76% rename from src/entities/UserEntity.ts rename to build.gradle.kts index 2c8ed18a..e71c10b4 100644 --- a/src/entities/UserEntity.ts +++ b/build.gradle.kts @@ -1,5 +1,5 @@ /** - * Copyright (c) 2019-2021 Nino + * Copyright (c) 2019-2022 Nino * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -20,16 +20,17 @@ * SOFTWARE. */ -import { Entity, Column, PrimaryColumn } from 'typeorm'; +import gay.floof.gradle.utils.* -@Entity({ name: 'users' }) -export default class UserEntity { - @Column({ default: 'en_US' }) - public language!: string; +plugins { + application +} - @Column({ array: true, type: 'text' }) - public prefixes!: string[]; +group = "sh.nino" +version = "$current" - @PrimaryColumn({ name: 'user_id' }) - public id!: string; +repositories { + mavenCentral() + mavenLocal() + noel() } diff --git a/buildSrc/build.gradle.kts b/buildSrc/build.gradle.kts new file mode 100644 index 00000000..378f43da --- /dev/null +++ b/buildSrc/build.gradle.kts @@ -0,0 +1,42 @@ +/** + * Copyright (c) 2019-2022 Nino + * + * 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. + */ + +plugins { + groovy + `kotlin-dsl` +} + +repositories { + mavenCentral() + gradlePluginPortal() + maven("https://maven.floofy.dev/repo/releases") +} + +dependencies { + implementation(kotlin("gradle-plugin", version = "1.6.10")) + implementation(kotlin("serialization", version = "1.6.10")) + implementation("org.jetbrains.kotlinx:atomicfu-gradle-plugin:0.17.0") + implementation("gay.floof.utils:gradle-utils:1.1.0") + implementation("com.diffplug.spotless:spotless-plugin-gradle:6.2.0") + implementation("io.kotest:kotest-gradle-plugin:0.3.9") + implementation(gradleApi()) +} diff --git a/buildSrc/src/main/kotlin/Project.kt b/buildSrc/src/main/kotlin/Project.kt new file mode 100644 index 00000000..1bd3734e --- /dev/null +++ b/buildSrc/src/main/kotlin/Project.kt @@ -0,0 +1,25 @@ +/** + * Copyright (c) 2019-2022 Nino + * + * 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. + */ + +import gay.floof.gradle.utils.* + +val current = Version(2, 0, 0, 0, ReleaseType.Beta) diff --git a/buildSrc/src/main/kotlin/nino-module.gradle.kts b/buildSrc/src/main/kotlin/nino-module.gradle.kts new file mode 100644 index 00000000..703093f5 --- /dev/null +++ b/buildSrc/src/main/kotlin/nino-module.gradle.kts @@ -0,0 +1,93 @@ +/** + * Copyright (c) 2019-2022 Nino + * + * 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. + */ + +import gay.floof.gradle.utils.* +import org.jetbrains.kotlin.gradle.tasks.KotlinCompile + +plugins { + kotlin("plugin.serialization") + id("com.diffplug.spotless") + id("kotlinx-atomicfu") + id("io.kotest") + kotlin("jvm") +} + +val javaVersion = JavaVersion.VERSION_17 + +group = "sh.nino.bot" +version = if (project.version != "unspecified") project.version else "$current" + +repositories { + mavenCentral() + mavenLocal() + noel() +} + +dependencies { + // Testing utilities + testImplementation(platform("io.kotest:kotest-bom:5.0.3")) + testImplementation("io.kotest:kotest-runner-junit5") + testImplementation("io.kotest:kotest-assertions-core") + testImplementation("io.kotest:kotest-property") + + // do not link :bot:commons to the project itself + if (name != "commons") { + implementation(project(":bot:commons")) + } +} + +// Setup Spotless in all subprojects +spotless { + kotlin { + trimTrailingWhitespace() + licenseHeaderFile("${rootProject.projectDir}/assets/HEADING") + endWithNewline() + + // We can't use the .editorconfig file, so we'll have to specify it here + // issue: https://github.com/diffplug/spotless/issues/142 + // ktlint 0.35.0 (default for Spotless) doesn't support trailing commas + ktlint("0.43.0") + .userData( + mapOf( + "no-consecutive-blank-lines" to "true", + "no-unit-return" to "true", + "disabled_rules" to "no-wildcard-imports,colon-spacing", + "indent_size" to "4" + ) + ) + } +} + +tasks { + withType { + kotlinOptions { + jvmTarget = javaVersion.toString() + javaParameters = true + freeCompilerArgs += listOf("-Xopt-in=kotlin.RequiresOptIn") + } + } +} + +java { + sourceCompatibility = javaVersion + targetCompatibility = javaVersion +} diff --git a/docker-compose.yml b/docker-compose.yml index 91d4fdfb..6ee72130 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,36 +1,35 @@ version: '3.8' services: - bot: + nino: + build: . container_name: nino restart: always - build: . depends_on: - - postgresql + - database - prometheus - timeouts + - cluster_operator - redis networks: - nino volumes: - - ./config.yml:/opt/Nino/config.yml:ro # Read-only - + - /run/media/noel/Storage/Projects/Nino/Nino/config.yml:/app/Nino/config.yml:ro redis: container_name: redis restart: always - image: redis:latest + image: redis ports: - - 6379:6379 + - "6379:6379" networks: - nino volumes: - redis:/data - - postgresql: - container_name: postgres + database: + container_name: database restart: always image: postgres:latest ports: - - 5432:5432 + - "5432:5432" networks: - nino volumes: @@ -38,8 +37,7 @@ services: environment: POSTGRES_USER: ${DATABASE_USERNAME} POSTGRES_PASSWORD: ${DATABASE_PASSWORD} - POSTGRES_DB: ${DATABASE_NAME} - + POSTGRES_DB: nino prometheus: container_name: prometheus build: ./docker/prometheus @@ -47,21 +45,29 @@ services: networks: - default +# cluster-operator: +# container_name: cluster-operator +# image: noelware/cluster-operator:81dd82c23ed00592de4293f6b222fcbad4948eee +# ports: +# - "3010:3010" +# networks: +# - nino +# volumes: +# - /run/media/noel/Storage/Projects/Nino/Nino/docker/cluster-operator/config.json:/app/config.json:ro + timeouts: container_name: timeouts restart: always image: docker.pkg.github.com/ninodiscord/timeouts/timeouts:latest ports: - - 4025:4025 + - "4025:4025" networks: - nino environment: AUTH: ${TIMEOUTS_AUTH} - volumes: redis: postgres: - networks: nino: internal: true diff --git a/docker/cluster-operator/Dockerfile b/docker/cluster-operator/Dockerfile new file mode 100644 index 00000000..f8a99895 --- /dev/null +++ b/docker/cluster-operator/Dockerfile @@ -0,0 +1,3 @@ +FROM noelware/cluster-operator:latest + +COPY config.json /app/config.json diff --git a/docker/cluster-operator/config.example.json b/docker/cluster-operator/config.example.json new file mode 100644 index 00000000..5641379f --- /dev/null +++ b/docker/cluster-operator/config.example.json @@ -0,0 +1,11 @@ +{ + "env": "Prod", + "clusters": 1, + "shards": 1, + "auth": "", + "webhook": "", + "metricsPrefix": "nino_", + "mergeMetrics": true, + "logEvents": false, + "exportDefaultMetrics": false +} diff --git a/docker/docker-entrypoint.sh b/docker/docker-entrypoint.sh new file mode 100644 index 00000000..08036965 --- /dev/null +++ b/docker/docker-entrypoint.sh @@ -0,0 +1,35 @@ +#!/bin/bash + +# Copyright (c) 2019-2022 Nino +# +# 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. + +set -o errexit +set -o nounset +set -o pipefail + +. /app/noelware/nino/scripts/liblog.sh + +info "" +info " Welcome to the ${BOLD}Nino${RESET} container image!" +info " Subscribe to the project for updates: https://github.com/NinoDiscord/Nino" +info " Submit issues if any bugs occur: https://github.com/NinoDiscord/Nino/issues" +info "" + +exec "$@" diff --git a/docker/run.sh b/docker/run.sh new file mode 100644 index 00000000..d2754631 --- /dev/null +++ b/docker/run.sh @@ -0,0 +1,44 @@ +#!/bin/bash + +# Copyright (c) 2019-2022 Nino +# +# 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. + +set -o errexit +set -o nounset +set -o pipefail + +. /app/noelware/nino/scripts/liblog.sh + +info "*** Starting Nino! ***" +debug " => Custom Logback Location: ${NINO_CUSTOM_LOGBACK_FILE:-unknown}" +debug " => Using Custom Gateway: ${NINO_USE_GATEWAY:-false}" +debug " => Dedicated Node: ${WINTERFOX_DEDI_NODE:-none}" + +JAVA_OPTS=("-XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8") + +if [[ -z "${NINO_CUSTOM_LOGBACK_FILE:-}" ]] + JAVA_OPTS+=("-Dlogback.configurationFile=${NINO_CUSTOM_LOGBACK_FILE} ") + +if [[ -z "${WINTERFOX_DEDI_NODE:-}" ]] + JAVA_OPTS+=("-Pwinterfox.dediNode=${WINTERFOX_DEDI_NODE} ") + +JAVA_OPTS+=("$@") + +/app/noelware/nino/bot/bin/bot diff --git a/docker/scripts/liblog.sh b/docker/scripts/liblog.sh new file mode 100644 index 00000000..db93c131 --- /dev/null +++ b/docker/scripts/liblog.sh @@ -0,0 +1,43 @@ +#!/bin/bash + +# Copyright (c) 2019-2022 Nino +# +# 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. + +BLUE='\033[38;2;81;81;140m' +GREEN='\033[38;2;165;204;165m' +PINK='\033[38;2;241;204;209m' +RESET='\033[0m' +BOLD='\033[1m' +UNDERLINE='\033[4m' + +info() { + timestamp=$(date +"%D ~ %r") + printf "%b\\n" "${GREEN}${BOLD}info${RESET} | ${PINK}${BOLD}${timestamp}${RESET} ~ $1" +} + +debug() { + local debug="${NINO_DEBUG:-false}" + shopt -s nocasematch + timestamp=$(date +"%D ~%r") + + if ! [[ "$debug" = "1" || "$debug" =~ ^(no|false)$ ]]; then + printf "%b\\n" "${BLUE}${BOLD}debug${RESET} | ${PINK}${BOLD}${timestamp}${RESET} ~ $1" + fi +} diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 00000000..0999f0bc --- /dev/null +++ b/gradle.properties @@ -0,0 +1,2 @@ +kotlin.code.style=official +org.gradle.jvmargs=-Xmx4096m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8 diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 00000000..7454180f Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 00000000..2e6e5897 --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-7.3.3-bin.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew new file mode 100755 index 00000000..744e882e --- /dev/null +++ b/gradlew @@ -0,0 +1,185 @@ +#!/usr/bin/env sh + +# +# Copyright 2015 the original author or authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn () { + echo "$*" +} + +die () { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MSYS* | MINGW* ) + msys=true + ;; + NONSTOP* ) + nonstop=true + ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin or MSYS, switch paths to Windows format before running java +if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=`expr $i + 1` + done + case $i in + 0) set -- ;; + 1) set -- "$args0" ;; + 2) set -- "$args0" "$args1" ;; + 3) set -- "$args0" "$args1" "$args2" ;; + 4) set -- "$args0" "$args1" "$args2" "$args3" ;; + 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Escape application args +save () { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " +} +APP_ARGS=`save "$@"` + +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" + +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 00000000..ac1b06f9 --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,89 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto execute + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/locales/en_US.json b/locales/en_US.json index a2931d52..3a21d830 100644 --- a/locales/en_US.json +++ b/locales/en_US.json @@ -2,147 +2,184 @@ "meta": { "contributors": ["239790360728043520"], "translator": "280158289667555328", - "aliases": ["en", "en_us", "english"], - "code": "en_US", - "full": "English (US)", - "flag": ":flag_us:" + "name": "English (United States)", + "flag": ":flag_us:", + "code": "en_US" }, "strings": { - "descriptions": { - "unknown": "This command doesn't have a description tied.", + "generic.enabled": "Enabled", + "generic.disabled": "Disabled", + "generic.lonely": "Heh... it's pretty empty, eh? I guess it's like a set of hearts, only one can be the chosen one! But, I shouldn't give you an extensional crisis right now. Go have fun! ...and maybe touch grass, you really seem to be lacking in that department x3", + "generic.nothing": "Nothing to show.", + "generic.yes": "Yes", + "generic.no": "No", - "help": "Returns a list of Nino's commands or documentation of a specific command or module.", - "invite": "Returns the invitation link to invite Nino into your guild! Also, support if you really need it.", - "locale": "Sets, resets, or views the current translations for Nino.", - "ping": "Returns the latency of anything that could cause me being... slighly un-usable...", - "shardinfo": "Returns shard information for Nino.", - "source": "Returns the source code URL of Nino, maybe to contribute or give a :star:?", - "statistics": "Returns nerd statistics related to Nino.", - "uptime": "How long I have been up for!", + "errors.ownerOnly": "You are not allowed to invoke the **${name}** command.", + "errors.missingPermsBot": "I am currently missing the following permissions: **${perms}**", + "errors.missingPermsUser": "You are currently missing the following permissions: **${perms}**", + "errors.cooldown": "You are currently on cooldown for command **${command}** for **${time}**.", + "errors.unknown.dev": [ + "Sorry! I was not able to execute the ${prefix} **${command}**! :<", + "If this keeps erroring, please report it to:", + "${owners}", + "", + "Since you are in development mode, the stacktrace can be seen below", + "and in the console!", + "", + "```kotlin", + "${stacktrace}", + "```" + ], - "ban": "Bans a user from this guild or outside the guild.", - "case": "Returns a mini embed of the case you searched up.", - "kick": ":boot: Kicks a member from this guild.", - "mute": "Mutes a member from any textable channel.", - "pardon": "Removes one or multiple warnings from a user in this guild.", - "purge": "Purges a certain amount of messages by user(s), system account, bot(s), or specific messages.", - "reason": "Changes the reason of a specific case.", - "softban": "Softly bans a user from this guild.", - "timeouts": "Returns a list of timeouts based on the type of action.", - "unban": "Unbans a user from this guild.", - "unmute": "Unmutes a user from this guild.", - "warn": "Warns a specific user.", - "warnings": "Shows a list of warnings from a specific user.", - "voice_mute": "Mutes a person in the voice channel you're in.", - "voice_deafen": "Deafens a person in the voice channel you're in.", - "voice_undeafen": "Undeafens a person in the voice channel you're in.", - "voice_unmute": "Unmutes a person in the voice channel you're in.", - "voice_kick": "Kicks any member(s) or bot(s) in a voice channel you're in.", + "errors.unknown.prod": [ + "Sorry! I was not able to execute the ${prefix} **${command}**! :<", + "If this keeps erroring, please report it to:", + "${owners}", + "on the Noelware Discord server under the <#824071651486335036> channel:", + "> https://discord.gg/ATmjFH9kMH" + ], - "automod": "Enables, disables, or views any automod utility available.", - "logging": "Enables or disable the Logging feature.", - "modlog": "Set or reset the mod log channel.", - "muted_role": "Sets or resets the Muted role.", - "prefix": "View, change, or reset a guild or user prefix.", - "punishments": "View, change, or remove a guild punishment.", - "reset": "Resets all guild settings!", - "settings": "Views a list of all the settings in this guild." - }, - "commands": { - "help": { - "embed": { - "title": "%s | Commands List", - "description": [ - ":pencil2: **For more documentation, you can type `%shelp ` with `` being the command or module in this list.**", - "", - "More information and a prettier UI for commands or modules can be viewed on the [website](https://nino.floofy.dev).", - "There are currently **%d** available commands." - ], - "fields": { - "moderation": "• Moderation [%d]", - "core": "• Core [%d]", - "settings": "• Settings [%d]" - } - }, - "command": { - "not_found": ":question: Command or module **%s** was not found.", - "embed": { - "title": "[ :pencil2: Command \"%s\" ]", - "description": "> **%s**", - "fields": { - "syntax": "• Syntax", - "category": "• Category", - "aliases": "• Aliases", - "owner_only": "• Owner Only", - "cooldown": "• Cooldown", - "user_perms": "• Required User Permissions", - "bot_perms": "• Required Bot Permissions", - "examples": "• Examples" - } - } - }, - "module": { - "embed": { - "title": "[ ·̩̩̥͙**•̩̩͙✩•̩̩͙* Module \"%s\" ˚*•̩̩͙✩•̩̩͙*˚*·̩̩̥͙ ]" - } - }, - "usage_title": "Command Usage", - "usage": [ - "So, if you're not familar with the command syntax, here's a breakdown:", - "", - "> A simple command might look like: `%shelp [cmdOrMod | \"usage\"]`", - "", - "```", - "x! help [cmdOrMod | \"usage\"]", - "^ ^ ^ ^ ^", - "| | | | |", - "| | | | |", - "| | | | |", - "prefix cmd parameter(s) \"or\" literal", - "```", - "", - "Parameters are easy to understand. If they are \"piped\" with a `|`, it means it's a \"or\", or what you can provide. If they are a literal", - "parameter, it means if you typed \"usage\" out, it'll print something else.", - "", - "- A parameter wrapped in `[]` means it's optional, but you can add additional arguments to make it run something else", - "- A parameter wrapped in `<>` means it's required, which means *you* have to add that argument to make the command perform correctly.", - "", - ":question: **Still stuck? There is always examples in the command's short overview to show how you can run that specific command.**" - ] - }, - "invite": [ - ":wave: Heyo **%s**! You want to invite me into your own server? That's pretty cool if I do say so myself~", - "You can do so by clicking this link: **%s**", - "", - "If you want to see bleeding edge features, you can also invite the beta instance:", - "**%s**", - "", - "Need any support? Join the **Noelware** Discord server under <#824071651486335036> to get help with me!", - "%s" - ] - }, - "generic": {}, - "automod": { - "blacklist": "Hey, I don't think you should say that here! (☍﹏⁰)。", - "invites": "Hey! You're not allowed to post server links here... o(╥﹏╥)o", - "mentions": "Uhm, I don't think that'll get their attention... (;¬_¬)", - "shortlinks": "You shouldn't send that kind of link here... (⁎˃ᆺ˂)", - "spam": "I don't think spamming will get you anywhere... o(╥﹏╥)o", - "raid": { - "locked": "Hey all! I decided to close off all channels you had access in for ~10 seconds due to someone being rude and raiding! ૮( ᵒ̌▱๋ᵒ̌ )ა", - "unlocked": "Heyo! I restored all channels, I hope there is no more raiders.. :3." - } - }, - "errors": { - "blacklists": { - "guild": [ - "Guild **${guild}** is blacklisted from using me by **${user}**", - "> **${reason}**", - "", - ":question: If there was an issue or a misunderstanding in the blacklist, contact us here:" - ], - "user": [] - } - } + "descriptions.admin.automod": "Command to update your automod settings!", + "descriptions.automod.messageLinks": "Toggles the Message Links automod -- read more [here](https://nino.sh/docs/automod/message-links)", + "descriptions.automod.accountAge": "Toggles the Account Age automod or sets a threshold -- read more [here](https://nino.sh/docs/automod/account-age)", + "descriptions.automod.dehoist": "Toggles the Dehoisting automod -- read more [here](https://nino.sh/docs/automod/dehoisting)", + "descriptions.automod.blacklist": "Toggles the Blacklist automod, or add/remove/list blacklisted words. -- read more [here](https://nino.sh/docs/automod/blacklist)", + "descriptions.automod.phishing": "Toggles the Phishing Links automod -- read more [here](https://nino.sh/docs/automod/phishing)", + "descriptions.automod.toxicity": "Toggles the Toxicity automod -- read more [here](https://nino.sh/docs/automod/toxicity)", + "descriptions.automod.spam": "Toggles the Spam automod -- read more [here](https://nino.sh/docs/automod/spam)", + "descriptions.automod.raid": "Toggles the Raid automod -- read more [here](https://nino.sh/docs/automod/raid)", + "descriptions.automod.invites": "Toggles the Invites automod -- read more [here](https://nino.sh/docs/automod/invites)", + "descriptions.automod.mentions": "Toggles the Mentions automod, or sets a mention threshold. -- read more [here](https://nino.sh/docs/automod/mentions)", + "descriptions.admin.export": "Exports your guild settings to be easily imported later on.", + "descriptions.admin.import": "Import your exported guild settings easily. You can't revert back once you import back.", + "descriptions.admin.logging": "Administrate your logging settings here!", + "descriptions.logging.omitUsers": "Add or remove users to omit from being logged in the specified log channel.", + "descriptions.logging.omitChannels": "Add or remove text channels to omit from being logged in the specified log channel.", + "descriptions.logging.config": "Shows the current logging configuration", + "descriptions.logging.events": "Toggle specific logging events -- read more [here](https://nino.sh/docs/logging)", + "descriptions.prefix": "Creates, views, and deletes the guild or your prefixes.", + "descriptions.prefix.set": "Adds a prefix to the list of prefixes that you or the guild can use, depends on your desire!", + "descriptions.prefix.delete": "Removes a prefix from the list of prefixes that you or the guild doesn't like, but it doesn't hurt to delete any... right?", + "descriptions.core.help": "Returns documentation on a command or module, or a list of commands you can use.", + "descriptions.core.invite": "Returns the link to invite me to your guild, master!", + "descriptions.core.ping": "Returns the latency from Discord to us", + "descriptions.core.shardinfo": "Returns dedicated information about all shards available.", + "descriptions.core.source": "Returns the link to Nino's source code", + "descriptions.core.statistics": "Returns miscellanous statistics about Nino, useless or not!", + "descriptions.core.uptime": "Returns how long Nino has been up.", + + "commands.automod.toggle": "${emoji} ${toggle} the **${name}** automod!", + "commands.admin.logging.toggle": "${emoji} ${toggle} the **Logging** feature.", + "commands.admin.logging.omitUsers.embed.title": "[ Omitted Users ]", + "commands.admin.logging.omitUsers.embed.description": [ + "Here is a list of users that are omitted from being logged:", + "", + "${list}" + ], + "commands.admin.logging.omitUsers.add.missingArgs": "You are missing users to omit from logging! You can use one or more mentions or user IDs.", + "commands.admin.logging.omitUsers.404": "I was unable to find the users to omit. Did you specify a mention or user ID?", + "commands.admin.logging.omitUsers.success": "${operation} **${users}** user${suffix}!", + "commands.admin.logging.omitUsers.del.missingArgs": "Missing user(s) to remove from the omitted list.", + "commands.admin.logging.omitChannels.404": "I was unable to find the channels to omit. Did you specify a mention or channel ID? Did you specify only voice channels?", + "commands.admin.logging.omitChannels.add.missingArgs": "You are missing text channels to omit from logging! You can use one or more mentions or channel IDs.", + "commands.admin.logging.omitChannels.success": "${operation} **${users}** text channel${suffix}!", + "commands.admin.logging.omitChannels.del.missingArgs": "Missing channel(s) to remove from the omitted list.", + "commands.admin.logging.omitChannels.embed.title": "[ Omitted Users ]", + "commands.admin.logging.omitChannels.embed.description": [ + "Here is a list of text channels that are omitted from being logged:", + "", + "${list}" + ], + "commands.admin.logging.success": "${emoji} Successfully set the logging channel to ${channel}!", + "commands.admin.logging.invalid": "Argument **${arg}** was not a valid channel mention or ID.", + "commands.admin.logging.config.message": [ + "```md", + "# Logging Configuration for ${name}", + "", + "• Enabled: ${enabled}", + "• Channel: ${channel}", + "```" + ], + "commands.admin.logging.events.list.embed.title": "Enabled Events for ${name}", + "commands.admin.logging.events.list.embed.description": [ + "This is a list of logging events that are enabled or disabled.", + "Read our [Terms of Service](https://nino.sh/terms) & [Privacy Policy](https://nino.sh/privacy) before enabling!", + "", + "${list}" + ], + + "commands.admin.prefix.user.list": [ + "```md", + "# Prefixes for ${user}", + "${list}", + "", + "## Note", + "> You can also use the following default prefixes as well:", + "", + "${prefixes}", + "```" + ], + + "commands.admin.prefix.guild.list": [ + "```md", + "# Prefixes for ${name}", + "${list}", + "", + "## Note", + "> You can also use the following default prefixes as well:", + "", + "${prefixes}", + "```" + ], + + "commands.admin.prefix.set.noPermission": "You are currently missing the **Manage Guild** permission to modify guild prefixes.", + "commands.admin.prefix.set.missingPrefix": "You are missing a prefix to set. You can use `\"` to use spaces in your prefixes. (example: `\"nino is the best\"` -> `nino is the best help`)", + "commands.admin.prefix.set.maxLengthExceeded": "You went **${chars.over}** characters over the limit with the prefix you supplied: ${prefix}", + "commands.admin.prefix.set.alreadySet": "Prefix ${prefix} is already available!", + "commands.admin.prefix.set.available": "Prefix ${prefix} is now available. :3", + "commands.admin.prefix.reset.alreadyRemoved": "Prefix ${prefix} is already unavailable...", + "commands.admin.prefix.reset.unavailable": "Prefix ${prefix} is now unavailable, if you want to add it back, then say so!", + "commands.admin.prefix.reset.guild.embed.title": "[ Prefixes for guild ${name} ]", + "commands.admin.prefix.reset.guild.embed.description": [ + "Hello again! I am here to remind you that you are missing a prefix to remove.", + "But don't worry, I will list them again for you... don't expect this to happen again!", + "", + "Just specify the prefix below to remove it (example):", + "> prefix reset \"nino is the best\"", + "", + "```md", + "${prefixes}", + "```" + ], + + "commands.admin.prefix.reset.user.embed.title": "[ Prefixes for ${name} ]", + "commands.admin.prefix.reset.user.embed.description": [ + "Hello again! I am here to remind you that you are missing a prefix to remove.", + "But don't worry, I will list them again for you... don't expect this to happen again!", + "", + "Just specify the prefix below to remove it (example):", + "> prefix reset \"nino is the best\"", + "", + "```md", + "${prefixes}", + "```" + ], + + "commands.admin.rolecfg.list": [ + "```md", + "# Role Configuration for ${guild}", + "• No Threads Role: ${noThreadsRole}", + "• Muted Role: ${mutedRole}", + "```" + ], + + "commands.admin.rolecfg.muted.noArgs": "You must provide a role ID to set a Muted role or you can use the reset subcommand to reset it.", + "commands.admin.rolecfg.muted.noMutedRole": "Cannot reset muted role due to one being reset or not configured.", + "commands.admin.rolecfg.muted.set.success": "Successfully set the Muted role to **${name}**", + "commands.admin.rolecfg.muted.reset.success": "Successfully reset the Muted role.", + "commands.admin.rolecfg.noThreads.noArgs": "You must provide a role ID to set a Muted role or you can use the reset subcommand to reset it.", + "commands.admin.rolecfg.noThreads.noRoleId": "Cannot reset No Threads role due to one being reset or not configured.", + "commands.admin.rolecfg.noThreads.set.success": "Successfully set the No Threads role to **${name}**", + "commands.admin.rolecfg.noThreads.reset.success": "Successfully reset the No Threads role." } } diff --git a/locales/fr_FR.json b/locales/fr_FR.json deleted file mode 100644 index 0b8db6dd..00000000 --- a/locales/fr_FR.json +++ /dev/null @@ -1,135 +0,0 @@ -{ - "meta": { - "contributors": [], - "translator": "424921636492279808", - "aliases": ["french", "francias", "françias", "fr"], - "code": "fr_FR", - "full": "Françias (FR)", - "flag": ":flag_fr:" - }, - "strings": { - "descriptions": { - "help": "Returns a list of Nino's commands or documentation of a specific command or module.", - "invite": "Returns the invitation link to invite Nino into your guild! Also, support if you really need it.", - "locale": "Sets, resets, or views the current translations for Nino.", - "ping": "Returns the latency of anything that could cause me being... slighly un-usable...", - "shardinfo": "Returns shard information for Nino.", - "source": "Returns the source code URL of Nino, maybe to contribute or give a :star:?", - "statistics": "Returns nerd statistics related to Nino.", - "uptime": "How long I have been up for!", - - "ban": "Bans a user from this guild or outside the guild.", - "case": "Returns a mini embed of the case you searched up.", - "kick": ":boot: Kicks a member from this guild.", - "mute": "Mutes a member from any textable channel.", - "pardon": "Removes one or multiple warnings from a user in this guild.", - "purge": "Purges a certain amount of messages by user(s), system account, bot(s), or specific messages.", - "reason": "Changes the reason of a specific case.", - "softban": "Softly bans a user from this guild.", - "timeouts": "Returns a list of timeouts based on the type of action.", - "unban": "Unbans a user from this guild.", - "unmute": "Unmutes a user from this guild.", - "warn": "Warns a specific user.", - "warnings": "Shows a list of warnings from a specific user.", - "voice_mute": "Mutes a person in the voice channel you're in.", - "voice_deafen": "Deafens a person in the voice channel you're in.", - "voice_undeafen": "Undeafens a person in the voice channel you're in.", - "voice_unmute": "Unmutes a person in the voice channel you're in.", - "voice_kick": "Kicks any member(s) or bot(s) in a voice channel you're in.", - - "automod": "Enables, disables, or views any automod utility available.", - "logging": "Enables or disable the Logging feature.", - "modlog": "Set or reset the mod log channel.", - "muted_role": "Sets or resets the Muted role.", - "prefix": "View, change, or reset a guild or user prefix.", - "punishments": "View, change, or remove a guild punishment.", - "settings": "Views a list of all the settings in this guild." - }, - "commands": { - "help": { - "embed": { - "title": "%s | Commands List", - "description": [ - ":pencil2: **For more documentation, you can type `%shelp ` with `` being the command or module in this list.**", - "", - "More information and a prettier UI for commands or modules can be viewed on the [website](https://nino.floofy.dev).", - "There are currently **%d** available commands." - ], - "fields": { - "moderation": "• Moderation [%d]", - "core": "• Core [%d]", - "settings": "• Settings [%d]" - } - }, - "command": { - "not_found": ":question: Command or module **%s** was not found.", - "embed": { - "title": "[ :pencil2: Command \"%s\" ]", - "description": "> **%s**", - "fields": { - "syntax": "• Syntax", - "category": "• Category", - "aliases": "• Aliases", - "owner_only": "• Owner Only", - "cooldown": "• Cooldown", - "user_perms": "• Required User Permissions", - "bot_perms": "• Required Bot Permissions", - "examples": "• Examples" - } - } - }, - "module": { - "embed": { - "title": "[ ·̩̩̥͙**•̩̩͙✩•̩̩͙* Module \"%s\" ˚*•̩̩͙✩•̩̩͙*˚*·̩̩̥͙ ]" - } - }, - "usage_title": "Command Usage", - "usage": [ - "So, if you're not familar with the command syntax, here's a breakdown:", - "", - "> A simple command might look like: `%shelp [cmdOrMod | \"usage\"]`", - "", - "```", - "x! help [cmdOrMod | \"usage\"]", - "^ ^ ^ ^ ^", - "| | | | |", - "| | | | |", - "| | | | |", - "prefix cmd parameter(s) \"or\" literal", - "```", - "", - "Parameters are easy to understand. If they are \"piped\" with a `|`, it means it's a \"or\", or what you can provide. If they are a literal", - "parameter, it means if you typed \"usage\" out, it'll print something else.", - "", - "- A parameter wrapped in `[]` means it's optional, but you can add additional arguments to make it run something else", - "- A parameter wrapped in `<>` means it's required, which means *you* have to add that argument to make the command perform correctly.", - "", - ":question: **Still stuck? There is always examples in the command's short overview to show how you can run that specific command.**" - ] - } - }, - "generic": {}, - "automod": { - "blacklist": "Hey, I don't think you should say that here! (☍﹏⁰)。", - "invites": "Hey! You're not allowed to post server links here... o(╥﹏╥)o", - "mentions": "Uhm, I don't think that'll get their attention... (;¬_¬)", - "shortlinks": "You shouldn't send that kind of link here... (⁎˃ᆺ˂)", - "spam": "I don't think spamming will get you anywhere... o(╥﹏╥)o", - "raid": { - "locked": "Hey all! I decided to close off all channels you had access in for ~10 seconds due to someone being rude and raiding! ૮( ᵒ̌▱๋ᵒ̌ )ა", - "unlocked": "Heyo! I restored all channels, I hope there is no more raiders.. :3." - } - }, - "errors": { - "blacklists": { - "guild": [ - "Guild **${guild}** is blacklisted from using me by **${user}**", - "> **${reason}**", - "", - ":question: If there was an issue or a misunderstanding in the blacklist, contact us here:" - ], - "user": [] - } - } - } -} diff --git a/locales/pt_BR.json b/locales/pt_BR.json deleted file mode 100644 index 92a2af0f..00000000 --- a/locales/pt_BR.json +++ /dev/null @@ -1,135 +0,0 @@ -{ - "meta": { - "contributors": [], - "translator": "197448151064379393", - "aliases": ["port", "portuges", "português"], - "code": "pt_BR", - "full": "Português (BR)", - "flag": ":flag_br:" - }, - "strings": { - "descriptions": { - "help": "Returns a list of Nino's commands or documentation of a specific command or module.", - "invite": "Returns the invitation link to invite Nino into your guild! Also, support if you really need it.", - "locale": "Sets, resets, or views the current translations for Nino.", - "ping": "Returns the latency of anything that could cause me being... slighly un-usable...", - "shardinfo": "Returns shard information for Nino.", - "source": "Returns the source code URL of Nino, maybe to contribute or give a :star:?", - "statistics": "Returns nerd statistics related to Nino.", - "uptime": "How long I have been up for!", - - "ban": "Bans a user from this guild or outside the guild.", - "case": "Returns a mini embed of the case you searched up.", - "kick": ":boot: Kicks a member from this guild.", - "mute": "Mutes a member from any textable channel.", - "pardon": "Removes one or multiple warnings from a user in this guild.", - "purge": "Purges a certain amount of messages by user(s), system account, bot(s), or specific messages.", - "reason": "Changes the reason of a specific case.", - "softban": "Softly bans a user from this guild.", - "timeouts": "Returns a list of timeouts based on the type of action.", - "unban": "Unbans a user from this guild.", - "unmute": "Unmutes a user from this guild.", - "warn": "Warns a specific user.", - "warnings": "Shows a list of warnings from a specific user.", - "voice_mute": "Mutes a person in the voice channel you're in.", - "voice_deafen": "Deafens a person in the voice channel you're in.", - "voice_undeafen": "Undeafens a person in the voice channel you're in.", - "voice_unmute": "Unmutes a person in the voice channel you're in.", - "voice_kick": "Kicks any member(s) or bot(s) in a voice channel you're in.", - - "automod": "Enables, disables, or views any automod utility available.", - "logging": "Enables or disable the Logging feature.", - "modlog": "Set or reset the mod log channel.", - "muted_role": "Sets or resets the Muted role.", - "prefix": "View, change, or reset a guild or user prefix.", - "punishments": "View, change, or remove a guild punishment.", - "settings": "Views a list of all the settings in this guild." - }, - "commands": { - "help": { - "embed": { - "title": "%s | Commands List", - "description": [ - ":pencil2: **For more documentation, you can type `%shelp ` with `` being the command or module in this list.**", - "", - "More information and a prettier UI for commands or modules can be viewed on the [website](https://nino.floofy.dev).", - "There are currently **%d** available commands." - ], - "fields": { - "moderation": "• Moderation [%d]", - "core": "• Core [%d]", - "settings": "• Settings [%d]" - } - }, - "command": { - "not_found": ":question: Command or module **%s** was not found.", - "embed": { - "title": "[ :pencil2: Command \"%s\" ]", - "description": "> **%s**", - "fields": { - "syntax": "• Syntax", - "category": "• Category", - "aliases": "• Aliases", - "owner_only": "• Owner Only", - "cooldown": "• Cooldown", - "user_perms": "• Required User Permissions", - "bot_perms": "• Required Bot Permissions", - "examples": "• Examples" - } - } - }, - "module": { - "embed": { - "title": "[ ·̩̩̥͙**•̩̩͙✩•̩̩͙* Module \"%s\" ˚*•̩̩͙✩•̩̩͙*˚*·̩̩̥͙ ]" - } - }, - "usage_title": "Command Usage", - "usage": [ - "So, if you're not familar with the command syntax, here's a breakdown:", - "", - "> A simple command might look like: `%shelp [cmdOrMod | \"usage\"]`", - "", - "```", - "x! help [cmdOrMod | \"usage\"]", - "^ ^ ^ ^ ^", - "| | | | |", - "| | | | |", - "| | | | |", - "prefix cmd parameter(s) \"or\" literal", - "```", - "", - "Parameters are easy to understand. If they are \"piped\" with a `|`, it means it's a \"or\", or what you can provide. If they are a literal", - "parameter, it means if you typed \"usage\" out, it'll print something else.", - "", - "- A parameter wrapped in `[]` means it's optional, but you can add additional arguments to make it run something else", - "- A parameter wrapped in `<>` means it's required, which means *you* have to add that argument to make the command perform correctly.", - "", - ":question: **Still stuck? There is always examples in the command's short overview to show how you can run that specific command.**" - ] - } - }, - "generic": {}, - "automod": { - "blacklist": "Hey, I don't think you should say that here! (☍﹏⁰)。", - "invites": "Hey! You're not allowed to post server links here... o(╥﹏╥)o", - "mentions": "Uhm, I don't think that'll get their attention... (;¬_¬)", - "shortlinks": "You shouldn't send that kind of link here... (⁎˃ᆺ˂)", - "spam": "I don't think spamming will get you anywhere... o(╥﹏╥)o", - "raid": { - "locked": "Hey all! I decided to close off all channels you had access in for ~10 seconds due to someone being rude and raiding! ૮( ᵒ̌▱๋ᵒ̌ )ა", - "unlocked": "Heyo! I restored all channels, I hope there is no more raiders.. :3." - } - }, - "errors": { - "blacklists": { - "guild": [ - "Guild **${guild}** is blacklisted from using me by **${user}**", - "> **${reason}**", - "", - ":question: If there was an issue or a misunderstanding in the blacklist, contact us here:" - ], - "user": [] - } - } - } -} diff --git a/ormconfig.js b/ormconfig.js deleted file mode 100644 index 88023984..00000000 --- a/ormconfig.js +++ /dev/null @@ -1,35 +0,0 @@ -const { parse } = require('@augu/dotenv'); -const { join } = require('path'); - -const config = parse({ - populate: false, - file: join(__dirname, '.env'), - - schema: { - DATABASE_USERNAME: 'string', - DATABASE_PASSWORD: 'string', - DATABASE_NAME: 'string', - DATABASE_HOST: 'string', - DATABASE_PORT: 'int', - NODE_ENV: { - type: 'string', - default: ['development', 'production'], - }, - }, -}); - -module.exports = { - migrations: ['./build/migrations/*.js'], - username: config.DATABASE_USERNAME, - password: config.DATABASE_PASSWORD, - entities: ['./build/entities/*.js'], - database: config.DATABASE_NAME, - logging: false, // enable this when the deprecated message is gone - type: 'postgres', - host: config.DATABASE_HOST, - port: config.DATABASE_PORT, - - cli: { - migrationsDir: 'src/migrations', - }, -}; diff --git a/package.json b/package.json deleted file mode 100644 index fdf75e9a..00000000 --- a/package.json +++ /dev/null @@ -1,86 +0,0 @@ -{ - "name": "nino", - "description": "🔨 Advanced and cute moderation discord bot as an entry of Discord's Hack Week", - "version": "2.0.0-alpha.0", - "private": true, - "homepage": "https://nino.floofy.dev", - "license": "MIT", - "repository": "https://github.com/NinoDiscord/Nino", - "main": "build/main.js", - "author": "August ", - "prettier": "@augu/prettier-config", - "maintainers": [ - { - "email": "cutie@floofy.dev", - "name": "August", - "url": "https://floofy.dev" - } - ], - "engines": { - "node": ">=14" - }, - "scripts": { - "clean:node_modules": "rimraf node_modules/**/node_modules && rimraf node_modules/@types/**/node_modules && rimraf node_modules/@augu/**/node_modules", - "clean:win:tar": "cp node_modules/@augu/collections/build/index.js.* node_modules/@augu/collections/build/index.js && rm node_modules/@augu/collections/build/index.js.*", - "husky:install": "husky install && rm .husky/.gitignore", - "build:no-lint": "eslint src --ext .ts && rimraf build && tsc", - "export:v1:db": "node scripts/export-v1-db.js", - "shortlinks": "node scripts/shortlinks.js", - "licenses": "node scripts/add-license.js", - "prepare": "husky install && yarn clean:node_modules", - "build": "yarn lint && yarn format && rimraf build && tsc", - "format": "prettier --write --parser typescript --config ./.prettierrc.json src/**/*.ts", - "start": "cd build && node main.js", - "lint": "eslint src --ext .ts --fix", - "dev": "cd src && nodemon --exec \"ts-node --project ../tsconfig.json --files\" main.ts" - }, - "dependencies": { - "@augu/collections": "1.1.0", - "@augu/dotenv": "1.3.0", - "@augu/lilith": "5.3.3", - "@augu/orchid": "3.1.1", - "@augu/utils": "1.5.6", - "@prisma/client": "3.7.0", - "@sentry/node": "6.16.1", - "eris": "github:DonovanDMC/eris#everything", - "fastify": "3.25.3", - "fastify-cors": "6.0.2", - "fastify-no-icon": "4.0.0", - "ioredis": "4.28.2", - "js-yaml": "4.1.0", - "luxon": "2.3.0", - "ms": "2.1.3", - "pg": "8.7.1", - "prom-client": "14.0.1", - "reflect-metadata": "0.1.13", - "slash-create": "5.0.1", - "source-map-support": "0.5.21", - "tslog": "3.3.1", - "typeorm": "0.2.31", - "ws": "8.4.0" - }, - "devDependencies": { - "@augu/eslint-config": "2.2.0", - "@augu/prettier-config": "1.0.2", - "@augu/tsconfig": "1.1.1", - "@types/ioredis": "4.28.5", - "@types/js-yaml": "4.0.5", - "@types/luxon": "2.0.8", - "@types/ms": "0.7.31", - "@types/node": "16.11.17", - "@types/ws": "8.2.2", - "@typescript-eslint/eslint-plugin": "5.8.1", - "@typescript-eslint/parser": "5.8.1", - "discord-api-types": "0.26.1", - "eslint": "8.6.0", - "eslint-config-prettier": "8.3.0", - "eslint-plugin-prettier": "4.0.0", - "husky": "7.0.4", - "nodemon": "2.0.15", - "prettier": "2.5.1", - "prisma": "3.7.0", - "rimraf": "3.0.2", - "ts-node": "10.4.0", - "typescript": "4.5.4" - } -} diff --git a/prisma/migrations/20210821005711_init/migration.sql b/prisma/migrations/20210821005711_init/migration.sql deleted file mode 100644 index 87d670f5..00000000 --- a/prisma/migrations/20210821005711_init/migration.sql +++ /dev/null @@ -1,115 +0,0 @@ --- CreateEnum -CREATE TYPE "LogEvent" AS ENUM ('VOICE_MEMBER_DEAFENED', 'VOICE_CHANNEL_LEAVE', 'VOICE_CHANNEL_SWITCH', 'VOICE_CHANNEL_JOIN', 'VOICE_MEMBER_MUTED', 'MESSAGE_UPDATED', 'MESSAGE_DELETED', 'MEMBER_BOOSTED', 'THREAD_CREATED', 'THREAD_DELETED'); - --- CreateEnum -CREATE TYPE "GlobalBanType" AS ENUM ('GUILD', 'USER'); - --- CreateEnum -CREATE TYPE "PunishmentType" AS ENUM ('ALLOW_THREADS_AGAIN', 'WARNING_REMOVED', 'VOICE_UNDEAFEN', 'WARNING_ADDED', 'VOICE_UNMUTE', 'VOICE_DEAFEN', 'VOICE_MUTE', 'NO_THREADS', 'UNMUTE', 'UNBAN', 'KICK', 'MUTE', 'BAN'); - --- CreateTable -CREATE TABLE "automod" ( - "blacklisted_words" TEXT[], - "dehoisting" BOOLEAN NOT NULL DEFAULT false, - "shortlinks" BOOLEAN NOT NULL DEFAULT false, - "blacklist" BOOLEAN NOT NULL DEFAULT false, - "mentions" BOOLEAN NOT NULL DEFAULT false, - "guild_id" TEXT NOT NULL, - "invites" BOOLEAN NOT NULL DEFAULT false, - "spam" BOOLEAN NOT NULL DEFAULT false, - "raid" BOOLEAN NOT NULL DEFAULT false -); - --- CreateTable -CREATE TABLE "global_bans" ( - "reason" TEXT, - "issuer" TEXT NOT NULL, - "type" "GlobalBanType" NOT NULL, - "id" TEXT NOT NULL -); - --- CreateTable -CREATE TABLE "cases" ( - "attachments" TEXT[], - "moderator_id" TEXT NOT NULL, - "message_id" TEXT, - "victim_id" TEXT NOT NULL, - "guildId" TEXT NOT NULL, - "reason" TEXT, - "index" INTEGER NOT NULL, - "type" "PunishmentType" NOT NULL, - "soft" BOOLEAN NOT NULL, - "time" INTEGER -); - --- CreateTable -CREATE TABLE "guilds" ( - "modlog_channel_id" TEXT, - "muted_role_id" TEXT, - "prefixes" TEXT[], - "language" TEXT NOT NULL, - "guild_id" TEXT NOT NULL -); - --- CreateTable -CREATE TABLE "logging" ( - "ignored_channels" TEXT[], - "ignored_users" TEXT[], - "channel_id" TEXT, - "enabled" BOOLEAN NOT NULL, - "events" "LogEvent"[], - "guild_id" TEXT NOT NULL -); - --- CreateTable -CREATE TABLE "punishments" ( - "warnings" INTEGER NOT NULL, - "guild_id" TEXT NOT NULL, - "index" INTEGER NOT NULL, - "extra" JSONB, - "soft" BOOLEAN NOT NULL, - "time" TEXT, - "type" "PunishmentType" NOT NULL -); - --- CreateTable -CREATE TABLE "users" ( - "prefixes" TEXT[], - "language" TEXT NOT NULL, - "user_id" TEXT NOT NULL -); - --- CreateTable -CREATE TABLE "Warning" ( - "guild_id" TEXT NOT NULL, - "reason" TEXT, - "amount" INTEGER NOT NULL, - "user_id" TEXT NOT NULL -); - --- CreateIndex -CREATE UNIQUE INDEX "automod.guild_id_unique" ON "automod"("guild_id"); - --- CreateIndex -CREATE UNIQUE INDEX "global_bans.id_unique" ON "global_bans"("id"); - --- CreateIndex -CREATE UNIQUE INDEX "cases.guildId_unique" ON "cases"("guildId"); - --- CreateIndex -CREATE UNIQUE INDEX "guilds.guild_id_unique" ON "guilds"("guild_id"); - --- CreateIndex -CREATE UNIQUE INDEX "logging.guild_id_unique" ON "logging"("guild_id"); - --- CreateIndex -CREATE UNIQUE INDEX "punishments.guild_id_unique" ON "punishments"("guild_id"); - --- CreateIndex -CREATE UNIQUE INDEX "punishments.index_unique" ON "punishments"("index"); - --- CreateIndex -CREATE UNIQUE INDEX "users.user_id_unique" ON "users"("user_id"); - --- CreateIndex -CREATE UNIQUE INDEX "Warning.guild_id_unique" ON "Warning"("guild_id"); diff --git a/prisma/migrations/20210821005734_fix_warnings_table/migration.sql b/prisma/migrations/20210821005734_fix_warnings_table/migration.sql deleted file mode 100644 index b05d6bcb..00000000 --- a/prisma/migrations/20210821005734_fix_warnings_table/migration.sql +++ /dev/null @@ -1,19 +0,0 @@ -/* - Warnings: - - - You are about to drop the `Warning` table. If the table is not empty, all the data it contains will be lost. - -*/ --- DropTable -DROP TABLE "Warning"; - --- CreateTable -CREATE TABLE "warnings" ( - "guild_id" TEXT NOT NULL, - "reason" TEXT, - "amount" INTEGER NOT NULL, - "user_id" TEXT NOT NULL -); - --- CreateIndex -CREATE UNIQUE INDEX "warnings.guild_id_unique" ON "warnings"("guild_id"); diff --git a/prisma/migrations/20210821012219_did_i_fuck_up/migration.sql b/prisma/migrations/20210821012219_did_i_fuck_up/migration.sql deleted file mode 100644 index 5c4568e6..00000000 --- a/prisma/migrations/20210821012219_did_i_fuck_up/migration.sql +++ /dev/null @@ -1,29 +0,0 @@ -/* - Warnings: - - - You are about to drop the `warnings` table. If the table is not empty, all the data it contains will be lost. - - A unique constraint covering the columns `[guild_id,index]` on the table `punishments` will be added. If there are existing duplicate values, this will fail. - -*/ --- DropIndex -DROP INDEX "punishments.guild_id_unique"; - --- DropIndex -DROP INDEX "punishments.index_unique"; - --- DropTable -DROP TABLE "warnings"; - --- CreateTable -CREATE TABLE "Warning" ( - "guild_id" TEXT NOT NULL, - "reason" TEXT, - "amount" INTEGER NOT NULL, - "user_id" TEXT NOT NULL -); - --- CreateIndex -CREATE UNIQUE INDEX "Warning.guild_id_user_id_unique" ON "Warning"("guild_id", "user_id"); - --- CreateIndex -CREATE UNIQUE INDEX "punishments.guild_id_index_unique" ON "punishments"("guild_id", "index"); diff --git a/prisma/migrations/20210821012246_i_should_stop_this/migration.sql b/prisma/migrations/20210821012246_i_should_stop_this/migration.sql deleted file mode 100644 index befcf72d..00000000 --- a/prisma/migrations/20210821012246_i_should_stop_this/migration.sql +++ /dev/null @@ -1,19 +0,0 @@ -/* - Warnings: - - - You are about to drop the `Warning` table. If the table is not empty, all the data it contains will be lost. - -*/ --- DropTable -DROP TABLE "Warning"; - --- CreateTable -CREATE TABLE "warnings" ( - "guild_id" TEXT NOT NULL, - "reason" TEXT, - "amount" INTEGER NOT NULL, - "user_id" TEXT NOT NULL -); - --- CreateIndex -CREATE UNIQUE INDEX "warnings.guild_id_user_id_unique" ON "warnings"("guild_id", "user_id"); diff --git a/prisma/migrations/migration_lock.toml b/prisma/migrations/migration_lock.toml deleted file mode 100644 index fbffa92c..00000000 --- a/prisma/migrations/migration_lock.toml +++ /dev/null @@ -1,3 +0,0 @@ -# Please do not edit this file manually -# It should be added in your version-control system (i.e. Git) -provider = "postgresql" \ No newline at end of file diff --git a/prisma/schema.prisma b/prisma/schema.prisma deleted file mode 100644 index 7dc926f9..00000000 --- a/prisma/schema.prisma +++ /dev/null @@ -1,138 +0,0 @@ -datasource db { - provider = "postgresql" - url = env("DATABASE_URL") -} - -generator client { - provider = "prisma-client-js" -} - -enum LogEvent { - VOICE_MEMBER_DEAFENED - VOICE_CHANNEL_LEAVE - VOICE_CHANNEL_SWITCH - VOICE_CHANNEL_JOIN - VOICE_MEMBER_MUTED - MESSAGE_UPDATED - MESSAGE_DELETED - MEMBER_BOOSTED - THREAD_CREATED - THREAD_DELETED -} - -enum GlobalBanType { - GUILD - USER -} - -enum PunishmentType { - ALLOW_THREADS_AGAIN - WARNING_REMOVED - VOICE_UNDEAFEN - WARNING_ADDED - VOICE_UNMUTE - VOICE_DEAFEN - VOICE_MUTE - NO_THREADS - UNMUTE - UNBAN - KICK - MUTE - BAN -} - -model Automod { - blacklistedWords String[] @map("blacklisted_words") - dehoisting Boolean @default(false) - shortlinks Boolean @default(false) - blacklist Boolean @default(false) - mentions Boolean @default(false) - guildId String @map("guild_id") - invites Boolean @default(false) - spam Boolean @default(false) - raid Boolean @default(false) - - @@map("automod") - @@unique([guildId]) -} - -model GlobalBans { - reason String? - issuer String - type GlobalBanType - id String - - @@map("global_bans") - @@unique([id]) -} - -model Cases { - attachments String[] - moderatorId String @map("moderator_id") - message_id String? @map("message_id") - victimId String @map("victim_id") - guildId String - reason String? - index Int - type PunishmentType - soft Boolean - time Int? - - @@map("cases") - @@unique([guildId]) -} - -model Guild { - modlogChannelId String? @map("modlog_channel_id") - mutedRoleId String? @map("muted_role_id") - prefixes String[] - language String - guildId String @map("guild_id") - - @@map("guilds") - @@unique([guildId]) -} - -model Logging { - ignoreChannels String[] @map("ignored_channels") - ignoredUsers String[] @map("ignored_users") - channelId String? @map("channel_id") - enabled Boolean - events LogEvent[] - guildId String @map("guild_id") - - @@map("logging") - @@unique([guildId]) -} - -model Punishments { - warnings Int - guildId String @map("guild_id") - index Int - extra Json? - soft Boolean - time String? - type PunishmentType - - @@map("punishments") - @@unique([guildId, index]) -} - -model User { - prefixes String[] - language String - userId String @map("user_id") - - @@map("users") - @@unique([userId]) -} - -model Warning { - guildId String @map("guild_id") - reason String? - amount Int - userId String @map("user_id") - - @@map("warnings") - @@unique([guildId, userId]) -} diff --git a/renovate.json b/renovate.json index d66fcce8..9777658a 100644 --- a/renovate.json +++ b/renovate.json @@ -2,5 +2,5 @@ "extends": ["config:base"], "automerge": true, "baseBranches": ["edge"], - "ignoreDeps": ["typeorm"] + "ignoreDeps": ["is-docker"] } diff --git a/scripts/add-license.js b/scripts/add-license.js deleted file mode 100644 index d070f264..00000000 --- a/scripts/add-license.js +++ /dev/null @@ -1,74 +0,0 @@ -/** - * Copyright (c) 2019-2021 Nino - * - * 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. - */ - -const { readdir } = require('@augu/utils'); -const path = require('path'); -const fs = require('fs'); - -const LICENSE = `/** - * Copyright (c) 2019-${new Date().getFullYear()} Nino - * - * 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. - */ - -`; - -const main = async () => { - console.log('adding licenses at the top of files...'); - const files = await Promise.all([ - readdir(path.join(__dirname, '..', 'src')), - readdir(path.join(__dirname, '..', 'scripts')), - ]).then((arr) => arr.flat().filter((s) => s.endsWith('.js') || s.endsWith('.ts'))); - - for (const file of files) { - console.log(`Adding license to ${file}...`); - const content = fs.readFileSync(file, 'utf8'); - const raw = content.includes('* Copyright (c)') ? content.split('\n').slice(22).join('\n') : content; - - await fs.promises.writeFile(file, LICENSE + raw); - - console.log(`Added license to ${file} :D`); - } -}; - -main() - .then(() => process.exit(0)) - .catch((error) => { - console.error(error); - process.exit(1); - }); diff --git a/scripts/export-v1-db.js b/scripts/export-v1-db.js deleted file mode 100644 index 8e5daab7..00000000 --- a/scripts/export-v1-db.js +++ /dev/null @@ -1,172 +0,0 @@ -/** - * Copyright (c) 2019-2021 Nino - * - * 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. - */ - -/* eslint-disable camelcase */ - -const { mkdir, writeFile } = require('fs/promises'); -const { LoggerWithoutCallSite } = require('tslog'); -const { createConnection } = require('typeorm'); -const { existsSync } = require('fs'); -const { join } = require('path'); - -const { default: PunishmentsEntity } = require('../build/entities/PunishmentsEntity'); -const { default: AutomodEntity } = require('../build/entities/AutomodEntity'); -const { default: CaseEntity } = require('../build/entities/CaseEntity'); -const { default: GuildEntity } = require('../build/entities/GuildEntity'); -const { default: WarningsEntity } = require('../build/entities/WarningsEntity'); -const { default: LoggingEntity } = require('../build/entities/LoggingEntity'); -const { default: UserEntity } = require('../build/entities/UserEntity'); - -const logger = new LoggerWithoutCallSite({ - displayFunctionName: true, - exposeErrorCodeFrame: true, - displayInstanceName: true, - displayFilePath: false, - dateTimePattern: '[ day-month-year / hour:minute:second ]', - instanceName: 'script: v0 -> v1', - name: 'scripts', -}); - -const main = async () => { - logger.info('Welcome to the export script for migrating from v1 -> v2.'); - - const key = `.nino/migration.json`; - const connection = await createConnection(); - logger.info(`Established the connection with PostgreSQL. I will be exporting data in ${key}, hold tight!`); - - if (!existsSync(join(process.cwd(), '.nino'))) await mkdir(join(process.cwd(), '.nino')); - - const guilds = await connection.getRepository(GuildEntity).find(); - const users = await connection.getRepository(UserEntity).find(); - const punishments = await connection.getRepository(PunishmentsEntity).find(); - const automod = await connection.getRepository(AutomodEntity).find(); - const cases = await connection.getRepository(CaseEntity).find(); - const warnings = await connection.getRepository(WarningsEntity).find(); - const logging = await connection.getRepository(LoggingEntity).find(); - - logger.info('Retrieved all entities! Now exporting...'); - const data = { - version: 1, - ran_at: Date.now(), - blame: require('os').userInfo().username.replace('cutie', 'Noel'), - data: { - automod: [], - cases: [], - logging: [], - guilds: [], - punishments: [], - warnings: [], - users: [], - }, - }; - - logger.info(`Found ${cases.length} cases to export!`); - for (const model of cases) { - data.data.cases.push({ - attachments: model.attachments, - moderator_id: model.moderatorID, - message_id: model.messageID, - victim_id: model.victimID, - guild_id: model.guildID, - reason: model.reason, - index: model.index, - soft: model.soft, - time: model.time, - }); - } - - logger.info(`Found ${guilds.length.toLocaleString()} guilds to export.`); - for (const guild of guilds) { - data.data.guilds.push({ - guild_id: guild.guildID, - prefixes: guild.prefixes, - language: guild.language, - modlog_channel_id: guild.modlogChannelID, - muted_role_id: guild.mutedRoleID, - }); - } - - logger.info(`Found ${users.length.toLocaleString()} users.`); - for (const user of users) { - data.data.users.push({ - user_id: user.id, - language: user.language, - prefixes: user.prefixes, - }); - } - - logger.info(`Found ${punishments.length.toLocaleString()} punishments.`); - for (const punishment of punishments) { - data.data.punishments.push({ - warnings: punishment.warnings, - guild_id: punishment.guildID, - index: punishment.index, - soft: punishment.soft, - time: punishment.time, - days: punishment.days, - type: punishment.type, - }); - } - - logger.info(`Found ${automod.length.toLocaleString()} guild automod settings.`); - for (const auto of automod) { - data.data.automod.push({ - blacklisted_words: auto.blacklistWords, - short_links: auto.shortLinks, - blacklist: auto.blacklist, - mentions: auto.mentions, - invites: auto.invites, - dehoisting: auto.dehoist, - guild_id: auto.guildID, - spam: auto.spam, - raid: auto.raid, - }); - } - - logger.info(`Found ${warnings.length.toLocaleString()} warnings.`); - for (const warning of warnings) { - data.data.warnings.push({ - guild_id: warning.guildID, - reason: warning.reason, - amount: warning.amount, - user_id: warning.userID, - id: warning.id, - }); - } - - logger.info(`Found ${logging.length.toLocaleString()} guild logging settings.`); - for (const log of logging) { - data.data.logging.push({ - ignore_channel_ids: log.ignoreChannels, - ignore_user_ids: log.ignoreUsers, - channel_id: log.channelID, - enabled: log.enabled, - events: log.events, - guild_id: log.guildID, - }); - } - - await writeFile(key, JSON.stringify(data, null, '\t')); - logger.info(`File has been exported to ${key}!`); -}; - -main(); diff --git a/scripts/migrate.js b/scripts/migrate.js deleted file mode 100644 index 216bb99b..00000000 --- a/scripts/migrate.js +++ /dev/null @@ -1,310 +0,0 @@ -/** - * Copyright (c) 2019-2021 Nino - * - * 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. - */ - -const { LoggerWithoutCallSite } = require('tslog'); -const { calculateHRTime, readdir } = require('@augu/utils'); -const determineCaseType = require('./util/getCaseType'); -const getRepositories = require('./util/getRepositories'); -const { existsSync } = require('fs'); -const { readFile } = require('fs/promises'); -const { createConnection } = require('typeorm'); - -const { - default: PunishmentsEntity, -} = require('../build/entities/PunishmentsEntity'); -const { default: AutomodEntity } = require('../build/entities/AutomodEntity'); -const { default: CaseEntity } = require('../build/entities/CaseEntity'); -const { default: GuildEntity } = require('../build/entities/GuildEntity'); -const { default: WarningsEntity } = require('../build/entities/WarningsEntity'); -const { - default: LoggingEntity, - LoggingEvents, -} = require('../build/entities/LoggingEntity'); -const { default: UserEntity } = require('../build/entities/UserEntity'); - -const argv = process.argv.slice(2); -const logger = new LoggerWithoutCallSite({ - displayFunctionName: true, - exposeErrorCodeFrame: true, - displayInstanceName: true, - displayFilePath: false, - dateTimePattern: '[ day-month-year / hour:minute:second ]', - instanceName: 'script: v0 -> v1', - name: 'scripts', -}); - -async function main() { - logger.info('Welcome to the database conversion script!'); - logger.info( - 'This script takes care of converting the Mongo database to the PostgreSQL one!' - ); - - if (argv[0] === undefined) { - logger.fatal( - 'You are required to output a directory after `node scripts/migrate.js`.' - ); - process.exit(1); - } - - const directory = argv[0]; - if (!existsSync(directory)) { - logger.fatal(`Directory ${argv[0]} doesn't exist.`); - process.exit(1); - } - - const files = await readdir(directory); - if (!files.every((file) => file.endsWith('.json'))) { - logger.fatal('Every file should end with ".json"'); - process.exit(1); - } - - logger.info('Creating PostgreSQL instance...'); - const connection = await createConnection(); - - const startTime = process.hrtime(); - const guilds = files.find((file) => file.endsWith('guilds.json')); - const guildData = await readFile(guilds, 'utf-8'); - const guildDocs = JSON.parse(guildData); - await convertGuilds(connection, guildDocs); - - const users = files.find((file) => file.endsWith('users.json')); - const userData = await readFile(users, 'utf-8'); - const userDocs = JSON.parse(userData); - await convertUsers(connection, userDocs); - - const warnings = files.find((file) => file.endsWith('warnings.json')); - const warningData = await readFile(warnings, 'utf-8'); - const warningDocs = JSON.parse(warningData); - await convertWarnings(connection, warningDocs); - - const cases = files.find((file) => file.endsWith('cases.json')); - const caseData = await readFile(cases, 'utf-8'); - const caseDocs = JSON.parse(caseData); - await convertCases(connection, caseDocs); - - logger.info( - `Converted ${userDocs.length} users, ${guildDocs.length} guilds, ${ - warningDocs.length - } warnings, and ${caseDocs.length} cases in ~${calculateHRTime( - startTime - )}ms.` - ); - process.exit(0); -} - -async function convertGuilds(connection, documents) { - logger.info(`Found ${documents.length} documents to convert...`); - const { guilds, punishments, automod, logging } = getRepositories(connection); - - const start = process.hrtime(); - for (let i = 0; i < documents.length; i++) { - const document = documents[i]; - - logger.info( - `Guild Entry: ${document.guildID} ${i + 1}/${documents.length}` - ); - const entry = new GuildEntity(); - entry.language = document.locale; - entry.prefixes = [document.prefix]; - entry.guildID = document.guildID; - - if (document.modlog !== undefined) entry.modlogChannelID = document.modlog; - - if (document.mutedRole !== undefined) - entry.mutedRoleID = document.mutedRole; - - await guilds.save(entry); - - logger.info(`Converting ${document.punishments.length} punishments...`); - for (const punishment of document.punishments) { - if (punishment.type === 'unrole' || punishment.type === 'role') { - logger.warn('Removing legacy punishment...', punishment); - continue; - } - - const entry = new PunishmentsEntity(); - entry.warnings = punishment.warnings; - entry.guildID = document.guildID; - entry.soft = punishment.soft === true; - entry.type = determineCaseType(punishment.type); - - if (punishment.temp !== null) entry.time = punishment.temp; - - await punishments.save(entry); - } - - logger.info('Converting automod actions...'); - const automodEntry = new AutomodEntity(); - automodEntry.blacklistWords = document.automod.badwords?.wordlist ?? []; - automodEntry.blacklist = document.automod.badwords?.enabled ?? false; - automodEntry.shortLinks = false; // wasn't introduced in v0 - automodEntry.mentions = document.automod.mention ?? false; - automodEntry.invites = document.automod.invites ?? false; - automodEntry.dehoist = document.automod.dehoist ?? false; - automodEntry.guildID = document.guildID; - automodEntry.spam = document.automod.spam ?? false; - automodEntry.raid = document.automod.raid ?? false; - - await automod.save(automodEntry); - - logger.info('Converting logging actions...'); - const loggingEntry = new LoggingEntity(); - loggingEntry.guildID = document.guildID; - - const _logging = document.logging ?? { enabled: false }; - if (!_logging.enabled) { - loggingEntry.enabled = false; - await logging.save(loggingEntry); - } else { - const events = []; - - loggingEntry.enabled = true; - loggingEntry.channelID = _logging.channelID ?? null; - loggingEntry.ignoreUsers = _logging.ignore ?? []; - loggingEntry.ignoreChannels = _logging.ignoreChannels ?? []; - - if (_logging.events.messageDelete === true) - events.push(LoggingEvents.MessageDeleted); - - if (_logging.events.messageUpdate === true) - events.push(LoggingEvents.MessageUpdated); - - await logging.save(loggingEntry); - } - } - - logger.info( - `Hopefully migrated ${documents.length} guild documents (~${calculateHRTime( - start - ).toFixed(2)}ms)` - ); -} - -async function convertUsers(connection, documents) { - logger.info(`Found ${documents.length} users to convert.`); - const { users } = getRepositories(connection); - - const startTime = process.hrtime(); - for (let i = 0; i < documents.length; i++) { - const document = documents[i]; - logger.info( - `User Entry: ${document.userID} (${i + 1}/${documents.length})` - ); - - const entry = new UserEntity(); - entry.language = document.locale; - entry.prefixes = []; - entry.id = document.userID; - - const user = await users.find({ id: document.userID }); - await users.save(entry); - } - - logger.info( - `Hopefully migrated ${documents.length} user documents (~${calculateHRTime( - startTime - ).toFixed(2)}ms)` - ); -} - -async function convertWarnings(connection, documents) { - logger.info(`Found ${documents.length} warnings to convert.`); - const { warnings } = getRepositories(connection); - - const startTime = process.hrtime(); - for (let i = 0; i < documents.length; i++) { - logger.info( - `Warning Entry: ${documents[i].guild} | ${documents[i].user} (${i + 1}/${ - documents.length - })` - ); - - const document = documents[i]; - const entry = new WarningsEntity(); - - entry.amount = document.amount; - entry.guildID = document.guild; - entry.userID = document.user; - if (typeof document.reason === 'string') entry.reason = document.reason; - - try { - await warnings.save(entry); - } catch (ex) { - logger.error('Unable to serialize input, setting to `1`:', ex); - - entry.amount = 1; - await warnings.save(entry); - } - } - - logger.info( - `Hopefully migrated ${ - documents.length - } warning documents (~${calculateHRTime(startTime).toFixed(2)}ms)` - ); -} - -async function convertCases(connection, documents) { - logger.info(`Found ${documents.length} cases to convert.`); - const { cases } = getRepositories(connection); - - const startTime = process.hrtime(); - for (let i = 0; i < documents.length; i++) { - const document = documents[i]; - logger.info( - `Case Entry: ${document.guild}, ${document.victim}, #${document.id}` - ); - - const entry = new CaseEntity(); - entry.moderatorID = document.moderator; - entry.messageID = document.message; - entry.victimID = document.victim; - entry.guildID = document.guild; - entry.index = document.id; - entry.type = determineCaseType(document.type) ?? document.type; - entry.soft = document.soft === true; - - if (document.reason !== null) entry.reason = document.reason; - - if (document.time !== undefined) entry.time = document.time; - - try { - await cases.save(entry); - } catch (ex) { - logger.info(`Skipping on entity #${document.id}: `, ex); - continue; - } - } - - logger.info( - `Hopefully migrated ${documents.length} case documents (~${calculateHRTime( - startTime - ).toFixed(2)}ms)` - ); -} - -main() - .then(process.exit) - .catch((ex) => { - logger.fatal(ex); - process.exit(1); - }); diff --git a/scripts/migrations/v1.js b/scripts/migrations/v1.js deleted file mode 100644 index 22d4e5a9..00000000 --- a/scripts/migrations/v1.js +++ /dev/null @@ -1,81 +0,0 @@ -/** - * Copyright (c) 2019-2021 Nino - * - * 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. - */ - -require('../../build/util/patches/RequirePatch'); - -const { - promises: { readFile }, - existsSync, -} = require('fs'); - -const { PrismaClient } = require('.prisma/client'); -const { withIndex } = require('~/build/util'); -const { join } = require('path'); - -const prisma = new PrismaClient(); - -const main = async () => { - console.log('migrations: v1: migrating...'); - - const MIGRATION_DATA_PATH = join(process.cwd(), '.nino', 'migration.json'); - if (!existsSync(MIGRATION_DATA_PATH)) { - console.warn('migrations: v1: run `yarn export:v1:db` before migrating...'); - await prisma.$disconnect(); - - process.exit(1); - } - - const contents = await readFile(MIGRATION_DATA_PATH, 'utf-8'); - const data = JSON.parse(contents); - - console.log( - `migrations: v1: running v1 -> v2 migrations that was executed at ${new Date( - Date.now() - data.ran_at - ).toString()} by ${data.blame}` - ); - - console.log(`migrations: v1: bulk insert ${data.data.automod.length} automod objects.`); - for (const [index, automod] of withIndex(data.data.automod)) { - console.log(`#${index}:`, automod); - - await prisma.automod.create({ - data: { - blacklistedWords: automod.blacklisted_words, - shortlinks: automod.shortlinks, - blacklist: automod.blacklist, - mentions: automod.mentions, - invites: automod.invites, - dehoisting: automod.dehoisting, - guildId: automod.guild_id, - spam: automod.spam, - raid: automod.raid, - }, - }); - } - - console.log('we are now done here!'); - await prisma.$disconnect(); - - process.exit(0); -}; - -main(); diff --git a/scripts/run-docker.sh b/scripts/run-docker.sh deleted file mode 100644 index b01d1f73..00000000 --- a/scripts/run-docker.sh +++ /dev/null @@ -1,15 +0,0 @@ -#!/bin/bash - -echo 'Checking migration details...' -yarn prisma migrate status - -echo 'Running migrations' -yarn prisma migrate deploy - -echo '[Legacy] Running TypeORM migrations...' -if type "typeorm" > /dev/null; then - yarn typeorm migration:run -fi - -echo 'Migrations and schemas should be synced.' -npm start diff --git a/scripts/shortlinks.js b/scripts/shortlinks.js deleted file mode 100644 index fbab43ff..00000000 --- a/scripts/shortlinks.js +++ /dev/null @@ -1,135 +0,0 @@ -/** - * Copyright (c) 2019-2021 Nino - * - * 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. - */ - -const { LoggerWithoutCallSite } = require('tslog'); -const { calculateHRTime } = require('@augu/utils'); -const { existsSync } = require('fs'); -const { HttpClient } = require('@augu/orchid'); -const { version } = require('../package.json'); -const { join } = require('path'); -const fs = require('fs/promises'); - -const http = new HttpClient({ - userAgent: `Nino/DiscordBot (https://github.com/NinoDiscord/Nino, v${version})`, -}); - -const logger = new LoggerWithoutCallSite({ - displayFunctionName: true, - exposeErrorCodeFrame: true, - displayInstanceName: true, - displayFilePath: false, - dateTimePattern: '[ day-month-year / hour:minute:second ]', - instanceName: 'script: shortlinks', - name: 'scripts', -}); - -const otherUrls = [ - 'shorte.st', - 'adf.ly', - 'bc.vc', - 'soo.gd', - 'ouo.io', - 'zzb.bz', - 'adfoc.us', - 'goo.gl', - 'grabify.link', - 'shorturl.at', - 'tinyurl.com', - 'tinyurl.one', - 'rotf.lol', - 'bit.do', - 'is.gd', - 'owl.ly', - 'buff.ly', - 'mcaf.ee', - 'su.pr', - 'bfy.tw', - 'owo.vc', - 'tiny.cc', - 'rb.gy', - 'bl.ink', - 'v.gd', - 'vurl.com', - 'turl.ca', - 'shrunken.com', - 'p.asia', - 'g.asia', - '3.ly', - '0.gp', - '2.ly', - '4.gp', - '4.ly', - '6.ly', - '7.ly', - '8.ly', - '9.ly', - '2.gp', - '6.gp', - '5.gp', - 'ur3.us', - 'kek.gg', - 'waa.ai', - 'steamcommunity.ru', - 'steanconmunity.ru', - 'discord-nitro.link', -]; - -const startTime = process.hrtime(); -(async () => { - logger.info( - 'Now reading list from https://raw.githubusercontent.com/sambokai/ShortURL-Services-List/master/shorturl-services-list.csv...' - ); - - const requestTime = process.hrtime(); - const res = await http.get( - 'https://raw.githubusercontent.com/sambokai/ShortURL-Services-List/master/shorturl-services-list.csv' - ); - - const requestEnd = calculateHRTime(requestTime); - logger.debug(`It took ~${requestEnd}ms to get a "${res.statusCode}" response.`); - - const data = res.body().split(/\n\r?/); - data.shift(); - - const res2 = await http - .get('https://raw.githubusercontent.com/Andre601/anti-scam-database/main/database/summary.json') - .header('Accept', 'application/json'); - - // We don't care if one of the affiliate links is Steam, since - // Nino only operates on Discord. - const data2 = res2.json().filter((s) => s.affected_platforms.includes('discord')); - - const shortlinks = [ - ...new Set( - [].concat( - data.map((s) => s.slice(0, s.length - 1)), - otherUrls, - data2.map((s) => s.domain) - ) - ), - ].filter((s) => s !== ''); - - if (!existsSync(join(__dirname, '..', 'assets'))) await fs.mkdir(join(__dirname, '..', 'assets')); - await fs.writeFile(join(__dirname, '..', 'assets', 'shortlinks.json'), `${JSON.stringify(shortlinks, null, '\t')}\n`); - logger.info(`It took about ~${calculateHRTime(startTime)}ms to retrieve ${shortlinks.length} short-links.`); - process.exit(0); -})(); diff --git a/scripts/update-punishments.js b/scripts/update-punishments.js deleted file mode 100644 index 97473493..00000000 --- a/scripts/update-punishments.js +++ /dev/null @@ -1,94 +0,0 @@ -/** - * Copyright (c) 2019-2021 Nino - * - * 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. - */ - -const { createConnection } = require('typeorm'); -const { - default: PunishmentsEntity, -} = require('../build/entities/PunishmentsEntity'); - -async function main() { - console.log( - `[scripts:update-punishments | ${new Date().toLocaleTimeString()}] Updating punishments...` - ); - const connection = await createConnection(); - - // Retrieve all punishments - const repository = connection.getRepository(PunishmentsEntity); - const punishments = await repository.find({}); - const traversedGuilds = []; - let i = 0; - - await repository.delete({}); - - // Update punishments - console.log( - `[scripts:update-punishments | ${new Date().toLocaleTimeString()}] Found ${ - punishments.length - } punishments to update` - ); - for (const punishment of punishments) { - console.log( - `idx: ${i} | guild entry: ${ - traversedGuilds.find((c) => c.guild === punishment.guildID) !== - undefined - }` - ); - - if ( - traversedGuilds.find((c) => c.guild === punishment.guildID) !== undefined - ) { - const f = traversedGuilds.find((c) => c.guild === punishment.guildID); - const id = traversedGuilds.findIndex( - (c) => c.guild === punishment.guildID - ); - const entity = new PunishmentsEntity(); - - entity.warnings = punishment.warnings; - entity.guildID = punishment.guildID; - entity.index = f.index += 1; - entity.soft = punishment.soft; - entity.time = punishment.time; - entity.type = punishment.type; - entity.days = punishment.days; - - traversedGuilds[id] = { - guild: punishment.guildID, - index: (f.index += 1), - }; - await repository.save(entity); - } else { - const entity = new PunishmentsEntity(); - entity.warnings = punishment.warnings; - entity.guildID = punishment.guildID; - entity.index = 1; - entity.soft = punishment.soft; - entity.time = punishment.time; - entity.type = punishment.type; - entity.days = punishment.days; - - traversedGuilds.push({ guild: punishment.guildID, index: 1 }); - await repository.save(entity); - } - } -} - -main(); diff --git a/scripts/util/getRepositories.js b/scripts/util/getRepositories.js deleted file mode 100644 index 1c7b66dd..00000000 --- a/scripts/util/getRepositories.js +++ /dev/null @@ -1,51 +0,0 @@ -/** - * Copyright (c) 2019-2021 Nino - * - * 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. - */ - -const { - default: PunishmentsEntity, -} = require('../../build/entities/PunishmentsEntity'); -const { - default: AutomodEntity, -} = require('../../build/entities/AutomodEntity'); -const { default: CaseEntity } = require('../../build/entities/CaseEntity'); -const { default: GuildEntity } = require('../../build/entities/GuildEntity'); -const { - default: WarningsEntity, -} = require('../../build/entities/WarningsEntity'); -const { - default: LoggingEntity, -} = require('../../build/entities/LoggingEntity'); -const { default: UserEntity } = require('../../build/entities/UserEntity'); - -/** - * Returns the repositories from the responding `connection`. - * @param {import('typeorm').Connection} connection The connection established - */ -module.exports = (connection) => ({ - punishments: connection.getRepository(PunishmentsEntity), - warnings: connection.getRepository(WarningsEntity), - logging: connection.getRepository(LoggingEntity), - automod: connection.getRepository(AutomodEntity), - guilds: connection.getRepository(GuildEntity), - cases: connection.getRepository(CaseEntity), - users: connection.getRepository(UserEntity), -}); diff --git a/src/@types/eris.d.ts b/settings.gradle.kts similarity index 58% rename from src/@types/eris.d.ts rename to settings.gradle.kts index 2c9a5d0f..a014c67e 100644 --- a/src/@types/eris.d.ts +++ b/settings.gradle.kts @@ -1,5 +1,5 @@ /** - * Copyright (c) 2019-2021 Nino + * Copyright (c) 2019-2022 Nino * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -20,30 +20,40 @@ * SOFTWARE. */ -import Eris from 'eris'; -import MessageCollector from '../structures/MessageCollector'; +rootProject.name = "Nino" -declare module 'eris' { - interface Collection extends Map { - get(key: string | number): T | undefined; - get(key: string | number): V; +// Slash commands implementation for Nino +include(":bot:slash-commands") - values(): IterableIterator; - values(): IterableIterator; +// Punishments core + utilities +include(":bot:punishments") - filter(func: (i: T) => boolean): T[]; - filter(func: (i: V) => boolean): V[]; - } +// Automod core + utilities +include(":bot:automod") - interface Guild { - channels: Collection; - } +// Text-based commands +include(":bot:commands") - interface User { - tag: string; - } +// Database models + transaction API +include(":bot:database") - interface Message { - collector: MessageCollector; - } -} +// Kotlin client for timeouts microservice +include(":bot:timeouts") + +// Markup language for custom messages +include(":bot:markup") + +// Common utilities + extensions +include(":bot:commons") + +// Prometheus metrics registry +include(":bot:metrics") + +// Core components that ties everything in +include(":bot:core") + +// Bot API (+ Slash Commands impl) +include(":bot:api") + +// Main bot directory +include(":bot") diff --git a/src/@types/global.d.ts b/src/@types/global.d.ts deleted file mode 100644 index 42969cb1..00000000 --- a/src/@types/global.d.ts +++ /dev/null @@ -1,100 +0,0 @@ -/** - * Copyright (c) 2019-2021 Nino - * - * 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. - */ - -/* eslint-disable camelcase */ - -import { Container } from '@augu/lilith'; -import { APIUser } from 'discord-api-types'; -import { crypto } from '../api/encryption'; - -declare global { - /** The current container running */ - var app: Container; - - namespace NodeJS { - interface Global { - /** The current container running */ - app: Container; - } - } - - interface APITokenResult extends Omit { - expiryDate: number; - data: APIUser; - id: string; - } - - interface RedisInfo { - total_connections_received: number; - total_commands_processed: number; - instantaneous_ops_per_sec: number; - total_net_input_bytes: number; - total_net_output_bytes: number; - instantaneous_input_kbps: number; - instantaneous_output_kbps: number; - rejected_connections: number; - sync_full: number; - sync_partial_ok: number; - sync_partial_err: number; - expired_keys: number; - expired_stale_perc: number; - expired_time_cap_reached_count: number; - evicted_keys: number; - keyspace_hits: number; - keyspace_misses: number; - pubsub_channels: number; - pubsub_patterns: number; - latest_fork_usec: number; - migrate_cached_sockets: number; - slave_expires_tracked_keys: number; - active_defrag_hits: number; - active_defrag_misses: number; - active_defrag_key_hits: number; - active_defrag_key_misses: number; - } - - interface RedisServerInfo { - redis_version: string; - redis_git_sha1: string; - redis_git_dirty: string; - redis_build_id: string; - redis_mode: string; - os: string; - arch_bits: string; - multiplexing_api: string; - atomicvar_api: string; - gcc_version: string; - process_id: string; - process_supervised: string; - run_id: string; - tcp_port: string; - server_time_usec: string; - uptime_in_seconds: string; - uptime_in_days: string; - hz: string; - configured_hz: string; - lru_clock: string; - executable: string; - config_file: string; - io_threads_active: string; - } -} diff --git a/src/@types/ioredis-lock.d.ts b/src/@types/ioredis-lock.d.ts deleted file mode 100644 index 67b32ab1..00000000 --- a/src/@types/ioredis-lock.d.ts +++ /dev/null @@ -1,102 +0,0 @@ -/** - * Copyright (c) 2019-2021 Nino - * - * 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. - */ - -/** */ -declare module 'ioredis-lock' { - import * as ioredis from 'ioredis'; - - interface RedisLockOptions { - timeout?: number; - retries?: number; - delay?: number; - } - - /** - * Creates and returns a new Lock instance, configured for use with the supplied redis client, as well as options, if provided. - * The options object may contain following three keys, as outlined at the start of the documentation: timeout, retries and delay. - */ - export function createLock(client: ioredis.Redis | ioredis.Cluster, options?: RedisLockOptions): Lock; - - /** - * Returns an array of currently active/acquired locks. - */ - export function getAcquiredLocks(): Promise; - - /** - * The constructor for a LockAcquisitionError. Thrown or returned when a lock - * could not be acquired. - */ - export class LockAcquisitionError extends Error { - constructor(message: string); - } - - /** - * The constructor for a LockReleaseError. Thrown or returned when a lock - * could not be released. - */ - export class LockReleaseError extends Error { - constructor(message: string); - } - - /** - * The constructor for a LockExtendError. Thrown or returned when a lock - * could not be extended. - */ - export class LockExtendError extends Error { - constructor(message: string); - } - - /** - * The constructor for a Lock object. Accepts both a redis client, as well as - * an options object with the following properties: timeout, retries and delay. - * Any options not supplied are subject to the current defaults. - */ - export class Lock { - public readonly config: RedisLockOptions; - - /** - * Attempts to acquire a lock, given a key, and an optional callback function. - * If the initial lock fails, additional attempts will be made for the - * configured number of retries, and padded by the delay. The callback is - * invoked with an error on failure, and returns a promise if no callback is - * supplied. If invoked in the context of a promise, it may throw a - * LockAcquisitionError. - * - * @param key The redis key to use for the lock - */ - public acquire(key: string): Promise; - - /** - * Attempts to extend the lock - * @param expire in `timeout` seconds - */ - public extend(expire: number): Promise; - - /** - * Attempts to release the lock, and accepts an optional callback function. - * The callback is invoked with an error on failure, and returns a promise - * if no callback is supplied. If invoked in the context of a promise, it may - * throw a LockReleaseError. - */ - public release(): Promise; - } -} diff --git a/src/@types/locale.d.ts b/src/@types/locale.d.ts deleted file mode 100644 index 343bbfd7..00000000 --- a/src/@types/locale.d.ts +++ /dev/null @@ -1,160 +0,0 @@ -/** - * Copyright (c) 2019-2021 Nino - * - * 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. - */ - -/* eslint-disable camelcase */ - -export {}; -/** */ -declare global { - /** - * Metadata for a locale, this is used in the "meta" object - */ - interface LocalizationMeta { - /** List of contributors (by user ID) who helped translate or fix minor issues with this Locale */ - contributors: string[]; - - /** The translator's ID */ - translator: string; - - /** Any additional aliases to use when setting or resetting a locale */ - aliases: string[]; - - /** The flag's emoji (example: `:flag_us:`) */ - flag: string; - - /** The full name of the Locale (i.e `English (UK)`) */ - full: string; - - /** The locale's code (i.e `en_US`) */ - code: string; - } - - interface LocalizationStrings { - descriptions: LocalizationStrings.Descriptions; - commands: LocalizationStrings.Commands; - automod: LocalizationStrings.Automod; - generic: LocalizationStrings.Generic; - errors: LocalizationStrings.Errors; - } - - namespace LocalizationStrings { - export interface Descriptions { - // Unknown - unknown: string; - - // Core - help: string; - invite: string; - locale: string; - ping: string; - shardinfo: string; - source: string; - statistics: string; - uptime: string; - - // Moderation - ban: string; - case: string; - kick: string; - mute: string; - pardon: string; - purge: string; - reason: string; - softban: string; - timeouts: string; - unban: string; - unmute: string; - warn: string; - warnings: string; - voice_mute: string; - voice_deafen: string; - voice_undeafen: string; - voice_unmute: string; - - // Settings - automod: string; - logging: string; - modlog: string; - muted_role: string; - prefix: string; - punishments: string; - } - - export interface Commands { - help: { - embed: { - title: string; - description: string[]; - fields: { - moderation: string; - core: string; - settings: string; - }; - }; - - command: { - not_found: string; - embed: { - title: string; - description: string; - fields: { - syntax: string; - category: string; - aliases: string; - owner_only: string; - cooldown: string; - user_perms: string; - bot_perms: string; - examples: string; - }; - }; - }; - - module: { - embed: { - title: string; - }; - }; - - usage_title: string; - usage: string[]; - }; - - invite: string[]; - } - - export interface Automod { - blacklist: string; - invites: string; - mentions: string; - shortlinks: string; - spam: string; - raid: Record<'locked' | 'unlocked', string>; - } - - // eslint-disable-next-line - export interface Generic {} - - // eslint-disable-next-line - export interface Errors {} - } -} diff --git a/src/@types/reflect-metadata.d.ts b/src/@types/reflect-metadata.d.ts deleted file mode 100644 index 5964d683..00000000 --- a/src/@types/reflect-metadata.d.ts +++ /dev/null @@ -1,49 +0,0 @@ -/** - * Copyright (c) 2019-2021 Nino - * - * 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. - */ - -/** */ -declare namespace Reflect { - /** - * Gets the metadata value for the provided metadata key on the target object or its prototype chain. - * @param metadataKey A key used to store and retrieve metadata. - * @param target The target object on which the metadata is defined. - * @returns The metadata value for the metadata key if found; otherwise, `undefined`. - * @example - * - * class Example { - * } - * - * // constructor - * result = Reflect.getMetadata("custom:annotation", Example); - * - */ - function getMetadata(metadataKey: any, target: any): T; - - /** - * Gets the metadata value for the provided metadata key on the target object or its prototype chain. - * @param metadataKey A key used to store and retrieve metadata. - * @param target The target object on which the metadata is defined. - * @param propertyKey The property key for the target. - * @returns The metadata value for the metadata key if found; otherwise, `undefined`. - */ - function getMetadata(metadataKey: any, target: any, propertyKey: string | symbol): T; -} diff --git a/src/api/API.ts b/src/api/API.ts deleted file mode 100644 index fc3ce727..00000000 --- a/src/api/API.ts +++ /dev/null @@ -1,165 +0,0 @@ -/** - * Copyright (c) 2019-2021 Nino - * - * 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. - */ - -/* eslint-disable camelcase */ - -import fastify, { FastifyReply, FastifyRequest } from 'fastify'; -import { Component, Inject } from '@augu/lilith'; -import LocalizationService from '../services/LocalizationService'; -import CommandService from '../services/CommandService'; -import { pluralize } from '@augu/utils'; -import { Logger } from 'tslog'; -import Config from '../components/Config'; - -@Component({ - priority: 0, - name: 'api', -}) -export default class API { - @Inject - private readonly localization!: LocalizationService; - - @Inject - private readonly commands!: CommandService; - - @Inject - private readonly logger!: Logger; - - @Inject - private readonly config!: Config; - private instance!: ReturnType; - - async load() { - const enabled = this.config.getProperty('api'); - if (!enabled) { - this.logger.warn("Internal API is not enabled, this isn't recommended on self-hosted instances."); - return; - } - - const server = fastify(); - server - .register(require('fastify-no-icon')) - .register(require('fastify-cors')); - - this.instance = server; - this.setupRoutes(); - - let resolver!: any; - let rejecter!: any; - - const promise = new Promise((resolve, reject) => { - resolver = resolve; - rejecter = reject; - }); - - server.listen(22345, (error, address) => { - if (error) return rejecter(error); - - this.logger.info(`🚀 Launched internal API on ${address}`); - resolver(); - }); - - return promise; - } - - private setupRoutes() { - this.logger.info('🚀 Setting up routing...'); - - this.instance - .get('/', (_, res) => void res.redirect('https://nino.sh')) - .get('/commands', (req, res) => { - this._handleAllCommandsRoute.call(this, req, res); - }) - .get('/commands/:name', (req, res) => { - this._handleSingleCommandLookup.call(this, req, res); - }); - } - - private _handleAllCommandsRoute(req: FastifyRequest, reply: FastifyReply) { - const query = req.query as Record<'locale' | 'l', string>; - let locale = - query['l'] !== undefined || query['locale'] !== undefined - ? this.localization.locales.find( - (l) => - l.code === query.l || - l.code === query.locale || - l.aliases.includes(query.l) || - l.aliases.includes(query.locale) - ) - : this.localization.defaultLocale; - - if (locale === null) locale = this.localization.defaultLocale; - - const prefix = this.config.getProperty('prefixes')?.[0] ?? 'x!'; - const commands = this.commands - .filter((s) => !s.ownerOnly || !s.hidden) - .map((command) => ({ - name: command.name, - description: locale!.translate(command.description), - examples: command.examples.map((s) => `${prefix}${s}`), - usage: command.format, - aliases: command.aliases, - cooldown: pluralize('second', command.cooldown), - category: command.category, - user_permissions: command.userPermissions, - bot_permissions: command.botPermissions, - })); - - return reply.status(200).send(commands); - } - - private _handleSingleCommandLookup(req: FastifyRequest, reply: FastifyReply) { - const query = (req.query as Record<'locale' | 'l', string>) ?? ({} as Record<'locale' | 'l', string>); - const params = req.params as Record<'name', string>; - let locale = - query['l'] !== undefined || query['locale'] !== undefined - ? this.localization.locales.find( - (l) => - l.code === query.l || - l.code === query.locale || - l.aliases.includes(query.l) || - l.aliases.includes(query.locale) - ) - : this.localization.defaultLocale; - - if (locale === null) locale = this.localization.defaultLocale; - - const prefix = this.config.getProperty('prefixes')?.[0] ?? 'x!'; - const command = this.commands.filter((s) => !s.ownerOnly || !s.hidden).filter((s) => s.name === params.name)[0]; - if (!command) - return void reply.status(404).send({ - message: `Command ${params.name} was not found.`, - }); - - return void reply.status(200).send({ - name: command.name, - description: command.description, - examples: command.examples.map((s) => `${prefix}${s}`), - usage: command.format, - aliases: command.aliases, - cooldown: pluralize('second', command.cooldown), - category: command.category, - user_permissions: command.userPermissions, - bot_permissions: command.botPermissions, - }); - } -} diff --git a/src/automod/Blacklist.ts b/src/automod/Blacklist.ts deleted file mode 100644 index 097c4c26..00000000 --- a/src/automod/Blacklist.ts +++ /dev/null @@ -1,78 +0,0 @@ -/** - * Copyright (c) 2019-2021 Nino - * - * 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. - */ - -import type { Message, TextChannel } from 'eris'; -import LocalizationService from '../services/LocalizationService'; -import PunishmentService from '../services/PunishmentService'; -import PermissionUtil from '../util/Permissions'; -import { Automod } from '../structures'; -import { Inject } from '@augu/lilith'; -import Database from '../components/Database'; -import Discord from '../components/Discord'; - -export default class BlacklistAutomod implements Automod { - public name: string = 'blacklists'; - - @Inject - private readonly locales!: LocalizationService; - - @Inject - private readonly punishments!: PunishmentService; - - @Inject - private readonly database!: Database; - - @Inject - private readonly discord!: Discord; - - async onMessage(msg: Message) { - const settings = await this.database.automod.get(msg.guildID); - if (settings === undefined || settings.blacklist === false) return false; - - if (!msg || msg === null) return false; - - const nino = msg.channel.guild.members.get(this.discord.client.user.id)!; - - if ( - (msg.member !== null && !PermissionUtil.isMemberAbove(nino, msg.member)) || - !msg.channel.permissionsOf(this.discord.client.user.id).has('manageMessages') || - msg.author.bot || - msg.channel.permissionsOf(msg.author.id).has('banMembers') - ) - return false; - - const content = msg.content.toLowerCase().split(' '); - for (const word of settings.blacklistWords) { - if (content.filter((c) => c === word.toLowerCase()).length > 0) { - const language = this.locales.get(msg.guildID, msg.author.id); - - await msg.channel.createMessage(language.translate('automod.blacklist')); - await msg.delete(); - await this.punishments.createWarning(msg.member, '[Automod] User said a word that is blacklisted here (☍﹏⁰)。'); - - return true; - } - } - - return false; - } -} diff --git a/src/automod/Invites.ts b/src/automod/Invites.ts deleted file mode 100644 index 19837b41..00000000 --- a/src/automod/Invites.ts +++ /dev/null @@ -1,114 +0,0 @@ -/** - * Copyright (c) 2019-2021 Nino - * - * 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. - */ - -import { DiscordRESTError, Invite, Message, TextChannel } from 'eris'; -import LocalizationService from '../services/LocalizationService'; -import PunishmentService from '../services/PunishmentService'; -import * as Constants from '../util/Constants'; -import PermissionUtil from '../util/Permissions'; -import { Automod } from '../structures'; -import { Inject } from '@augu/lilith'; -import Database from '../components/Database'; -import Discord from '../components/Discord'; - -export default class Invites implements Automod { - public name: string = 'invites'; - - @Inject - private readonly punishments!: PunishmentService; - - @Inject - private readonly database!: Database; - - @Inject - private readonly locales!: LocalizationService; - - @Inject - private readonly discord!: Discord; - - async onMessage(msg: Message) { - const settings = await this.database.automod.get(msg.guildID); - if (settings === undefined || settings.invites === false) return false; - - if (!msg || msg === null) return false; - - const nino = msg.channel.guild.members.get(this.discord.client.user.id)!; - - if ( - (msg.member !== null && !PermissionUtil.isMemberAbove(nino, msg.member)) || - !msg.channel.permissionsOf(this.discord.client.user.id).has('manageMessages') || - msg.author.bot || - msg.channel.permissionsOf(msg.author.id).has('banMembers') - ) - return false; - - if (msg.content.match(Constants.DISCORD_INVITE_REGEX) !== null) { - const invites = await msg.channel.guild - .getInvites() - .then((invites) => invites.map((i) => i.code)) - .catch(() => [] as unknown as string[]); - - // Guild#getInvites doesn't add vanity urls, so we have to do it ourselves - if (msg.channel.guild.features.includes('VANITY_URL') && msg.channel.guild.vanityURL !== null) - invites.push(msg.channel.guild.vanityURL); - - const regex = Constants.DISCORD_INVITE_REGEX.exec(msg.content); - if (regex === null) return false; - - const code = regex[0]?.split('/').pop(); - if (code === undefined) return false; - - let invalid = false; - try { - const invite = await this.discord.client.requestHandler - .request('GET', `/invites/${code}`, true) - .then((data) => new Invite(data as any, this.discord.client)) - .catch(() => null); - - if (invite === null) { - invalid = true; - } else { - const hasInvite = invites.filter((inv) => inv === invite.code).length > 0; - if (!hasInvite && invite.guild !== undefined && invite.guild.id === msg.channel.guild.id) - invites.push(invite.code); - } - } catch (ex) { - if (ex instanceof DiscordRESTError && ex.code === 100006 && ex.message.includes('Unknown Invite')) - invalid = true; - } - - if (invalid) return false; - - if (invites.find((inv) => inv === code)) return false; - - const language = this.locales.get(msg.guildID, msg.author.id); - - await msg.channel.createMessage(language.translate('automod.invites')); - await msg.delete(); - await this.punishments.createWarning(msg.member, `[Automod] Advertising in ${msg.channel.mention}`); - - return true; - } - - return false; - } -} diff --git a/src/automod/Mentions.ts b/src/automod/Mentions.ts deleted file mode 100644 index c6f79299..00000000 --- a/src/automod/Mentions.ts +++ /dev/null @@ -1,78 +0,0 @@ -/** - * Copyright (c) 2019-2021 Nino - * - * 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. - */ - -import type { Message, TextChannel } from 'eris'; -import LocalizationService from '../services/LocalizationService'; -import PunishmentService from '../services/PunishmentService'; -import PermissionUtil from '../util/Permissions'; -import { Automod } from '../structures'; -import { Inject } from '@augu/lilith'; -import Database from '../components/Database'; -import Discord from '../components/Discord'; - -export default class Mentions implements Automod { - public name: string = 'mentions'; - - @Inject - private readonly locales!: LocalizationService; - - @Inject - private readonly punishments!: PunishmentService; - - @Inject - private readonly database!: Database; - - @Inject - private readonly discord!: Discord; - - async onMessage(msg: Message) { - const settings = await this.database.automod.get(msg.guildID); - if (settings === undefined || settings.invites === false) return false; - - if (!msg || msg === null) return false; - - const nino = msg.channel.guild.members.get(this.discord.client.user.id)!; - - if ( - (msg.member !== null && !PermissionUtil.isMemberAbove(nino, msg.member)) || - !msg.channel.permissionsOf(this.discord.client.user.id).has('manageMessages') || - msg.author.bot || - msg.channel.permissionsOf(msg.author.id).has('banMembers') - ) - return false; - - if (msg.mentions.length >= 4) { - const language = this.locales.get(msg.guildID, msg.author.id); - - await msg.channel.createMessage(language.translate('automod.mentions')); - await msg.delete(); - await this.punishments.createWarning( - msg.member, - `[Automod] Mentioned 4 or more people in ${msg.channel.mention}` - ); - - return true; - } - - return false; - } -} diff --git a/src/automod/Raid.ts b/src/automod/Raid.ts deleted file mode 100644 index 4253c897..00000000 --- a/src/automod/Raid.ts +++ /dev/null @@ -1,273 +0,0 @@ -/** - * Copyright (c) 2019-2021 Nino - * - * 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. - */ - -import type { Message, TextChannel, Overwrite } from 'eris'; -import LocalizationService from '../services/LocalizationService'; -import { PunishmentType } from '@prisma/client'; -import PunishmentService from '../services/PunishmentService'; -import PermissionUtil from '../util/Permissions'; -import { isObject } from '@augu/utils'; -import { Automod } from '../structures'; -import * as luxon from 'luxon'; -import { Inject } from '@augu/lilith'; -import Database from '../components/Database'; -import Discord from '../components/Discord'; -import Redis from '../components/Redis'; - -interface RaidChannelLock { - affectedIn: string; - state: { - channelID: string; - position: RaidChannelLockPositionArray[]; - }[]; -} - -interface RaidChannelLockPositionArray { - allow: bigint; - deny: bigint; - type: Overwrite['type']; - id: string; -} - -interface IBigIntSerialized { - value: number; - bigint: boolean; -} - -// Serializer for JSON.stringify for bigints -const bigintSerializer = (_: string, value: unknown) => { - if (typeof value === 'bigint') return { value: Number(value), bigint: true }; - else return value; -}; - -// Deserializer for JSON.parse for bigints -const bigintDeserializer = (_: string, value: unknown) => { - if (isObject(value) && value.bigint === true) return BigInt(value.value); - else return value; -}; - -export default class RaidAutomod implements Automod { - public name = 'raid'; - - @Inject - private readonly punishments!: PunishmentService; - - @Inject - private readonly locales!: LocalizationService; - - @Inject - private readonly database!: Database; - - @Inject - private readonly discord!: Discord; - - @Inject - private readonly redis!: Redis; - - async onMessage(msg: Message) { - const settings = await this.database.automod.get(msg.guildID); - - if (settings === undefined || settings.raid === false) return false; - - if (!msg || msg === null) return false; - - const nino = msg.channel.guild.members.get(this.discord.client.user.id)!; - - if ( - (msg.member !== null && !PermissionUtil.isMemberAbove(nino, msg.member)) || - !msg.channel.permissionsOf(this.discord.client.user.id).has('manageMessages') || - msg.author.bot || - msg.channel.permissionsOf(msg.author.id).has('banMembers') || - msg.member.joinedAt === null // in v9, it can be null for some reason? - ) - return false; - - // A raid can happen with 25-100+ pings - const joinedAt = luxon.DateTime.fromJSDate(new Date(msg.member.joinedAt)); - const now = luxon.DateTime.now(); - const difference = Math.floor(now.diff(joinedAt, ['days']).days); - //const language = this.locales.get(msg.guildID, msg.author.id); - - if (msg.mentions.length > 20 && difference < 3) { - // Lockdown all channels @everyone has access in - // await this._lockChannels(msg.channel.id, msg.channel.guild); - // await msg.channel.createMessage(language.translate('automod.raid.locked')); - - try { - await this.punishments.apply({ - moderator: this.discord.client.user, - publish: true, - reason: `[Automod] Raid in <#${msg.channel.id}> (Pinged ${msg.mentions.length} users)`, - member: { - guild: msg.channel.guild, - id: msg.author.id, - }, - soft: false, - type: PunishmentType.BAN, - }); - } catch { - // skip if we can't ban the user - } - - return true; - - // if (this._raidLocks.hasOwnProperty(msg.channel.guild.id)) { - // await this._raidLocks[msg.channel.guild.id].lock.extend(`raid:lockdown:${msg.guildID}`); - // clearTimeout(this._raidLocks[msg.channel.guild.id].timeout); - - // // Create a new timeout - // this._raidLocks[msg.channel.guild.id].timeout = setTimeout(async() => { - // const _raidLock = this._raidLocks[msg.guildID]; - // if (_raidLock !== undefined) { - // try { - // await _raidLock.lock.release(); - // } catch { - // // ignore if we can't release the lock - // } - - // await this._restore(msg.channel.guild); - // await msg.channel.createMessage(language.translate('automod.raid.unlocked')); - // await this.redis.client.del(`nino:raid:lockdown:indicator:${msg.guildID}`); - - // delete this._raidLocks[msg.guildID]; - // } - // }, 3000); - - // return true; - // } else { - // const timeout = setTimeout(async() => { - // const _raidLock = this._raidLocks[msg.guildID]; - // if (_raidLock !== undefined) { - // try { - // await _raidLock.lock.release(); - // } catch { - // // ignore if we can't release the lock - // } - - // await this._restore(msg.channel.guild); - // await msg.channel.createMessage(language.translate('automod.raid.unlocked')); - // await this.redis.client.del(`nino:raid:lockdown:indicator:${msg.guildID}`); - - // delete this._raidLocks[msg.guildID]; - // } - // }, 3000); - - // const lock = RedisLock.create(); - // await lock.acquire(`raid:lockdown:${msg.guildID}`); - - // this._raidLocks[msg.guildID] = { - // timeout, - // lock - // }; - - // await this.redis.client.set(`nino:raid:lockdown:indicator:${msg.guildID}`, msg.guildID); - // return true; - // } - } - - return false; - } - - // protected async _lockChannels(affectedID: string, guild: Guild) { - // // Retrieve old permissions - // const state = guild.channels.map(channel => ({ - // channelID: channel.id, - // position: channel.permissionOverwrites - // .filter(overwrite => overwrite.type === 'role') - // .map(overwrite => ({ - // allow: overwrite.allow, - // deny: overwrite.deny, - // type: overwrite.type, - // id: overwrite.id - // })) - // })); - - // await this.redis.client.hset('nino:raid:lockdowns:channels', guild.id, JSON.stringify({ affectedIn: affectedID, state }, bigintSerializer)); - - // const automod = await this.database.automod.get(guild.id); - // const filter = (channel: TextChannel) => - // channel.type === 0 && - // !automod!.whitelistChannelsDuringRaid.includes(channel.id); - - // // Change @everyone's permissions in all text channels - // for (const channel of guild.channels.filter(filter)) { - // const allow = channel.permissionOverwrites.has(guild.id) ? channel.permissionOverwrites.get(guild.id)!.allow : 0n; - // const deny = channel.permissionOverwrites.has(guild.id) ? channel.permissionOverwrites.get(guild.id)!.deny : 0n; - - // // this shouldn't happen but whatever - // // Checks if `deny` can be shifted with `sendMessages` - // if (!!(deny & Constants.Permissions.sendMessages) === true) - // continue; - - // await channel.editPermission( - // /* role id */ guild.id, - // /* allowed */ allow & ~Constants.Permissions.sendMessages, - // /* denied */ deny | Constants.Permissions.sendMessages, - // /* type */ 'role', - // /* reason */ '[Lockdown] Raid occured.' - // ); - // } - // } - - // protected async _restore(guild: Guild) { - // const locks = await this.redis.client.hget('nino:raid:lockdowns:channels', guild.id) - // .then(data => data !== null ? JSON.parse(data, bigintDeserializer) : null) - // .catch(() => null) as RaidChannelLock | null; - - // if (locks !== null) { - // // Release the locks of the channels - // const channel = guild.channels.get(locks.affectedIn); - // if (channel !== undefined && channel.type === 0) { - // const overwrite = channel.permissionOverwrites.get(guild.id); - // await channel.editPermission( - // guild.id, - // overwrite?.allow ?? 0n, - // overwrite?.deny ?? 0n, - // 'role', - // '[Lockdown] Raid lock has been released.' - // ); - // } - - // for (const { channelID, position } of locks.state.filter(c => c.channelID !== locks.affectedIn)) { - // const channel = guild.channels.get(channelID); - // if (channel !== undefined && channel.type !== 0) - // continue; - - // for (const pos of position) { - // try { - // await channel!.editPermission( - // pos.id, - // pos.allow, - // pos.deny, - // pos.type, - // '[Lockdown] Raid lock has been released.' - // ); - // } catch(ex) { - // console.error(ex); - // } - // } - // } - // } - - // await this.redis.client.hdel('nino:raid:lockdowns:channels', guild.id); - // } -} diff --git a/src/automod/Shortlinks.ts b/src/automod/Shortlinks.ts deleted file mode 100644 index 141e440a..00000000 --- a/src/automod/Shortlinks.ts +++ /dev/null @@ -1,95 +0,0 @@ -/** - * Copyright (c) 2019-2021 Nino - * - * 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. - */ - -import type { Message, TextChannel } from 'eris'; -import LocalizationService from '../services/LocalizationService'; -import PunishmentService from '../services/PunishmentService'; -import * as Constants from '../util/Constants'; -import PermissionUtil from '../util/Permissions'; -import { Automod } from '../structures'; -import { Inject } from '@augu/lilith'; -import Database from '../components/Database'; -import Discord from '../components/Discord'; - -// tfw ice the adorable fluff does all the regex for u :woeme: -const LINK_REGEX = /(https?:\/\/)?(\w*\.\w*)/gi; - -export default class Shortlinks implements Automod { - public name: string = 'shortlinks'; - - @Inject - private readonly punishments!: PunishmentService; - - @Inject - private readonly locales!: LocalizationService; - - @Inject - private readonly database!: Database; - - @Inject - private readonly discord!: Discord; - - async onMessage(msg: Message) { - if (!msg || msg === null) return false; - - const nino = msg.channel.guild.members.get(this.discord.client.user.id)!; - - if ( - (msg.member !== null && !PermissionUtil.isMemberAbove(nino, msg.member)) || - !msg.channel.permissionsOf(this.discord.client.user.id).has('manageMessages') || - msg.author.bot || - msg.channel.permissionsOf(msg.author.id).has('banMembers') - ) - return false; - - const settings = await this.database.automod.get(msg.author.id); - if (settings !== undefined && settings.shortLinks === false) return false; - - const matches = msg.content.match(LINK_REGEX); - if (matches === null) return false; - - // Why not use .find/.filter? - // Because, it should traverse *all* matches, just not one match. - // - // So for an example: - // "haha scam thing!!! floofy.dev bit.ly/jnksdjklsdsj" - // - // In the string, if `.find`/`.filter` was the solution here, - // it'll only match "owo.com" and not "bit.ly" - for (let i = 0; i < matches.length; i++) { - if (Constants.SHORT_LINKS.includes(matches[i])) { - const language = this.locales.get(msg.guildID, msg.author.id); - - await msg.delete(); - await msg.channel.createMessage(language.translate('automod.shortlinks')); - await this.punishments.createWarning( - msg.member, - `[Automod] Sending a blacklisted URL in ${msg.channel.mention}` - ); - - return true; - } - } - - return false; - } -} diff --git a/src/automod/Spam.ts b/src/automod/Spam.ts deleted file mode 100644 index abe8a090..00000000 --- a/src/automod/Spam.ts +++ /dev/null @@ -1,108 +0,0 @@ -/** - * Copyright (c) 2019-2021 Nino - * - * 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. - */ - -import type { Message, TextChannel } from 'eris'; -import LocalizationService from '../services/LocalizationService'; -import PunishmentService from '../services/PunishmentService'; -import { Collection } from '@augu/collections'; -import PermissionUtil from '../util/Permissions'; -import { Automod } from '../structures'; -import { Inject } from '@augu/lilith'; -import Database from '../components/Database'; -import Discord from '../components/Discord'; - -export default class Mentions implements Automod { - private cache: Collection> = new Collection(); - public name: string = 'mentions'; - - @Inject - private readonly punishments!: PunishmentService; - - @Inject - private readonly database!: Database; - - @Inject - private readonly locales!: LocalizationService; - - @Inject - private readonly discord!: Discord; - - async onMessage(msg: Message) { - const settings = await this.database.automod.get(msg.guildID); - if (settings === undefined || settings.invites === false) return false; - - if (!msg || msg === null) return false; - - const nino = msg.channel.guild.members.get(this.discord.client.user.id)!; - - if ( - (msg.member !== null && !PermissionUtil.isMemberAbove(nino, msg.member)) || - !msg.channel.permissionsOf(this.discord.client.user.id).has('manageMessages') || - msg.author.bot || - msg.channel.permissionsOf(msg.author.id).has('banMembers') - ) - return false; - - const queue = this.get(msg.guildID, msg.author.id); - queue.push(msg.timestamp); - - if (queue.length >= 5) { - const old = queue.shift()!; - if (msg.editedTimestamp && msg.editedTimestamp > msg.timestamp) return false; - - if (msg.timestamp - old <= 3000) { - const language = this.locales.get(msg.guildID, msg.author.id); - this.clear(msg.guildID, msg.author.id); - - await msg.channel.createMessage(language.translate('automod.spam')); - await this.punishments.createWarning(msg.member, `[Automod] Spamming in ${msg.channel.mention} o(╥﹏╥)o`); - return true; - } - } - - this.clean(msg.guildID); - return false; - } - - private clean(guildID: string) { - const now = Date.now(); - const buckets = this.cache.get(guildID); - - // Let's just not do anything if there is no spam cache for this guild - if (buckets === undefined) return; - - const ids = buckets.filterKeys((val) => now - val[val.length - 1] >= 5000); - for (const id of ids) this.cache.delete(id); - } - - private get(guildID: string, userID: string) { - if (!this.cache.has(guildID)) this.cache.set(guildID, new Collection()); - - if (!this.cache.get(guildID)!.has(userID)) this.cache.get(guildID)!.set(userID, []); - - return this.cache.get(guildID)!.get(userID)!; - } - - private clear(guildID: string, userID: string) { - this.cache.get(guildID)!.delete(userID); - } -} diff --git a/src/commands/core/HelpCommand.ts b/src/commands/core/HelpCommand.ts deleted file mode 100644 index d4371581..00000000 --- a/src/commands/core/HelpCommand.ts +++ /dev/null @@ -1,182 +0,0 @@ -/** - * Copyright (c) 2019-2021 Nino - * - * 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. - */ - -import { Command, CommandMessage, EmbedBuilder } from '../../structures'; -import { Constants as ErisConstants } from 'eris'; -import { firstUpper } from '@augu/utils'; -import CommandService from '../../services/CommandService'; -import Permissions from '../../util/Permissions'; -import { Inject } from '@augu/lilith'; -import Discord from '../../components/Discord'; - -interface CommandCategories { - moderation?: Command[]; - settings?: Command[]; - general?: Command[]; -} - -export default class HelpCommand extends Command { - private categories!: CommandCategories; - private parent!: CommandService; - - @Inject - private discord!: Discord; - - constructor() { - super({ - description: 'descriptions.help', - examples: ['help', 'help help', 'help General'], - cooldown: 2, - aliases: ['halp', 'h', 'cmds', 'commands'], - usage: '[cmdOrMod | "usage"]', - name: 'help', - }); - } - - run(msg: CommandMessage, [command]: [string]) { - return command !== undefined ? this.renderDoc(msg, command) : this.renderHelpCommand(msg); - } - - private async renderHelpCommand(msg: CommandMessage) { - if (this.categories === undefined) { - this.categories = {}; - - const commands = this.parent.filter((cmd) => !cmd.ownerOnly); - for (let i = 0; i < commands.length; i++) { - const command = commands[i]; - (this.categories[command.category] ??= []).push(command); - } - } - - const prefix = msg.settings.prefixes[Math.floor(Math.random() * msg.settings.prefixes.length)]; - const embed = EmbedBuilder.create() - .setTitle( - msg.locale.translate('commands.help.embed.title', [ - `${this.discord.client.user.username}#${this.discord.client.user.discriminator}`, - ]) - ) - .setDescription(msg.locale.translate('commands.help.embed.description', [prefix, this.parent.size])); - - for (const cat in this.categories as Required) { - const commands = this.categories[cat] as Command[]; - embed.addField( - msg.locale.translate(`commands.help.embed.fields.${cat}` as any, [this.categories[cat].length]), - commands.map((cmd) => `**\`${cmd.name}\`**`).join(', '), - false - ); - } - - return msg.reply(embed); - } - - private async renderDoc(msg: CommandMessage, cmdOrMod: string) { - const command = this.parent.filter( - (cmd) => (!cmd.hidden && cmd.name === cmdOrMod) || cmd.aliases.includes(cmdOrMod) - )[0]; - - const prefix = msg.settings.prefixes[msg.settings.prefixes.length - 1]; - - if (command !== undefined) { - const description = msg.locale.translate(command.description as any); - const embed = EmbedBuilder.create() - .setTitle(msg.locale.translate('commands.help.command.embed.title', [command.name])) - .setDescription(msg.locale.translate('commands.help.command.embed.description', [description])) - .addFields([ - { - name: msg.locale.translate('commands.help.command.embed.fields.syntax'), - value: `**\`${prefix}${command.format}\`**`, - inline: false, - }, - { - name: msg.locale.translate('commands.help.command.embed.fields.category'), - value: firstUpper(command.category), - inline: true, - }, - { - name: msg.locale.translate('commands.help.command.embed.fields.aliases'), - value: command.aliases.join(', ') || 'No aliases available', - inline: true, - }, - { - name: msg.locale.translate('commands.help.command.embed.fields.owner_only'), - value: command.ownerOnly ? 'Yes' : 'No', - inline: true, - }, - { - name: msg.locale.translate('commands.help.command.embed.fields.cooldown'), - value: `${command.cooldown} Seconds`, - inline: true, - }, - { - name: msg.locale.translate('commands.help.command.embed.fields.user_perms'), - value: - Permissions.stringify( - command.userPermissions.reduce((acc, curr) => acc | ErisConstants.Permissions[curr], 0n) - ) || 'None', - inline: true, - }, - { - name: msg.locale.translate('commands.help.command.embed.fields.bot_perms'), - value: - Permissions.stringify( - command.botPermissions.reduce((acc, curr) => acc | ErisConstants.Permissions[curr], 0n) - ) || 'None', - inline: true, - }, - { - name: msg.locale.translate('commands.help.command.embed.fields.examples'), - value: - command.examples.map((example) => `• **${prefix}${example}**`).join('\n') || 'No examples are available.', - inline: false, - }, - ]); - - return msg.reply(embed); - } else { - if (cmdOrMod === 'usage') { - const embed = EmbedBuilder.create() - .setTitle(msg.locale.translate('commands.help.usage_title')) - .setDescription(msg.locale.translate('commands.help.usage', [msg.settings.prefixes[0]])); - - return msg.reply(embed); - } - - const mod = this.parent.filter((cmd) => cmd.category.toLowerCase() === cmdOrMod.toLowerCase()); - if (mod.length > 0) { - const embed = EmbedBuilder.create() - .setAuthor(msg.locale.translate('commands.help.module.embed.title', [firstUpper(cmdOrMod)])) - .setDescription( - mod.map( - (command) => - `**\`${prefix}${command.format}\`** ~ \u200b \u200b**${msg.locale.translate( - command.description as any - )}**` - ) - ); - - return msg.reply(embed); - } else { - return msg.reply(msg.locale.translate('commands.help.command.not_found', [cmdOrMod])); - } - } - } -} diff --git a/src/commands/core/LocaleCommand.ts b/src/commands/core/LocaleCommand.ts deleted file mode 100644 index 7750391a..00000000 --- a/src/commands/core/LocaleCommand.ts +++ /dev/null @@ -1,125 +0,0 @@ -/** - * Copyright (c) 2019-2021 Nino - * - * 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. - */ - -import { Command, CommandMessage, EmbedBuilder, Subcommand } from '../../structures'; -import LocalizationService from '../../services/LocalizationService'; -import { Inject } from '@augu/lilith'; -import Database from '../../components/Database'; -import Discord from '../../components/Discord'; - -interface Flags { - user?: string | true; - u?: string | true; -} - -export default class LocaleCommand extends Command { - @Inject - private languages!: LocalizationService; - - @Inject - private database!: Database; - - @Inject - private discord!: Discord; - - constructor() { - super({ - description: 'descriptions.locale', - examples: [ - 'locale', - 'locale list', - 'locale set fr_FR --user', - 'locale set fr_FR', - 'locale reset --user', - 'locale reset', - ], - aliases: ['language', 'lang'], - name: 'locale', - }); - } - - run(msg: CommandMessage) { - const embed = EmbedBuilder.create() - .setTitle('[ Localization ]') - .setDescription([ - '> **Current Settings**', - '```apache', - `Guild: ${msg.settings.language}`, - `User: ${msg.userSettings.language}`, - '```', - '', - `• Use \`${msg.settings.prefixes[0]}locale set fr_FR\` to set the guild's language (Requires \`Manage Guild\`)`, - '• Use the `--user` or `-u` flag to set or reset your language.', - `• Use \`${msg.settings.prefixes[0]}locale list\` to view a list of the languages available`, - ]); - - return msg.reply(embed); - } - - @Subcommand() - list(msg: CommandMessage) { - const languages = this.languages.locales.map((locale) => { - const user = this.discord.client.users.get(locale.translator); - return `❯ ${locale.flag} **${locale.full} (${locale.code})** by **${user?.username ?? 'Unknown User'}**#**${ - user?.discriminator ?? '0000' - }** (${locale.contributors.length} contributers, \`${msg.settings.prefixes[0]}locale set ${locale.code} -u\`)`; - }); - - return msg.reply(EmbedBuilder.create().setTitle('[ Languages ]').setDescription(languages)); - } - - @Subcommand('', { permissions: 'manageGuild' }) - async set(msg: CommandMessage, [locale]: [string]) { - if (!locale) - return msg.reply( - `No locale has been specified, use \`${msg.settings.prefixes[0]}locale list\` to list all of them` - ); - - if (!this.languages.locales.has(locale)) - return msg.reply( - `Locale \`${locale}\` doesn't exist, use \`${msg.settings.prefixes[0]}locale list\` to list all of them` - ); - - const flags = msg.flags(); - const isUser = flags.user === true || flags.u === true; - const controller = isUser ? this.database.users : this.database.guilds; - const id = isUser ? msg.author.id : msg.guild.id; - - await controller.update(id, { language: locale }); - return msg.reply(`Language for ${isUser ? 'user' : 'server'} has been set to **${locale}**`); - } - - @Subcommand(undefined, { permissions: 'manageGuild' }) - async reset(msg: CommandMessage) { - const flags = msg.flags(); - const isUser = flags.user === true || flags.u === true; - const controller = isUser ? this.database.users : this.database.guilds; - const id = isUser ? msg.author.id : msg.guild.id; - - await controller.update(id, { - language: this.languages.defaultLocale.code, - }); - return msg.reply( - `Language for ${isUser ? 'user' : 'server'} has been resetted to **${this.languages.defaultLocale.code}**` - ); - } -} diff --git a/src/commands/core/PingCommand.ts b/src/commands/core/PingCommand.ts deleted file mode 100644 index aa6b14dc..00000000 --- a/src/commands/core/PingCommand.ts +++ /dev/null @@ -1,68 +0,0 @@ -/** - * Copyright (c) 2019-2021 Nino - * - * 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. - */ - -import { Command, CommandMessage } from '../../structures'; -import { calculateHRTime } from '@augu/utils'; -import { Inject } from '@augu/lilith'; -import Database from '../../components/Database'; -import Discord from '../../components/Discord'; -import Redis from '../../components/Redis'; - -export default class PingCommand extends Command { - @Inject - private discord!: Discord; - - constructor() { - super({ - description: 'descriptions.ping', - aliases: ['pong', 'latency', 'lat'], - cooldown: 1, - name: 'ping', - }); - } - - async run(msg: CommandMessage) { - const startedAt = process.hrtime(); - const message = await msg.reply('What did you want me to respond to?'); - const ping = calculateHRTime(startedAt); - const node = process.env.REGION ?? 'unknown'; - - const deleteMsg = process.hrtime(); - await message.delete(); - - const shard = this.discord.client.shards.get(msg.guild.shard.id)!; - const redis = await app.get('redis').getStatistics(); - const postgres = await app.get('database').getStatistics(); - - return msg.reply( - [ - `:satellite_orbital: Running under node **${node}**`, - '', - `> **Message Delete**: ${calculateHRTime(deleteMsg).toFixed()}ms`, - `> **Message Send**: ${ping.toFixed()}ms`, - `> **PostgreSQL**: ${postgres.ping}`, - `> **Shard #${shard.id}**: ${shard.latency}ms`, - `> **Redis**: ${redis.ping}`, - ].join('\n') - ); - } -} diff --git a/src/commands/core/ShardInfoCommand.ts b/src/commands/core/ShardInfoCommand.ts deleted file mode 100644 index 34911210..00000000 --- a/src/commands/core/ShardInfoCommand.ts +++ /dev/null @@ -1,89 +0,0 @@ -/** - * Copyright (c) 2019-2021 Nino - * - * 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. - */ - -import { Command, CommandMessage, EmbedBuilder } from '../../structures'; -import { firstUpper } from '@augu/utils'; -import type { Shard } from 'eris'; -import { Inject } from '@augu/lilith'; -import Discord from '../../components/Discord'; - -type ShardStatus = Shard['status']; - -const hearts: { [P in ShardStatus]: string } = { - disconnected: ':heart:', - handshaking: ':yellow_heart:', - connecting: ':yellow_heart:', - resuming: ':blue_heart:', - ready: ':green_heart:', - identifying: ':question:', -}; - -interface ShardInfo { - current: boolean; - status: ShardStatus; - guilds: number; - users: number; - heart: string; - id: number; -} - -export default class ShardInfoCommand extends Command { - @Inject - private discord!: Discord; - - constructor() { - super({ - description: 'descriptions.shardinfo', - aliases: ['shard', 'shards'], - cooldown: 6, - name: 'shardinfo', - }); - } - - run(msg: CommandMessage) { - const shards = this.discord.client.shards.map((shard) => ({ - current: msg.guild.shard.id === shard.id, - status: shard.status, - guilds: this.discord.client.guilds.filter((guild) => guild.shard.id === shard.id).length, - users: this.discord.client.guilds - .filter((guild) => guild.shard.id === shard.id) - .reduce((a, b) => a + b.memberCount, 0), - heart: hearts[shard.status], - id: shard.id, - })); - - const embed = EmbedBuilder.create().addFields( - shards.map((shard) => ({ - name: `❯ Shard #${shard.id}`, - value: [ - `${shard.heart} **${firstUpper(shard.status)}**${shard.current ? ' (current)' : ''}`, - '', - `• **Guilds**: ${shard.guilds}`, - `• **Users**: ${shard.users}`, - ].join('\n'), - inline: true, - })) - ); - - return msg.reply(embed); - } -} diff --git a/src/commands/core/StatisticsCommand.ts b/src/commands/core/StatisticsCommand.ts deleted file mode 100644 index b5eda992..00000000 --- a/src/commands/core/StatisticsCommand.ts +++ /dev/null @@ -1,146 +0,0 @@ -/** - * Copyright (c) 2019-2021 Nino - * - * 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. - */ - -/* eslint-disable camelcase */ - -import { Command, CommandMessage, EmbedBuilder } from '../../structures'; -import { Color, version, commitHash } from '../../util/Constants'; -import { firstUpper, humanize } from '@augu/utils'; -import CommandService from '../../services/CommandService'; -import { formatSize } from '../../util'; -import { Inject } from '@augu/lilith'; -import Database from '../../components/Database'; -import Discord from '../../components/Discord'; -import Config from '../../components/Config'; -import Redis from '../../components/Redis'; -import os from 'os'; - -export default class StatisticsCommand extends Command { - private parent!: CommandService; - - @Inject - private readonly database!: Database; - - @Inject - private readonly discord!: Discord; - - @Inject - private readonly config!: Config; - - @Inject - private readonly redis!: Redis; - - constructor() { - super({ - description: 'descriptions.statistics', - aliases: ['stats', 'botinfo', 'info', 'me'], - cooldown: 8, - name: 'statistics', - }); - } - - async run(msg: CommandMessage) { - const database = await this.database.getStatistics(); - const redis = await this.redis.getStatistics(); - const guilds = this.discord.client.guilds.size.toLocaleString(); - const users = this.discord.client.guilds.reduce((a, b) => a + b.memberCount, 0).toLocaleString(); - const channels = Object.keys(this.discord.client.channelGuildMap).length.toLocaleString(); - const memoryUsage = process.memoryUsage(); - const avgPing = this.discord.client.shards.reduce((a, b) => a + b.latency, 0); - const node = process.env.REGION ?? 'unknown'; - const ownerIDs = this.config.getProperty('owners') ?? []; - const owners = await Promise.all( - ownerIDs.map((id) => { - const user = this.discord.client.users.get(id); - if (user === undefined) return this.discord.client.getRESTUser(id); - else return Promise.resolve(user); - }) - ); - - const dashboardUrl = - this.discord.client.user.id === '531613242473054229' - ? 'https://stats.floofy.dev/d/e3KPDLknk/nino-prod?orgId=1' - : this.discord.client.user.id === '613907896622907425' - ? 'https://stats.floofy.dev/d/C5bZHVZ7z/nino-edge?orgId=1' - : ''; - - const embed = new EmbedBuilder() - .setAuthor( - `[ ${this.discord.client.user.username}#${this.discord.client.user.discriminator} ~ v${version} (${ - commitHash ?? '' - }) ]`, - 'https://nino.floofy.dev', - this.discord.client.user.dynamicAvatarURL('png', 1024) - ) - .setColor(Color) - .addFields([ - { - name: '❯ Discord', - value: [ - `• **Commands Executed (session)**\n${this.parent.commandsExecuted.toLocaleString()}`, - `• **Messages Seen (session)**\n${this.parent.messagesSeen.toLocaleString()}`, - `• **Shards [C / T]**\n${msg.guild.shard.id} / ${this.discord.client.shards.size} (${avgPing}ms avg.)`, - `• **Channels**\n${channels}`, - `• **Guilds**\n${guilds}`, - `• **Users**\n${users}`, - ].join('\n'), - inline: true, - }, - { - name: `❯ Process [${process.pid}]`, - value: [ - `• **System Memory [Free / Total]**\n${formatSize(os.freemem())} / ${formatSize(os.totalmem())}`, - `• **Memory Usage [RSS / Heap]**\n${formatSize(memoryUsage.rss)} / ${formatSize(memoryUsage.heapUsed)}`, - `• **Current Node**\n${node ?? 'Unknown'}`, - `• **Uptime**\n${humanize(Math.floor(process.uptime() * 1000), true)}`, - ].join('\n'), - inline: true, - }, - { - name: `❯ Redis v${redis.server.redis_version} [${firstUpper(redis.server.redis_mode)}]`, - value: [ - `• **Network I/O**\n${formatSize(redis.stats.total_net_input_bytes)} / ${formatSize( - redis.stats.total_net_output_bytes - )}`, - `• **Uptime**\n${humanize(Number(redis.server.uptime_in_seconds) * 1000, true)}`, - `• **Ops/s**\n${redis.stats.instantaneous_ops_per_sec.toLocaleString()}`, - `• **Ping**\n${redis.ping}`, - ].join('\n'), - inline: true, - }, - { - name: `❯ PostgreSQL v${database.version}`, - value: [ - `• **Insert / Delete / Update / Fetched**:\n${database.inserted.toLocaleString()} / ${database.deleted.toLocaleString()} / ${database.updated.toLocaleString()} / ${database.fetched.toLocaleString()}`, - `• **Uptime**\n${database.uptime}`, - `• **Ping**\n${database.ping}`, - ].join('\n'), - inline: true, - }, - ]) - .setFooter(`Owners: ${owners.map((user) => `${user.username}#${user.discriminator}`).join(', ')}`); - - if (dashboardUrl !== '') embed.setDescription(`[[**Metrics Dashboard**]](${dashboardUrl})`); - - return msg.reply(embed); - } -} diff --git a/src/commands/moderation/BanCommand.ts b/src/commands/moderation/BanCommand.ts deleted file mode 100644 index b43c08d7..00000000 --- a/src/commands/moderation/BanCommand.ts +++ /dev/null @@ -1,182 +0,0 @@ -/** - * Copyright (c) 2019-2021 Nino - * - * 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. - */ - -import { DiscordRESTError, Member, User } from 'eris'; -import { Command, CommandMessage } from '../../structures'; -import { PunishmentType } from '@prisma/client'; -import PunishmentService from '../../services/PunishmentService'; -import { Categories } from '../../util/Constants'; -import Permissions from '../../util/Permissions'; -import { Inject } from '@augu/lilith'; -import Discord from '../../components/Discord'; -import ms = require('ms'); - -interface BanFlags { - soft?: string | true; - days?: string | true; - d?: string | true; -} - -export default class BanCommand extends Command { - @Inject - private readonly punishments!: PunishmentService; - - @Inject - private readonly discord!: Discord; - - constructor() { - super({ - userPermissions: 'banMembers', - botPermissions: 'banMembers', - description: 'descriptions.ban', - category: Categories.Moderation, - examples: [ - 'ban @Nino', - 'ban @Nino some reason!', - 'ban @Nino some reason! | 1d', - 'ban @Nino some reason! | 1d -d 7', - ], - aliases: ['banne', 'bent', 'bean'], - usage: ' [reason [| time]]', - name: 'ban', - }); - } - - async run(msg: CommandMessage, args: string[]) { - if (args.length < 1) return msg.reply('No bot or user was specified.'); - - const userID = args[0]; - let user!: User | null; - try { - user = await this.discord.getUser(userID); - } catch (ex) { - if (ex instanceof DiscordRESTError && ex.code === 10013) - return msg.reply(`User or bot with ID "${userID}" was not found. (assuming it's a deleted bot or user)`); - - return msg.reply( - [ - 'Uh-oh! An internal error has occured while running this.', - 'Contact the developers in discord.gg/ATmjFH9kMH under <#824071651486335036>:', - '', - '```js', - (ex as any).stack ?? '<... no stacktrace? ...>', - '```', - ].join('\n') - ); - } - - if (user === null) return msg.reply('Bot or user was not found.'); - - const member = msg.guild.members.get(user.id) ?? { - id: user.id, - guild: msg.guild, - }; - - if (member.id === msg.guild.ownerID) - return msg.reply("I don't think I can perform this action due to you banning the owner, you idiot."); - - if (member.id === this.discord.client.user.id) return msg.reply(';w; why would you ban me from here? **(/。\)**'); - - if (member instanceof Member) { - // this won't work for banning members not in this guild - if (member.permissions.has('administrator') || member.permissions.has('banMembers')) - return msg.reply( - `I can't perform this action due to **${user.username}#${user.discriminator}** being a server moderator.` - ); - - if (!Permissions.isMemberAbove(msg.member, member)) - return msg.reply(`User **${user.username}#${user.discriminator}** is the same or above you.`); - - if (!Permissions.isMemberAbove(msg.self!, member)) - return msg.reply(`User **${user.username}#${user.discriminator}** is the same or above me.`); - } - - const ban = await msg.guild.getBan(user.id).catch(() => null); - if (ban !== null) - return msg.reply( - `${user.bot ? 'Bot' : 'User'} was previously banned for ${ban.reason ?? '*(no reason provided)*'}` - ); - - args.shift(); // remove user ID - - let reason = args.length > 0 ? args.join(' ') : undefined; - let time: string | null = null; - - if (reason !== undefined) { - const [r, t] = reason.split(' | '); - reason = r; - time = t ?? null; - } - - const flags = msg.flags(); - if (typeof flags.days === 'boolean' || typeof flags.d === 'boolean') - return msg.reply('The `--days` flag must have a value appended. Example: `--days=7` or `-d 7`'); - - const days = flags.days ?? flags.d ?? 7; - if (Number(days) > 7) return msg.reply('You can only concat 7 days worth of messages'); - - if (flags.soft !== undefined) - await msg.reply( - 'Flag `--soft` is deprecated and will be removed in a future release, use the `softban` command.' - ); - - try { - await this.punishments.apply({ - attachments: msg.attachments, - moderator: msg.author, - publish: true, - reason, - member: msg.guild.members.get(user.id) || { - id: user.id, - guild: msg.guild, - }, - soft: flags.soft === true, - type: PunishmentType.BAN, - days: Number(days), - time: time !== null ? ms(time) : undefined, - }); - - return msg.reply( - `${user.bot ? 'Bot' : 'User'} **${user.username}#${user.discriminator}** has been banned${ - reason ? ` *for ${reason}${time !== null ? ` in ${time}` : ''}` : '.' - }*` - ); - } catch (ex) { - if (ex instanceof DiscordRESTError && ex.code === 10007) { - return msg.reply( - `Member **${user.username}#${user.discriminator}** has left but been detected. Kinda weird if you ask me, to be honest.` - ); - } - - return msg.reply( - [ - 'Uh-oh! An internal error has occured while running this.', - 'Contact the developers in discord.gg/ATmjFH9kMH under <#824071651486335036>:', - '', - '```js', - (ex as any).stack ?? '<... no stacktrace? ...>', - '```', - ].join('\n') - ); - } - } -} diff --git a/src/commands/moderation/CaseCommand.ts b/src/commands/moderation/CaseCommand.ts deleted file mode 100644 index ad9d4273..00000000 --- a/src/commands/moderation/CaseCommand.ts +++ /dev/null @@ -1,100 +0,0 @@ -/** - * Copyright (c) 2019-2021 Nino - * - * 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. - */ - -import { Command, CommandMessage, EmbedBuilder } from '../../structures'; -import { Categories } from '../../util/Constants'; -import { Inject } from '@augu/lilith'; -import Database from '../../components/Database'; -import Discord from '../../components/Discord'; -import ms from 'ms'; - -export default class CaseCommand extends Command { - @Inject - private readonly database!: Database; - - @Inject - private readonly discord!: Discord; - - constructor() { - super({ - userPermissions: 'banMembers', - description: 'descriptions.case', - examples: ['case', 'case 3'], - category: Categories.Moderation, - aliases: ['lookup'], - usage: '[caseID]', - name: 'case', - }); - } - - async run(msg: CommandMessage, args: string[]) { - if (args.length < 1) return msg.reply('No bot or user was found.'); - - const caseID = args[0]; - if (isNaN(Number(caseID))) return msg.reply(`Case \`${caseID}\` was not a number.`); - - const caseModel = await this.database.cases.repository.findOne({ - guildID: msg.guild.id, - index: Number(caseID), - }); - - if (caseModel === undefined) return msg.reply(`Case #**${caseID}** was not found.`); - - const moderator = this.discord.client.users.get(caseModel.moderatorID) ?? { - username: 'Unknown User', - discriminator: '0000', - }; - - const victim = this.discord.client.users.get(caseModel.victimID) ?? { - discriminator: '0000', - username: 'Unknown User', - }; - - const embed = EmbedBuilder.create() - .setAuthor( - `[ Case #${caseModel.index} | ${victim.username}#${victim.discriminator} (${caseModel.victimID})]`, - undefined, - (victim as any).dynamicAvatarURL?.('png', 1024) - ) // dynamicAvatarURL might not exist since partials - .setDescription([ - `${ - caseModel.reason - ? `**${caseModel.reason}**` - : `*Unknown, use \`${msg.settings.prefixes[0]}reason ${caseModel.index} \` to set a reason*` - }`, - '', - caseModel.messageID !== null || msg.settings.modlogChannelID !== null - ? `[**\`[Jump Here]\`**](https://discord.com/channels/${msg.guild.id}/${msg.settings.modlogChannelID}/${caseModel.messageID})` - : '', - ]) - .addField( - '• Moderator', - `${moderator.username}#${moderator.discriminator} (${(moderator as any).id ?? '(unknown)'})`, - true - ) - .addField('• Type', caseModel.type, true); - - if (caseModel.time !== null) embed.addField('• Time', ms(Number(caseModel.time!), { long: true }), true); - - return msg.reply(embed); - } -} diff --git a/src/commands/moderation/KickCommand.ts b/src/commands/moderation/KickCommand.ts deleted file mode 100644 index e771e13b..00000000 --- a/src/commands/moderation/KickCommand.ts +++ /dev/null @@ -1,131 +0,0 @@ -/** - * Copyright (c) 2019-2021 Nino - * - * 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. - */ - -import { Command, CommandMessage } from '../../structures'; -import { DiscordRESTError, User } from 'eris'; -import { PunishmentType } from '@prisma/client'; -import PunishmentService from '../../services/PunishmentService'; -import { Categories } from '../../util/Constants'; -import { Inject } from '@augu/lilith'; -import Permissions from '../../util/Permissions'; -import Discord from '../../components/Discord'; - -export default class KickCommand extends Command { - @Inject - private readonly punishments!: PunishmentService; - - @Inject - private readonly discord!: Discord; - - constructor() { - super({ - userPermissions: 'kickMembers', - botPermissions: 'kickMembers', - description: 'descriptions.kick', - category: Categories.Moderation, - examples: ['kick @Nino get yeeted!'], - aliases: ['yeet', 'yeetafluff', 'yeetfluff', 'boot'], - usage: ' [reason]', - name: 'kick', - }); - } - - async run(msg: CommandMessage, [userID, ...reason]: string[]) { - if (!userID) return msg.reply('No bot or user was specified.'); - - let user!: User | null; - try { - user = await this.discord.getUser(userID); - } catch (ex) { - if (ex instanceof DiscordRESTError && ex.code === 10013) - return msg.reply(`User or bot with ID "${userID}" was not found. (assuming it's a deleted bot or user)`); - - return msg.reply( - [ - 'Uh-oh! An internal error has occured while running this.', - 'Contact the developers in discord.gg/ATmjFH9kMH under <#824071651486335036>:', - '', - '```js', - (ex as any).stack ?? '<... no stacktrace? ...>', - '```', - ].join('\n') - ); - } - - if (user === null) return msg.reply('Bot or user was not found.'); - - if (!msg.guild.members.has(user.id)) return msg.reply('Cannot kick members outside the server.'); - - const member = msg.guild.members.get(user.id)!; - if (member.id === msg.guild.ownerID) - return msg.reply("I don't think I can perform this action due to you kicking the owner, you idiot."); - - if (member.id === this.discord.client.user.id) - return msg.reply(';w; why would you kick me from here? **(/。\)**'); - - if (member.permissions.has('administrator') || member.permissions.has('banMembers')) - return msg.reply( - `I can't perform this action due to **${user.username}#${user.discriminator}** being a server moderator.` - ); - - if (!Permissions.isMemberAbove(msg.member, member)) - return msg.reply(`User **${user.username}#${user.discriminator}** is the same or above as you.`); - - if (!Permissions.isMemberAbove(msg.self!, member)) - return msg.reply(`User **${user.username}#${user.discriminator}** is the same or above me.`); - - try { - await this.punishments.apply({ - attachments: msg.attachments, - moderator: msg.author, - publish: true, - reason: reason.length ? reason.join(' ') : undefined, - member: msg.guild.members.get(user.id)!, - soft: false, - type: PunishmentType.KICK, - }); - - return msg.reply( - `${user.bot ? 'Bot' : 'User'} **${user.username}#${user.discriminator}** has been kicked${ - reason.length ? ` *for ${reason.join(' ')}*` : '.' - }` - ); - } catch (ex) { - if (ex instanceof DiscordRESTError && ex.code === 10007) { - return msg.reply( - `Member **${user.username}#${user.discriminator}** has left but been detected. Kinda weird if you ask me, to be honest.` - ); - } - - return msg.reply( - [ - 'Uh-oh! An internal error has occured while running this.', - 'Contact the developers in discord.gg/ATmjFH9kMH under <#824071651486335036>:', - '', - '```js', - (ex as any).stack ?? '<... no stacktrace? ...>', - '```', - ].join('\n') - ); - } - } -} diff --git a/src/commands/moderation/MuteCommand.ts b/src/commands/moderation/MuteCommand.ts deleted file mode 100644 index 01f2c38a..00000000 --- a/src/commands/moderation/MuteCommand.ts +++ /dev/null @@ -1,143 +0,0 @@ -/** - * Copyright (c) 2019-2021 Nino - * - * 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. - */ - -import { DiscordRESTError, User } from 'eris'; -import { Command, CommandMessage } from '../../structures'; -import { PunishmentType } from '@prisma/client'; -import PunishmentService from '../../services/PunishmentService'; -import { Categories } from '../../util/Constants'; -import { Inject } from '@augu/lilith'; -import Permissions from '../../util/Permissions'; -import Discord from '../../components/Discord'; -import ms = require('ms'); - -export default class MuteCommand extends Command { - @Inject - private readonly punishments!: PunishmentService; - - @Inject - private readonly discord!: Discord; - - constructor() { - super({ - userPermissions: 'manageMessages', - botPermissions: 'manageRoles', - description: 'descriptions.mute', - category: Categories.Moderation, - examples: ['mute @Nino', 'mute @Nino bap!', 'mute @Nino bap bap | 1d'], - usage: ' [reason [|time]]', - name: 'mute', - }); - } - - async run(msg: CommandMessage, [userID, ...reason]: [string, ...string[]]) { - if (!userID) return msg.reply('No bot or user was specified.'); - - let user!: User | null; - try { - user = await this.discord.getUser(userID); - } catch (ex) { - if (ex instanceof DiscordRESTError && ex.code === 10013) - return msg.reply(`User or bot with ID "${userID}" was not found. (assuming it's a deleted bot or user)`); - - return msg.reply( - [ - 'Uh-oh! An internal error has occured while running this.', - 'Contact the developers in discord.gg/ATmjFH9kMH under <#824071651486335036>:', - '', - '```js', - (ex as any).stack ?? '<... no stacktrace? ...>', - '```', - ].join('\n') - ); - } - - if (user === null) return msg.reply('Bot or user was not found.'); - - if (!msg.guild.members.has(user.id)) return msg.reply('Cannot mute members outside the server.'); - - const member = msg.guild.members.get(user.id)!; - if (member.id === msg.guild.ownerID) - return msg.reply("I don't think I can perform this action due to you kicking the owner, you idiot."); - - if (member.id === this.discord.client.user.id) return msg.reply("I don't have the Muted role."); - - if (member.permissions.has('administrator') || member.permissions.has('banMembers')) - return msg.reply( - `I can't perform this action due to **${user.username}#${user.discriminator}** being a server moderator.` - ); - - if (!Permissions.isMemberAbove(msg.member, member)) - return msg.reply(`User **${user.username}#${user.discriminator}** is the same or above as you.`); - - if (!Permissions.isMemberAbove(msg.self!, member)) - return msg.reply(`User **${user.username}#${user.discriminator}** is the same or above me.`); - - const areason = reason.join(' '); - let actualReason: string | undefined = undefined; - let time: string | undefined = undefined; - - if (areason !== '') { - const [r, t] = areason.split(' | '); - actualReason = r; - time = t; - } - - if (msg.settings.mutedRoleID !== undefined && member.roles.includes(msg.settings.mutedRoleID)) - return msg.reply('Member is already muted.'); - - try { - await this.punishments.apply({ - attachments: msg.attachments, - moderator: msg.author, - publish: true, - reason: actualReason, - member, - type: PunishmentType.MUTE, - time: time !== undefined ? ms(time) : undefined, - }); - - return msg.reply( - `${user.bot ? 'Bot' : 'User'} **${user.username}#${user.discriminator}** has been muted${ - actualReason ? ` *for ${actualReason}${time !== null ? ` in ${time}*` : ''}` : '.' - }` - ); - } catch (ex) { - if (ex instanceof DiscordRESTError && ex.code === 10007) { - return msg.reply( - `Member **${user.username}#${user.discriminator}** has left but been detected. Kinda weird if you ask me, to be honest.` - ); - } - - return msg.reply( - [ - 'Uh-oh! An internal error has occured while running this.', - 'Contact the developers in discord.gg/ATmjFH9kMH under <#824071651486335036>:', - '', - '```js', - (ex as any).stack ?? '<... no stacktrace? ...>', - '```', - ].join('\n') - ); - } - } -} diff --git a/src/commands/moderation/PardonCommand.ts b/src/commands/moderation/PardonCommand.ts deleted file mode 100644 index f884334b..00000000 --- a/src/commands/moderation/PardonCommand.ts +++ /dev/null @@ -1,135 +0,0 @@ -/** - * Copyright (c) 2019-2021 Nino - * - * 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. - */ - -import { DiscordRESTError, User, Member } from 'eris'; -import { Command, CommandMessage } from '../../structures'; -import PunishmentService from '../../services/PunishmentService'; -import { Categories } from '../../util/Constants'; -import Permissions from '../../util/Permissions'; -import { Inject } from '@augu/lilith'; -import Database from '../../components/Database'; -import Discord from '../../components/Discord'; - -export default class PardonCommand extends Command { - @Inject - private readonly punishments!: PunishmentService; - - @Inject - private readonly database!: Database; - - @Inject - private readonly discord!: Discord; - - constructor() { - super({ - userPermissions: 'kickMembers', - description: 'descriptions.pardon', - category: Categories.Moderation, - examples: ['pardon 280158289667555328 1', 'pardon 280158289667555328 1 yes'], - aliases: ['rmwarn', 'rmw'], - usage: ' [amount] [...reason]', - name: 'pardon', - }); - } - - async run(msg: CommandMessage, [userID, amount, ...reason]: string[]) { - if (!userID) return msg.reply('No bot or user was specified.'); - - let user!: User | null; - try { - user = await this.discord.getUser(userID); - } catch (ex) { - if (ex instanceof DiscordRESTError && ex.code === 10013) - return msg.reply(`User or bot with ID "${userID}" was not found. (assuming it's a deleted bot or user)`); - - return msg.reply( - [ - 'Uh-oh! An internal error has occured while running this.', - 'Contact the developers in discord.gg/ATmjFH9kMH under <#824071651486335036>:', - '', - '```js', - (ex as any).stack ?? '<... no stacktrace? ...>', - '```', - ].join('\n') - ); - } - - if (user === null) return msg.reply('Bot or user was not found.'); - - if (!msg.guild.members.has(user.id)) return msg.reply('Cannot warn members outside the server.'); - - const member = msg.guild.members.get(user.id)!; - if (!(member instanceof Member)) return msg.reply("Cannot remove warnings from a member that isn't here."); - - if (member.id === msg.guild.ownerID) - return msg.reply("I don't think I can perform this action due to you kicking the owner, you idiot."); - - if (member.id === this.discord.client.user.id) return msg.reply(';w; why would you warn me? **(/。\)**'); - - if (member.permissions.has('administrator') || member.permissions.has('banMembers')) - return msg.reply( - `I can't perform this action due to **${user.username}#${user.discriminator}** being a server moderator.` - ); - - if (!Permissions.isMemberAbove(msg.member, member)) - return msg.reply(`User **${user.username}#${user.discriminator}** is the same or above as you.`); - - if (!Permissions.isMemberAbove(msg.self!, member)) - return msg.reply(`User **${user.username}#${user.discriminator}** is the same or above me.`); - - const actualAmount = amount !== 'all' ? Number(amount) : 'all'; - if (actualAmount !== 'all' && isNaN(actualAmount)) - return msg.reply(`The amount provided (\`${amount}\`) was not a number.`); - - try { - await this.punishments.removeWarning( - msg.guild.members.get(user.id)!, - reason.join(' ') || 'No reason was provided.', - actualAmount - ); - - const warnings = await this.database.warnings.getAll(msg.guild.id, user.id); - const _amount = warnings.reduce((acc, curr) => acc + curr.amount, 0); - return msg.reply( - `User **${user.username}#${user.discriminator}** now has **${_amount === 0 ? 'no' : _amount}** warnings left.` - ); - } catch (ex) { - if (ex instanceof RangeError || ex instanceof SyntaxError) return msg.error(ex.message); - if (ex instanceof DiscordRESTError && ex.code === 10007) { - return msg.reply( - `Member **${user.username}#${user.discriminator}** has left but been detected. Kinda weird if you ask me, to be honest.` - ); - } - - return msg.reply( - [ - 'Uh-oh! An internal error has occured while running this.', - 'Contact the developers in discord.gg/ATmjFH9kMH under <#824071651486335036>:', - '', - '```js', - (ex as any).stack ?? '<... no stacktrace? ...>', - '```', - ].join('\n') - ); - } - } -} diff --git a/src/commands/moderation/PurgeCommand.ts b/src/commands/moderation/PurgeCommand.ts deleted file mode 100644 index ff3c7d31..00000000 --- a/src/commands/moderation/PurgeCommand.ts +++ /dev/null @@ -1,252 +0,0 @@ -/** - * Copyright (c) 2019-2021 Nino - * - * 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. - */ - -import { Command, CommandMessage, EmbedBuilder, Subcommand } from '../../structures'; -import { Categories, DISCORD_INVITE_REGEX } from '../../util/Constants'; -import type { Message, User } from 'eris'; -import { pluralize } from '@augu/utils'; -import { Inject } from '@augu/lilith'; -import Discord from '../../components/Discord'; - -// It's a function so it can properly hydrate instead of being in the command scope as a getter. -const getTwoWeeksFromNow = () => Date.now() - 1000 * 60 * 60 * 24 * 14; - -export default class PurgeCommand extends Command { - @Inject - private readonly discord!: Discord; - - constructor() { - super({ - userPermissions: ['manageMessages'], - botPermissions: ['manageMessages'], - description: 'descriptions.purge', - examples: [ - 'prune | Automatically delete 100 messages from this channel', - 'prune 6 | Delete 6 messages from the channel', - 'prune August#5820 5 | Removes messages that **August** has said. (do not actually do this)', - 'prune self 6 | Removes 6 messages that I have said', - 'prune system | Prunes all system-related messages', - 'prune attachments | Prunes all messages with attachments', - ], - aliases: ['prune', 'delmsgs', 'delmsg'], - category: Categories.Moderation, - usage: ' [amount] | | [amount]', - name: 'purge', - }); - } - - private _generateEmbed(msg: CommandMessage, messages: Message[]) { - // removes duplicated ids (so it doesn't look garbage (i.e: https://github.com/NinoDiscord/Nino/blob/56290f3cce32ec8376d704ab510a10bdeea7fa7a/src/commands/moderation/Prune.ts#L64)) - const users = [...new Set(messages.map((s) => s.author.id))]; - return EmbedBuilder.create() - .setAuthor( - `[ ${msg.author.username}#${msg.author.discriminator} ~ Purge Result ]`, - '', - msg.author.dynamicAvatarURL('png', 1024) - ) - .setDescription([ - `${msg.successEmote} I have deleted **${pluralize('message', messages.length)}** from this channel.`, - '> 👤 **Users Affected**', - '```apache', - users - .map((user) => { - const u = this.discord.client.users.get(user) ?? { - username: 'Unknown User', - discriminator: '0000', - id: user, - bot: false, - }; - const messagesByUser = messages.filter((c) => c.author.id === user); - const percentage = ((messagesByUser.length / messages.length) * 100).toFixed(); - - return `• ${u.username}#${u.discriminator}${u.bot ? ' (Bot)' : ''} (${u.id}) - ${percentage}% deleted`; - }) - .join('\n'), - '```', - ]); - } - - async run(msg: CommandMessage, [userIdOrAmount, amount]: [string, string?]) { - if (!userIdOrAmount) { - await msg.reply('Now purging 100 messages from this channel...'); - - const messages = await msg.channel - .getMessages({ limit: 100 }) - .then((f) => f.filter((c) => c.timestamp >= getTwoWeeksFromNow())); - if (!messages.length) return msg.error('No messages were found that is under 2 weeks?'); - - await this.discord.client.deleteMessages( - /* channelID */ msg.channel.id, - /* messages */ messages.map((i) => i.id), - /* reason */ `[Purge] User ${msg.author.username}#${msg.author.discriminator} ran the \`purge\` command.` - ); - - return msg.reply(this._generateEmbed(msg, messages), false); - } - - const user = await this.discord.getUser(userIdOrAmount).catch(() => null); - if (user !== null) - return this._deleteByUser(msg, user.id, amount !== undefined && !isNaN(Number(amount)) ? Number(amount) : 50); - else - return this._deleteAmount( - msg, - !isNaN(Number(userIdOrAmount)) - ? Number(userIdOrAmount) - : amount !== undefined && !isNaN(Number(amount)) - ? Number(amount) - : 50 - ); - } - - private async _deleteByUser(msg: CommandMessage, userID: string, amount?: number) { - const amountToDelete = amount ?? 50; - if (amountToDelete > 100) - return msg.reply(`Cannot delete more or equal to 100 messages. (went over **${amountToDelete - 100}**.)`); - - const messages = await msg.channel - .getMessages({ limit: amountToDelete }) - .then((messages) => messages.filter((m) => m.author.id === userID && m.timestamp >= getTwoWeeksFromNow())); - if (!messages.length) return msg.error('No messages were found that is under 2 weeks?'); - - if (amountToDelete === 1) { - const message = messages[0]; - await message.delete(); - - return msg.success('Deleted one message.'); - } - - const user = (await this.discord.getUser(userID)) as User; - await this.discord.client.deleteMessages( - /* channelID */ msg.channel.id, - /* messages */ messages.map((i) => i.id), - /* reason */ `[Purge] User ${msg.author.username}#${msg.author.discriminator} ran the \`purge\` command. (used to delete ${amountToDelete} messages from ${user.username}#${user.discriminator})` - ); - - return msg.reply(this._generateEmbed(msg, messages), false); - } - - private async _deleteAmount(msg: CommandMessage, amount: number) { - if (amount > 100) return msg.reply(`Cannot delete more or equal to 100 messages. (went over **${amount - 100}**.)`); - - const messages = await msg.channel - .getMessages({ limit: amount }) - .then((m) => m.filter((c) => c.timestamp > getTwoWeeksFromNow())); - if (!messages.length) return msg.error('No messages were found that is under 2 weeks?'); - - if (amount === 1) { - const message = messages[0]; - await message.delete(); - - return msg.success('Deleted the previous message'); - } - - await this.discord.client.deleteMessages( - /* channelID */ msg.channel.id, - /* messages */ messages.map((i) => i.id), - /* reason */ `[Purge] User ${msg.author.username}#${msg.author.discriminator} ran the \`purge\` command. (used to delete ${amount} messages from all users)` - ); - - return msg.reply(this._generateEmbed(msg, messages), false); - } - - @Subcommand('[amount]', ['images', 'imgs']) - async attachments(msg: CommandMessage, [amount]: [string?]) { - const toDelete = amount !== undefined && !isNaN(Number(amount)) ? Number(amount) : 50; - if (toDelete > 100) - return msg.reply(`Cannot delete more or equal to 100 messages. (went over **${toDelete - 100}**.)`); - - const messages = await msg.channel - .getMessages({ limit: toDelete }) - .then((m) => m.filter((c) => c.attachments.length > 0).filter((c) => c.timestamp > getTwoWeeksFromNow())); - if (toDelete === 1) { - const message = messages[0]; - await message.delete(); - - return msg.success('Deleted one message.'); - } - - await this.discord.client.deleteMessages( - /* channelID */ msg.channel.id, - /* messages */ messages.map((i) => i.id), - /* reason */ `[Purge] User ${msg.author.username}#${msg.author.discriminator} ran the \`purge\` command. (used to delete ${toDelete} messages with attachments)` - ); - - return msg.reply(this._generateEmbed(msg, messages), false); - } - - @Subcommand() - async system(msg: CommandMessage) { - const messages = await msg.channel - .getMessages({ limit: 10 }) - .then((m) => m.filter((c) => c.author.system).filter((c) => c.timestamp > getTwoWeeksFromNow())); - await this.discord.client.deleteMessages( - /* channelID */ msg.channel.id, - /* messages */ messages.map((i) => i.id), - /* reason */ `[Purge] User ${msg.author.username}#${msg.author.discriminator} ran the \`purge\` command. (used to delete 10 messages that are system-related)` - ); - - return msg.reply(this._generateEmbed(msg, messages), false); - } - - @Subcommand() - async invites(msg: CommandMessage) { - const messages = await msg.channel - .getMessages({ limit: 10 }) - .then((m) => - m.filter((c) => DISCORD_INVITE_REGEX.test(c.content)).filter((c) => c.timestamp > getTwoWeeksFromNow()) - ); - await this.discord.client.deleteMessages( - /* channelID */ msg.channel.id, - /* messages */ messages.map((i) => i.id), - /* reason */ `[Purge] User ${msg.author.username}#${msg.author.discriminator} ran the \`purge\` command. (used to delete 10 messages that are system-related)` - ); - - return msg.reply(this._generateEmbed(msg, messages), false); - } - - @Subcommand('[amount]') - async self(msg: CommandMessage, [amount]: [string?]) { - const toDelete = amount !== undefined && !isNaN(Number(amount)) ? Number(amount) : 50; - if (toDelete > 100) - return msg.reply(`Cannot delete more or equal to 100 messages. (went over **${toDelete - 100}**.)`); - - const messages = await msg.channel - .getMessages({ limit: toDelete }) - .then((m) => - m.filter((c) => c.author.id === this.discord.client.user.id).filter((c) => c.timestamp > getTwoWeeksFromNow()) - ); - if (toDelete === 1) { - const message = messages[0]; - await message.delete(); - - return msg.success('Deleted one message.'); - } - - await this.discord.client.deleteMessages( - /* channelID */ msg.channel.id, - /* messages */ messages.map((i) => i.id), - /* reason */ `[Purge] User ${msg.author.username}#${msg.author.discriminator} ran the \`purge\` command. (used to delete ${toDelete} messages that I said)` - ); - - return msg.reply(this._generateEmbed(msg, messages), false); - } -} diff --git a/src/commands/moderation/ReasonCommand.ts b/src/commands/moderation/ReasonCommand.ts deleted file mode 100644 index fec3305c..00000000 --- a/src/commands/moderation/ReasonCommand.ts +++ /dev/null @@ -1,134 +0,0 @@ -/** - * Copyright (c) 2019-2021 Nino - * - * 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. - */ - -import { Command, CommandMessage, Subcommand } from '../../structures'; -import type { TextChannel } from 'eris'; -import PunishmentService from '../../services/PunishmentService'; -import type CaseEntity from '../../entities/CaseEntity'; -import { Categories } from '../../util/Constants'; -import { Inject } from '@augu/lilith'; -import Database from '../../components/Database'; -import Discord from '../../components/Discord'; -import ms from 'ms'; - -export default class ReasonCommand extends Command { - @Inject - private readonly punishments!: PunishmentService; - - @Inject - private readonly database!: Database; - - @Inject - private readonly discord!: Discord; - - constructor() { - super({ - userPermissions: 'kickMembers', - botPermissions: 'manageMessages', - description: 'descriptions.reason', - category: Categories.Moderation, - examples: [ - 'reason 69 some reason!', - 'reason latest another reason', - 'reason l another reason that is the recent case', - ], - usage: '[caseID | "l" | "latest"] [...reason]', - aliases: ['set-reason', 'r'], - name: 'reason', - }); - } - - async run(msg: CommandMessage, [caseID, ...reason]: [string, ...string[]]) { - if (!caseID) return msg.reply('Missing case ID.'); - - const id = Number(caseID); - if (isNaN(id)) return msg.reply('Case ID was not a number.'); - - let caseModel = await this.database.cases.get(msg.guild.id, id); - if (!caseModel) return msg.reply(`Case with ID #**${id}** was not found.`); - - if (reason.includes(' | ') && ms(reason.join(' ').split(' | ')[1]) !== undefined) - await msg.reply( - 'Due to infrastructure issues with some internal stuff, editing times will be deprecated & removed in a future release.' - ); - - await this.database.cases.update(msg.guild.id, caseModel.index, { - reason: reason.join(' ') || 'No reason was provided.', - }); - - caseModel = (await this.database.cases.get(msg.guild.id, id)) as unknown as CaseEntity; - if (caseModel.messageID !== null && msg.settings.modlogChannelID !== null) { - const channel = await this.discord.getChannel(msg.settings.modlogChannelID!); - - if (channel === null) - return msg.reply( - 'unknown error occured, report to devs here under <#824071651486335036>: https://discord.gg/ATmjFH9kMH' - ); - - const message = await this.discord.client.getMessage(channel!.id, caseModel.messageID!); - // @ts-ignore - await this.punishments.editModLog(caseModel, message); - - return msg.reply(`Updated case #**${caseModel.index}** with reason **${reason.join(' ') || '(unknown)'}**`); - } - - return msg.reply( - "Unable to edit case due to no mod-log channel or that case didn't create a message in the mod-log." - ); - } - - @Subcommand('<...reason>', ['l']) - async latest(msg: CommandMessage, reason: string[]) { - const latestCases = await this.database.cases.getAll(msg.guild.id); - if (!latestCases.length) return msg.reply('There are no recent cases to edit, maybe punish someone?'); - - if (reason.includes(' | ') && ms(reason.join(' ').split(' | ')[1]) !== undefined) - await msg.reply( - 'Due to infrastructure issues with some internal stuff, editing times will be deprecated & removed in a future release.' - ); - - let latestCaseModel = latestCases[latestCases.length - 1]; // .last(); when :woeme: - await this.database.cases.update(msg.guild.id, latestCaseModel.index, { - reason: reason.join(' ') || 'No reason was provided.', - }); - - latestCaseModel = await this.database.cases.get(msg.guild.id, latestCaseModel.index).then((r) => r!); - if (latestCaseModel.messageID !== null && msg.settings.modlogChannelID !== null) { - const channel = await this.discord.getChannel(msg.settings.modlogChannelID!); - - if (channel === null) - return msg.reply( - 'unknown error occured, report to devs here under <#824071651486335036>: https://discord.gg/ATmjFH9kMH' - ); - - const message = await this.discord.client.getMessage(channel!.id, latestCaseModel.messageID!); - // @ts-ignore - await this.punishments.editModLog(latestCaseModel, message); - - return msg.reply(`Updated case #**${latestCaseModel.index}** with reason **${reason.join(' ') || '(unknown)'}**`); - } - - return msg.reply( - "Unable to edit case due to no mod-log channel or that case didn't create a message in the mod-log." - ); - } -} diff --git a/src/commands/moderation/SoftbanCommand.ts b/src/commands/moderation/SoftbanCommand.ts deleted file mode 100644 index 700c7916..00000000 --- a/src/commands/moderation/SoftbanCommand.ts +++ /dev/null @@ -1,158 +0,0 @@ -/** - * Copyright (c) 2019-2021 Nino - * - * 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. - */ - -import { DiscordRESTError, Member, User } from 'eris'; -import { Command, CommandMessage } from '../../structures'; -import { PunishmentType } from '@prisma/client'; -import PunishmentService from '../../services/PunishmentService'; -import { Categories } from '../../util/Constants'; -import { Inject } from '@augu/lilith'; -import Permissions from '../../util/Permissions'; -import Discord from '../../components/Discord'; - -interface Flags { - days?: string | true; - d?: string | true; -} - -export default class SoftbanCommand extends Command { - @Inject - private readonly punishments!: PunishmentService; - - @Inject - private readonly discord!: Discord; - - constructor() { - super({ - userPermissions: 'banMembers', - botPermissions: 'banMembers', - description: 'descriptions.softban', - category: Categories.Moderation, - examples: [ - 'softban 154254569632587456', - 'softban 154254569632587456 bad!', - 'softban @Nino bad bot!', - 'softban @Nino bad bot! -d 7', - 'softban 154254569632587456 bad bot! --days=7', - ], - usage: ' [reason] [--days | -d]', - name: 'softban', - }); - } - - async run(msg: CommandMessage, [userID, ...reason]: [string, ...string[]]) { - if (!userID) return msg.reply('No bot or user was specified.'); - - let user!: User | null; - try { - user = await this.discord.getUser(userID); - } catch (ex) { - if (ex instanceof DiscordRESTError && ex.code === 10013) - return msg.reply(`User or bot with ID "${userID}" was not found. (assuming it's a deleted bot or user)`); - - return msg.reply( - [ - 'Uh-oh! An internal error has occured while running this.', - 'Contact the developers in discord.gg/ATmjFH9kMH under <#824071651486335036>:', - '', - '```js', - (ex as any).stack ?? '<... no stacktrace? ...>', - '```', - ].join('\n') - ); - } - - if (user === null) return msg.reply('Bot or user was not found.'); - - if (!msg.guild.members.has(user.id)) - return msg.reply( - `Bot or user **${user.username}#${user.discriminator}** must be in the guild to perform this action.` - ); - - const member = msg.guild.members.get(user.id)!; - if (member.id === msg.guild.ownerID) - return msg.reply("I don't think I can perform this action due to you banning the owner, you idiot."); - - if (member.id === this.discord.client.user.id) - return msg.reply(';w; why would you soft-ban me from here? **(/。\)**'); - - if (member instanceof Member) { - // this won't work for banning members not in this guild - if (member.permissions.has('administrator') || member.permissions.has('banMembers')) - return msg.reply( - `I can't perform this action due to **${user.username}#${user.discriminator}** being a server moderator.` - ); - - if (!Permissions.isMemberAbove(msg.member, member)) - return msg.reply(`User **${user.username}#${user.discriminator}** is the same or above you.`); - - if (!Permissions.isMemberAbove(msg.self!, member)) - return msg.reply(`User **${user.username}#${user.discriminator}** is the same or above me.`); - } - - const flags = msg.flags(); - if (typeof flags.days === 'boolean' || typeof flags.d === 'boolean') - return msg.reply('The `--days` flag must have a value appended. Example: `--days=7` or `-d 7`'); - - const days = flags.days ?? flags.d ?? 7; - if (Number(days) > 7) return msg.reply('You can only concat 7 days worth of messages'); - - try { - await this.punishments.apply({ - attachments: msg.attachments, - moderator: msg.author, - publish: true, - reason: reason.join(' ') || 'No reason was provided.', - member: msg.guild.members.get(user.id) || { - id: user.id, - guild: msg.guild, - }, - soft: true, - type: PunishmentType.BAN, - days: Number(days), - }); - - return msg.reply( - `${user.bot ? 'Bot' : 'User'} **${user.username}#${user.discriminator}** has been banned${ - reason ? ` *for ${reason}*` : '' - }` - ); - } catch (ex) { - if (ex instanceof DiscordRESTError && ex.code === 10007) { - return msg.reply( - `Member **${user.username}#${user.discriminator}** has left but been detected. Kinda weird if you ask me, to be honest.` - ); - } - - return msg.reply( - [ - 'Uh-oh! An internal error has occured while running this.', - 'Contact the developers in discord.gg/ATmjFH9kMH under <#824071651486335036>:', - '', - '```js', - (ex as any).stack ?? '<... no stacktrace? ...>', - '```', - ].join('\n') - ); - } - } -} diff --git a/src/commands/moderation/TimeoutsCommand.ts b/src/commands/moderation/TimeoutsCommand.ts deleted file mode 100644 index 0b0cef02..00000000 --- a/src/commands/moderation/TimeoutsCommand.ts +++ /dev/null @@ -1,170 +0,0 @@ -/** - * Copyright (c) 2019-2021 Nino - * - * 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. - */ - -import { Command, CommandMessage, EmbedBuilder } from '../../structures'; -import type { Timeout } from '../../components/timeouts/types'; -import { Categories } from '../../util/Constants'; -import { firstUpper } from '@augu/utils'; -import { Inject } from '@augu/lilith'; -import Discord from '../../components/Discord'; -import Redis from '../../components/Redis'; - -export default class TimeoutsCommand extends Command { - @Inject - private readonly discord!: Discord; - - @Inject - private readonly redis!: Redis; - - constructor() { - super({ - userPermissions: 'manageMessages', - botPermissions: 'manageMessages', - description: 'descriptions.timeouts', - category: Categories.Moderation, - examples: ['timeouts', 'timeouts unban'], - name: 'timeouts', - }); - } - - async run(msg: CommandMessage, [type]: [string]) { - return type !== undefined ? this._sendTimeoutsAsSpecific(msg, type) : this._sendTimeouts(msg); - } - - private async _sendTimeoutsAsSpecific(msg: CommandMessage, type: string) { - const timeouts = await this.redis.client - .hget('nino:timeouts', msg.guild.id) - .then((value) => (value !== null ? JSON.parse(value) : [])) - .catch(() => [] as Timeout[]); - - if (!timeouts.length) return msg.reply(`Guild **${msg.guild.name}** doesn't have any concurrent timeouts.`); - - const all = timeouts.filter((p) => p.type.toLowerCase() === type.toLowerCase()); - if (!all.length) return msg.reply(`Punishment type **${type}** didn't have any timeouts.`); - - const h = await Promise.all( - all.slice(0, 10).map(async (pkt, idx) => { - const user = await this.discord - .getUser(pkt.user) - .then((user) => - user === null - ? { - username: 'Unknown User', - discriminator: '0000', - id: pkt.user, - } - : user! - ) - .catch(() => ({ - username: 'Unknown User', - discriminator: '0000', - id: pkt.user, - })); - - const moderator = this.discord.client.users.get(pkt.moderator) ?? { - username: 'Unknown User', - discriminator: '0000', - }; - const issuedAt = new Date(pkt.issued); - return { - name: `❯ #${idx + 1}: User ${user!.username}#${user!.discriminator}`, - value: [ - `• **Issued At**: ${issuedAt.toUTCString()}`, - `• **Expires At**: ${new Date(pkt.expired).toUTCString()}`, - `• **Moderator**: ${moderator.username}#${moderator.discriminator}`, - `• **Reason**: ${pkt.reason ?? '*No reason was defined.*'}`, - ].join('\n'), - inline: true, - }; - }) - ); - - const embed = EmbedBuilder.create() - .setAuthor( - `[ Timeouts in ${msg.guild.name} (${msg.guild.id}) ]`, - undefined, - msg.guild.dynamicIconURL?.('png', 1024) ?? undefined - ) - .addFields(h) - .setFooter('Only showing 10 entries.'); - - return msg.reply(embed); - } - - private async _sendTimeouts(msg: CommandMessage) { - const timeouts = await this.redis.client - .hget('nino:timeouts', msg.guild.id) - .then((value) => (value !== null ? JSON.parse(value) : [])) - .catch(() => [] as Timeout[]); - - if (!timeouts.length) return msg.reply(`Guild **${msg.guild.name}** doesn't have any concurrent timeouts.`); - - const h = await Promise.all( - timeouts.slice(0, 10).map(async (pkt, idx) => { - const user = await this.discord - .getUser(pkt.user) - .then((user) => - user === null - ? { - username: 'Unknown User', - discriminator: '0000', - id: pkt.user, - } - : user! - ) - .catch(() => ({ - username: 'Unknown User', - discriminator: '0000', - id: pkt.user, - })); - - const moderator = this.discord.client.users.get(pkt.moderator) ?? { - username: 'Unknown User', - discriminator: '0000', - }; - const issuedAt = new Date(pkt.issued); - return { - name: `❯ #${idx + 1}: User ${user!.username}#${user!.discriminator}`, - value: [ - `• **Issued At**: ${issuedAt.toUTCString()}`, - `• **Expires At**: ${new Date(pkt.expired).toDateString()}`, - `• **Moderator**: ${moderator.username}#${moderator.discriminator}`, - `• **Reason**: ${pkt.reason ?? '*No reason was defined.*'}`, - `• **Punishment**: ${firstUpper(pkt.type)}`, - ].join('\n'), - inline: true, - }; - }) - ); - - const embed = EmbedBuilder.create() - .setAuthor( - `[ Timeouts in ${msg.guild.name} (${msg.guild.id}) ]`, - undefined, - msg.guild.dynamicIconURL?.('png', 1024) ?? undefined - ) - .addFields(h) - .setFooter('Only showing 10 entries.'); - - return msg.reply(embed); - } -} diff --git a/src/commands/moderation/UnbanCommand.ts b/src/commands/moderation/UnbanCommand.ts deleted file mode 100644 index 06abf15c..00000000 --- a/src/commands/moderation/UnbanCommand.ts +++ /dev/null @@ -1,82 +0,0 @@ -/** - * Copyright (c) 2019-2021 Nino - * - * 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. - */ - -import { Command, CommandMessage } from '../../structures'; -import { PunishmentType } from '@prisma/client'; -import PunishmentService from '../../services/PunishmentService'; -import { Categories } from '../../util/Constants'; -import { Inject } from '@augu/lilith'; -import Redis from '../../components/Redis'; - -export default class UnbanCommand extends Command { - @Inject - private readonly punishments!: PunishmentService; - - @Inject - private readonly redis!: Redis; - - constructor() { - super({ - userPermissions: 'banMembers', - botPermissions: 'banMembers', - description: 'descriptions.ban', - category: Categories.Moderation, - examples: ['unban @Nino', 'unban @Nino some reason!'], - aliases: ['unbent', 'unbean'], - usage: ' [reason]', - name: 'unban', - }); - } - - async run(msg: CommandMessage, [userID, ...reason]: [string, ...string[]]) { - if (!userID) return msg.reply('No bot or user was specified.'); - - try { - await this.punishments.apply({ - moderator: msg.author, - publish: true, - reason: reason.join(' ') || 'No description was provided.', - member: { id: userID, guild: msg.guild }, - type: PunishmentType.UNBAN, - }); - - const timeouts = await this.redis.getTimeouts(msg.guild.id); - const available = timeouts.filter( - (pkt) => pkt.type !== PunishmentType.UNBAN.toLowerCase() && pkt.user !== userID && pkt.guild === msg.guild.id - ); - - await this.redis.client.hmset('nino:timeouts', [msg.guild.id, available]); - return msg.reply('User or bot has been unbanned successfully.'); - } catch (ex) { - return msg.reply( - [ - 'Uh-oh! An internal error has occured while running this.', - 'Contact the developers in discord.gg/ATmjFH9kMH under <#824071651486335036>:', - '', - '```js', - (ex as any).stack ?? '<... no stacktrace? ...>', - '```', - ].join('\n') - ); - } - } -} diff --git a/src/commands/moderation/UnmuteCommand.ts b/src/commands/moderation/UnmuteCommand.ts deleted file mode 100644 index 95df7281..00000000 --- a/src/commands/moderation/UnmuteCommand.ts +++ /dev/null @@ -1,140 +0,0 @@ -/** - * Copyright (c) 2019-2021 Nino - * - * 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. - */ - -import { Command, CommandMessage } from '../../structures'; -import { DiscordRESTError, User } from 'eris'; -import { PunishmentType } from '@prisma/client'; -import PunishmentService from '../../services/PunishmentService'; -import { Categories } from '../../util/Constants'; -import { Inject } from '@augu/lilith'; -import Permissions from '../../util/Permissions'; -import Discord from '../../components/Discord'; -import Redis from '../../components/Redis'; - -export default class UnmuteCommand extends Command { - @Inject - private readonly punishments!: PunishmentService; - - @Inject - private readonly discord!: Discord; - - @Inject - private readonly redis!: Redis; - - constructor() { - super({ - userPermissions: 'kickMembers', - botPermissions: 'manageRoles', - description: 'descriptions.unmute', - category: Categories.Moderation, - examples: ['unmute 1245454585452365896', 'unmute 1245454585452365896 some reason'], - name: 'unmute', - }); - } - - async run(msg: CommandMessage, [userID, ...reason]: string[]) { - if (!userID) return msg.reply('No bot or user was specified.'); - - let user!: User | null; - try { - user = await this.discord.getUser(userID); - } catch (ex) { - if (ex instanceof DiscordRESTError && ex.code === 10013) - return msg.reply(`User or bot with ID "${userID}" was not found. (assuming it's a deleted bot or user)`); - - return msg.reply( - [ - 'Uh-oh! An internal error has occured while running this.', - 'Contact the developers in discord.gg/ATmjFH9kMH under <#824071651486335036>:', - '', - '```js', - (ex as any).stack ?? '<... no stacktrace? ...>', - '```', - ].join('\n') - ); - } - - if (user === null) return msg.reply('Bot or user was not found.'); - - if (!msg.guild.members.has(user.id)) return msg.reply('Cannot unmute members outside the server.'); - - const member = msg.guild.members.get(user.id)!; - if (member.id === msg.guild.ownerID) - return msg.reply("I don't think I can perform this action due to you kicking the owner, you idiot."); - - if (member.id === this.discord.client.user.id) return msg.reply("I don't have the Muted role."); - - if (member.permissions.has('administrator') || member.permissions.has('banMembers')) - return msg.reply( - `I can't perform this action due to **${user.username}#${user.discriminator}** being a server moderator.` - ); - - if (!Permissions.isMemberAbove(msg.member, member)) - return msg.reply(`User **${user.username}#${user.discriminator}** is the same or above as you.`); - - if (!Permissions.isMemberAbove(msg.self!, member)) - return msg.reply(`User **${user.username}#${user.discriminator}** is the same or above me.`); - - if (msg.settings.mutedRoleID !== undefined && !member.roles.includes(msg.settings.mutedRoleID)) - return msg.reply('Member is already un-muted.'); - - try { - await this.punishments.apply({ - attachments: msg.attachments, - moderator: msg.author, - publish: true, - reason: reason.join(' ') || 'No reason was provided', - member: msg.guild.members.get(user.id)!, - type: PunishmentType.UNMUTE, - }); - - const timeouts = await this.redis.getTimeouts(msg.guild.id); - const available = timeouts.filter( - (pkt) => pkt.type !== PunishmentType.UNMUTE.toLowerCase() && pkt.user !== userID && pkt.guild === msg.guild.id - ); - - await this.redis.client.hmset('nino:timeouts', [msg.guild.id, available]); - return msg.reply( - `:thumbsup: Successfully unmuted **${user.username}#${user.discriminator}**${ - reason.length > 0 ? `, for **${reason.join(' ')}**` : '.' - }` - ); - } catch (ex) { - if (ex instanceof DiscordRESTError && ex.code === 10007) { - return msg.reply( - `Member **${user.username}#${user.discriminator}** has left but been detected. Kinda weird if you ask me, to be honest.` - ); - } - - return msg.reply( - [ - 'Uh-oh! An internal error has occured while running this.', - 'Contact the developers in discord.gg/ATmjFH9kMH under <#824071651486335036>:', - '', - '```js', - (ex as any).stack ?? '<... no stacktrace? ...>', - '```', - ].join('\n') - ); - } - } -} diff --git a/src/commands/moderation/WarnCommand.ts b/src/commands/moderation/WarnCommand.ts deleted file mode 100644 index e01e1c7c..00000000 --- a/src/commands/moderation/WarnCommand.ts +++ /dev/null @@ -1,134 +0,0 @@ -/** - * Copyright (c) 2019-2021 Nino - * - * 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. - */ - -import { Command, CommandMessage } from '../../structures'; -import { DiscordRESTError, User } from 'eris'; -import PunishmentService from '../../services/PunishmentService'; -import { Categories } from '../../util/Constants'; -import Permissions from '../../util/Permissions'; -import { pluralize } from '@augu/utils'; -import { Inject } from '@augu/lilith'; -import Database from '../../components/Database'; -import Discord from '../../components/Discord'; - -export default class WarnCommand extends Command { - @Inject - private readonly punishments!: PunishmentService; - - @Inject - private readonly database!: Database; - - @Inject - private readonly discord!: Discord; - - constructor() { - super({ - userPermissions: 'kickMembers', - description: 'descriptions.warn', - category: Categories.Moderation, - examples: ['warn 280158289667555328 no'], - aliases: ['addwarn'], - name: 'warn', - }); - } - - async run(msg: CommandMessage, [userID, ...reason]: string[]) { - if (!userID) return msg.reply('No bot or user was specified.'); - - let user!: User | null; - try { - user = await this.discord.getUser(userID); - } catch (ex) { - if (ex instanceof DiscordRESTError && ex.code === 10013) - return msg.reply(`User or bot with ID "${userID}" was not found. (assuming it's a deleted bot or user)`); - - return msg.reply( - [ - 'Uh-oh! An internal error has occured while running this.', - 'Contact the developers in discord.gg/ATmjFH9kMH under <#824071651486335036>:', - '', - '```js', - (ex as any).stack ?? '<... no stacktrace? ...>', - '```', - ].join('\n') - ); - } - - if (user === null) return msg.reply('Bot or user was not found.'); - - if (!msg.guild.members.has(user.id)) return msg.reply('Cannot warn members outside the server.'); - - const member = msg.guild.members.get(user.id)!; - if (member.id === msg.guild.ownerID) - return msg.reply("I don't think I can perform this action due to you kicking the owner, you idiot."); - - if (member.id === this.discord.client.user.id) return msg.reply(';w; why would you warn me? **(/。\)**'); - - if (member.permissions.has('administrator') || member.permissions.has('banMembers')) - return msg.reply( - `I can't perform this action due to **${user.username}#${user.discriminator}** being a server moderator.` - ); - - if (!Permissions.isMemberAbove(msg.member, member)) - return msg.reply(`User **${user.username}#${user.discriminator}** is the same or above as you.`); - - if (!Permissions.isMemberAbove(msg.self!, member)) - return msg.reply(`User **${user.username}#${user.discriminator}** is the same or above me.`); - - try { - await this.punishments.createWarning( - msg.guild.members.get(user.id)!, - reason.join(' ') || 'No reason was provided.', - 1 - ); - - const warnings = await this.database.warnings - .getAll(msg.guild.id, user.id) - .then((warnings) => warnings.filter((warns) => warns.amount > 0)); - - const count = warnings.reduce((acc, curr) => acc + curr.amount, 0); - - return msg.reply( - `:thumbsup: Warned **${user.username}#${user.discriminator}**${ - reason.length > 0 ? ` for **${reason.join(' ')}**` : ' ' - }, they now have **${pluralize('warning', count)}**.` - ); - } catch (ex) { - if (ex instanceof DiscordRESTError && ex.code === 10007) { - return msg.reply( - `Member **${user.username}#${user.discriminator}** has left but been detected. Kinda weird if you ask me, to be honest.` - ); - } - - return msg.reply( - [ - 'Uh-oh! An internal error has occured while running this.', - 'Contact the developers in discord.gg/ATmjFH9kMH under <#824071651486335036>:', - '', - '```js', - (ex as any).stack ?? '<... no stacktrace? ...>', - '```', - ].join('\n') - ); - } - } -} diff --git a/src/commands/moderation/WarningsCommand.ts b/src/commands/moderation/WarningsCommand.ts deleted file mode 100644 index a241be0f..00000000 --- a/src/commands/moderation/WarningsCommand.ts +++ /dev/null @@ -1,100 +0,0 @@ -/** - * Copyright (c) 2019-2021 Nino - * - * 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. - */ - -import { Command, CommandMessage, EmbedBuilder } from '../../structures'; -import { DiscordRESTError, User } from 'eris'; -import { Categories } from '../../util/Constants'; -import { Inject } from '@augu/lilith'; -import Database from '../../components/Database'; -import Discord from '../../components/Discord'; - -export default class WarningsCommand extends Command { - @Inject - private readonly database!: Database; - - @Inject - private readonly discord!: Discord; - - constructor() { - super({ - description: 'descriptions.warnings', - category: Categories.Moderation, - examples: ['warnings 280158289667555328'], - aliases: ['warns', 'view-warns'], - name: 'warnings', - }); - } - - async run(msg: CommandMessage, [userID]: string[]) { - if (!userID) return msg.reply('No user was specified.'); - - let user!: User | null; - try { - user = await this.discord.getUser(userID); - } catch (ex) { - if (ex instanceof DiscordRESTError && ex.code === 10013) - return msg.reply(`User with ID "${userID}" was not found. (assuming it's a deleted user)`); - - return msg.reply( - [ - 'Uh-oh! An internal error has occured while running this.', - 'Contact the developers in discord.gg/ATmjFH9kMH under <#824071651486335036>:', - '', - '```js', - (ex as any).stack ?? '<... no stacktrace? ...>', - '```', - ].join('\n') - ); - } - - if (user === null) return msg.reply('Bot or user was not found.'); - if (!msg.guild.members.has(user.id)) return msg.reply('Cannot view warnings outside of this guild.'); - if (user.bot) return msg.reply('Bots cannot be warned.'); - - const member = msg.guild.members.get(user.id)!; - if (member.id === msg.guild.ownerID) return msg.reply('Why would the server owner have any warnings...?'); - if (member.id === this.discord.client.user.id) return msg.reply('W-why would I have any warnings?!'); - if (member.permissions.has('administrator') || member.permissions.has('banMembers')) - return msg.reply("Moderators or administrators don't have warnings attached to them."); - - const warnings = await this.database.warnings - .getAll(msg.guild.id, user.id) - .then((warnings) => warnings.filter((warn) => warn.amount > 0)); - if (warnings.length === 0) - return msg.reply(`User **${user.username}#${user.discriminator}** doesn't have any warnings attached to them.`); - - const embed = EmbedBuilder.create() - .setTitle(`[ ${user.username}#${user.discriminator} (${user.id}) <~> Warnings ]`) - .setDescription(`They have a total of **${warnings.length}** warnings attached`) - .addFields( - warnings.map((warn, idx) => ({ - name: `❯ Warning #${idx + 1}`, - value: [`• **Amount**: ${warn.amount}`, `• **Reason**: ${warn.reason ?? '(no reason was provided)'}`].join( - '\n' - ), - inline: true, - })) - ); - - return msg.reply(embed); - } -} diff --git a/src/commands/moderation/voice/VoiceDeafenCommand.ts b/src/commands/moderation/voice/VoiceDeafenCommand.ts deleted file mode 100644 index ac97ed79..00000000 --- a/src/commands/moderation/voice/VoiceDeafenCommand.ts +++ /dev/null @@ -1,157 +0,0 @@ -/** - * Copyright (c) 2019-2021 Nino - * - * 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. - */ - -import { DiscordRESTError, Member, User, VoiceChannel } from 'eris'; -import { Command, CommandMessage } from '../../../structures'; -import { PunishmentType } from '@prisma/client'; -import PunishmentService from '../../../services/PunishmentService'; -import { Categories } from '../../../util/Constants'; -import Permissions from '../../../util/Permissions'; -import { Inject } from '@augu/lilith'; -import Discord from '../../../components/Discord'; -import ms = require('ms'); - -export default class VoiceDeafenCommand extends Command { - @Inject - private readonly punishments!: PunishmentService; - - @Inject - private readonly discord!: Discord; - - constructor() { - super({ - userPermissions: 'voiceMuteMembers', - description: 'descriptions.voice_deafen', - category: Categories.Moderation, - examples: ['vcdeaf <@256548545856545896>', 'vcdeaf 3', 'vcdeaf 3 some reason!', 'vcdeaf 3 some reason! | 3d'], - aliases: ['deafvc', 'vcdeaf'], - name: 'vcdeafen', - }); - } - - async run(msg: CommandMessage, [userID, ...reason]: string[]) { - if (!userID) return msg.reply('No bot or user was specified.'); - - let user!: User | null; - try { - user = await this.discord.getUser(userID); - } catch (ex) { - if (ex instanceof DiscordRESTError && ex.code === 10013) - return msg.reply(`User or bot with ID "${userID}" was not found. (assuming it's a deleted bot or user)`); - - return msg.reply( - [ - 'Uh-oh! An internal error has occured while running this.', - 'Contact the developers in discord.gg/ATmjFH9kMH under <#824071651486335036>:', - '', - '```js', - (ex as any).stack ?? '<... no stacktrace? ...>', - '```', - ].join('\n') - ); - } - - if (user === null) return msg.reply('Bot or user was not found.'); - - const member = msg.guild.members.get(user.id) ?? { - id: user.id, - guild: msg.guild, - }; - if (member.id === msg.guild.ownerID) - return msg.reply("I don't think I can perform this action due to you banning the owner, you idiot."); - - if (member instanceof Member) { - if (member.permissions.has('administrator') || member.permissions.has('banMembers')) - return msg.reply( - `I can't perform this action due to **${user.username}#${user.discriminator}** being a server moderator.` - ); - - if (!Permissions.isMemberAbove(msg.member, member)) - return msg.reply(`User **${user.username}#${user.discriminator}** is the same or above as you.`); - - if (!Permissions.isMemberAbove(msg.self!, member)) - return msg.reply(`User **${user.username}#${user.discriminator}** is the same or above me.`); - } - - if (msg.member.voiceState.channelID === null) - return msg.reply('You must be in a voice channel to perform this action.'); - - const channel = this.discord.client.getChannel(msg.member.voiceState.channelID) as VoiceChannel; - if (channel.voiceMembers.size === 1) return msg.reply('You must be in an active voice channel.'); - if (!channel.voiceMembers.has(user.id)) - return msg.reply(`Member **${user.username}#${user.discriminator}** is not in this voice channel.`); - - const voiceState = channel.voiceMembers.get(user.id)!.voiceState; - if (voiceState.deaf === true) - return msg.reply(`Member **${user.username}#${user.discriminator}** is already server deafened.`); - - const areason = reason.join(' '); - let actualReason: string | undefined = undefined; - let time: string | undefined = undefined; - - if (areason !== '') { - const [r, t] = areason.split(' | '); - actualReason = r; - time = t; - } - - // Nino needs to join the voice channel they're in. - await this.discord.client.joinVoiceChannel(msg.member.voiceState.channelID); - try { - await this.punishments.apply({ - moderator: msg.author, - publish: true, - reason: actualReason, - member: msg.guild.members.get(user.id) || { - id: user.id, - guild: msg.guild, - }, - type: PunishmentType.VOICE_DEAFEN, - time: time !== undefined ? ms(time!) : undefined, - }); - - this.discord.client.leaveVoiceChannel(msg.member.voiceState.channelID); - return msg.reply( - `:thumbsup: Member **${user.username}#${user.discriminator}** has been server deafened in voice channels.${ - reason.length ? ` *for ${reason.join(' ')}${time !== undefined ? `, for ${time}*` : '*'}` : '.' - }` - ); - } catch (ex) { - if (ex instanceof DiscordRESTError && ex.code === 10007) { - return msg.reply( - `Member **${user.username}#${user.discriminator}** has left but been detected. Kinda weird if you ask me, to be honest.` - ); - } - - return msg.reply( - [ - 'Uh-oh! An internal error has occured while running this.', - 'Contact the developers in discord.gg/ATmjFH9kMH under <#824071651486335036>:', - '', - '```js', - (ex as any).stack ?? '<... no stacktrace? ...>', - '```', - ].join('\n') - ); - } - } -} diff --git a/src/commands/moderation/voice/VoiceKickCommand.ts b/src/commands/moderation/voice/VoiceKickCommand.ts deleted file mode 100644 index 59364a8a..00000000 --- a/src/commands/moderation/voice/VoiceKickCommand.ts +++ /dev/null @@ -1,321 +0,0 @@ -/** - * Copyright (c) 2019-2021 Nino - * - * 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. - */ - -import { Command, CommandMessage, Subcommand } from '../../../structures'; -import type { Member, VoiceChannel } from 'eris'; -import { Categories } from '../../../util/Constants'; -import { Inject } from '@augu/lilith'; -import Discord from '../../../components/Discord'; - -const condition = (discord: Discord, member: Member) => - member.user.id !== discord.client.user.id && // If it's not Nino - member.guild.ownerID === member.user.id && // If the owner is in the voice channel - member.permissions.has('voiceMuteMembers'); // If the member is a voice moderator - -const botCondition = (discord: Discord, member: Member) => - member.user.id !== discord.client.user.id && // If it's not Nino - member.bot === true; // If it's a bot - -export default class VoiceKickCommand extends Command { - @Inject - private readonly discord!: Discord; - - constructor() { - super({ - userPermissions: 'voiceMoveMembers', - description: 'descriptions.voice_kick', - category: Categories.Moderation, - examples: [ - 'vckick | Kick all members in your voice channel', - 'vckick <#521254554543485646> | Kick all members in a specific channel', - 'vckick bots | Kick all bots in your voice channel', - 'vckick bots <#521254554543485646> | Kick all bots in a specific channel', - ], - aliases: ['kickvc'], - name: 'vckick', - }); - } - - async run(msg: CommandMessage, [channelOrAmount]: [string?]) { - if (!channelOrAmount) { - const message = await msg.reply('Kicking all members...'); - if (!msg.member.voiceState.channelID) return msg.reply('You must be in a voice channel.'); - - const id = msg.member.voiceState.channelID; // cache it if they decide to leave - const voiceChan = await this.discord.getChannel(id); - if (voiceChan === null) return msg.error("Unknown voice channel you're in."); - - if ( - !voiceChan.permissionsOf(this.discord.client.user.id).has('voiceConnect') || - !voiceChan.permissionsOf(this.discord.client.user.id).has('voiceMoveMembers') - ) - return msg.reply('I do not have permissions to **Connect** or **Move Members**.'); - - const members = voiceChan.voiceMembers.filter((c) => condition(this.discord, c)); - if (members.length === 0) - return msg.error( - 'No users were in this channel. (excluding myself, the owner, and people with **`Voice Mute Members`** permission)' - ); - - if (members.length === 1) { - await this.discord.client.joinVoiceChannel(id); - try { - await members[0].edit( - { channelID: null }, - encodeURIComponent( - `[Voice Kick] Told to kick ${members[0].username}#${members[0].discriminator} (${members[0].id})` - ) - ); - } catch { - return msg.error(`Unable to kick **${members[0].username}#${members[0].discriminator}**.`); - } - } - - await this.discord.client.joinVoiceChannel(id); - await message.edit(`ℹ️ **Removing ${members.length} members...**`); - - let success = 0; - let errored = 0; - for (const member of members) { - try { - success++; - await member.edit( - { channelID: null }, - encodeURIComponent(`[Voice Kick] Told to kick ${member.username}#${member.discriminator} (${member.id})`) - ); - } catch { - errored++; - } - } - - const errorRate = ((errored / members.length) * 100).toFixed(2); - const successRate = ((success / members.length) * 100).toFixed(2); - this.discord.client.leaveVoiceChannel(id); - - await message.delete(); - return msg.reply( - [ - `Successfully kicked **${success}/${members.length}** members.`, - '', - `> ${msg.successEmote} **Success Rate**: ${successRate}%`, - `> ${msg.errorEmote} **Error Rate**: ${errorRate}%`, - ].join('\n') - ); - } - - const channel = await this.discord.getChannel(channelOrAmount); - - // if I can recall correctly, IDs are around 15-21 but I could be wrong. - // ~ Noel - if (channel === null) return msg.reply(`Channel with ID **${channelOrAmount}** was not found.`); - - if (channel.type !== 2) return msg.reply('Channel was not a voice channel.'); - - if ( - !channel.permissionsOf(this.discord.client.user.id).has('voiceConnect') || - !channel.permissionsOf(this.discord.client.user.id).has('voiceMoveMembers') - ) - return msg.reply('I do not have permissions to **Connect** or **Move Members**.'); - - const members = channel.voiceMembers.filter((c) => condition(this.discord, c)); - if (members.length === 0) - return msg.error( - 'No users were in this channel. (excluding myself, the owner, and people with **`Voice Mute Members`** permission)' - ); - - if (members.length === 1) { - await this.discord.client.joinVoiceChannel(channel.id); - try { - await members[0].edit( - { channelID: null }, - encodeURIComponent( - `[Voice Kick] Told to kick ${members[0].username}#${members[0].discriminator} (${members[0].id})` - ) - ); - } catch { - return msg.error(`Unable to kick **${members[0].username}#${members[0].discriminator}**.`); - } - } - - const message = await msg.reply(`ℹ️ Kicking all members in <#${channel.id}> (${members.length} members)`); - await this.discord.client.joinVoiceChannel(channel.id); - - let success = 0; - let errored = 0; - for (const member of members) { - try { - success++; - await member.edit( - { channelID: null }, - encodeURIComponent(`[Voice Kick] Told to kick ${member.username}#${member.discriminator} (${member.id})`) - ); - } catch { - errored++; - } - } - - const errorRate = ((errored / members.length) * 100).toFixed(2); - const successRate = ((success / members.length) * 100).toFixed(2); - this.discord.client.leaveVoiceChannel(channel.id); - - await message.delete(); - return msg.reply( - [ - `Successfully kicked **${success}/${members.length}** members.`, - '', - `> ${msg.successEmote} **Success Rate**: ${successRate}%`, - `> ${msg.errorEmote} **Error Rate**: ${errorRate}%`, - ].join('\n') - ); - } - - @Subcommand('') - async bots(msg: CommandMessage, [channelOrAmount]: [string?]) { - if (!channelOrAmount) { - const message = await msg.reply('Kicking all bots...'); - if (!msg.member.voiceState.channelID) return msg.reply('You must be in a voice channel.'); - - const id = msg.member.voiceState.channelID; // cache it if they decide to leave - const voiceChan = await this.discord.getChannel(id); - if (voiceChan === null) return msg.error("Unknown voice channel you're in."); - - if ( - !voiceChan.permissionsOf(this.discord.client.user.id).has('voiceConnect') || - !voiceChan.permissionsOf(this.discord.client.user.id).has('voiceMoveMembers') - ) - return msg.reply('I do not have permissions to **Connect** or **Move Members**.'); - - const members = voiceChan.voiceMembers.filter((c) => botCondition(this.discord, c)); - if (members.length === 0) return msg.error('No bots were in this channel. (excluding myself)'); - - if (members.length === 1) { - await this.discord.client.joinVoiceChannel(id); - try { - await members[0].edit( - { channelID: null }, - encodeURIComponent( - `[Voice Kick] Told to kick ${members[0].username}#${members[0].discriminator} (${members[0].id})` - ) - ); - } catch { - return msg.error(`Unable to kick bot **${members[0].username}#${members[0].discriminator}**.`); - } - } - - await this.discord.client.joinVoiceChannel(id); - await message.edit(`ℹ️ **Removing ${members.length} bots...**`); - - let success = 0; - let errored = 0; - for (const member of members) { - try { - success++; - await member.edit( - { channelID: null }, - encodeURIComponent(`[Voice Kick] Told to kick ${member.username}#${member.discriminator} (${member.id})`) - ); - } catch { - errored++; - } - } - - const errorRate = ((errored / members.length) * 100).toFixed(2); - const successRate = ((success / members.length) * 100).toFixed(2); - this.discord.client.leaveVoiceChannel(id); - - await message.delete(); - return msg.reply( - [ - `Successfully kicked **${success}/${members.length}** bots.`, - '', - `> ${msg.successEmote} **Success Rate**: ${successRate}%`, - `> ${msg.errorEmote} **Error Rate**: ${errorRate}%`, - ].join('\n') - ); - } - - const channel = await this.discord.getChannel(channelOrAmount); - - // if I can recall correctly, IDs are around 15-21 but I could be wrong. - // ~ Noel - if (channel === null) return msg.reply(`Channel with ID **${channelOrAmount}** was not found.`); - - if (channel.type !== 2) return msg.reply('Channel was not a voice channel.'); - - if ( - !channel.permissionsOf(this.discord.client.user.id).has('voiceConnect') || - !channel.permissionsOf(this.discord.client.user.id).has('voiceMoveMembers') - ) - return msg.reply('I do not have permissions to **Connect** or **Move Members**.'); - - const members = channel.voiceMembers.filter((c) => botCondition(this.discord, c)); - if (members.length === 0) - return msg.error( - 'No users were in this channel. (excluding myself, the owner, and people with **`Voice Mute Members`** permission)' - ); - - if (members.length === 1) { - await this.discord.client.joinVoiceChannel(channel.id); - try { - await members[0].edit( - { channelID: null }, - encodeURIComponent( - `[Voice Kick] Told to kick ${members[0].username}#${members[0].discriminator} (${members[0].id})` - ) - ); - } catch { - return msg.error(`Unable to kick **${members[0].username}#${members[0].discriminator}**.`); - } - } - - const message = await msg.reply(`ℹ️ Kicking all members in <#${channel.id}> (${members.length} members)`); - await this.discord.client.joinVoiceChannel(channel.id); - - let success = 0; - let errored = 0; - for (const member of members) { - try { - success++; - await member.edit( - { channelID: null }, - encodeURIComponent(`[Voice Kick] Told to kick ${member.username}#${member.discriminator} (${member.id})`) - ); - } catch { - errored++; - } - } - - const errorRate = ((errored / members.length) * 100).toFixed(2); - const successRate = ((success / members.length) * 100).toFixed(2); - this.discord.client.leaveVoiceChannel(channel.id); - - await message.delete(); - return msg.reply( - [ - `Successfully kicked **${success}/${members.length}** members.`, - '', - `> ${msg.successEmote} **Success Rate**: ${successRate}%`, - `> ${msg.errorEmote} **Error Rate**: ${errorRate}%`, - ].join('\n') - ); - } -} diff --git a/src/commands/moderation/voice/VoiceMuteCommand.ts b/src/commands/moderation/voice/VoiceMuteCommand.ts deleted file mode 100644 index 6dc3ebe1..00000000 --- a/src/commands/moderation/voice/VoiceMuteCommand.ts +++ /dev/null @@ -1,157 +0,0 @@ -/** - * Copyright (c) 2019-2021 Nino - * - * 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. - */ - -import { DiscordRESTError, Member, User, VoiceChannel } from 'eris'; -import { Command, CommandMessage } from '../../../structures'; -import { PunishmentType } from '@prisma/client'; -import PunishmentService from '../../../services/PunishmentService'; -import { Categories } from '../../../util/Constants'; -import Permissions from '../../../util/Permissions'; -import { Inject } from '@augu/lilith'; -import Discord from '../../../components/Discord'; -import ms = require('ms'); - -export default class VoiceMuteCommand extends Command { - @Inject - private readonly punishments!: PunishmentService; - - @Inject - private readonly discord!: Discord; - - constructor() { - super({ - description: 'descriptions.voice_mute', - category: Categories.Moderation, - examples: ['vcmute <@256548545856545896>', 'vcmute 3', 'vcmute 3 some reason!', 'vcmute 3 some reason! | 3d'], - aliases: ['mutevc'], - name: 'vcmute', - }); - } - - async run(msg: CommandMessage, [userID, ...reason]: string[]) { - if (!userID) return msg.reply('No bot or user was specified.'); - - let user!: User | null; - try { - user = await this.discord.getUser(userID); - } catch (ex) { - if (ex instanceof DiscordRESTError && ex.code === 10013) - return msg.reply(`User or bot with ID "${userID}" was not found. (assuming it's a deleted bot or user)`); - - return msg.reply( - [ - 'Uh-oh! An internal error has occured while running this.', - 'Contact the developers in discord.gg/ATmjFH9kMH under <#824071651486335036>:', - '', - '```js', - (ex as any).stack ?? '<... no stacktrace? ...>', - '```', - ].join('\n') - ); - } - - if (user === null) return msg.reply('Bot or user was not found.'); - - const member = msg.guild.members.get(user.id) ?? { - id: user.id, - guild: msg.guild, - }; - if (member.id === msg.guild.ownerID) - return msg.reply("I don't think I can perform this action due to you banning the owner, you idiot."); - - if (member instanceof Member) { - if (member.permissions.has('administrator') || member.permissions.has('banMembers')) - return msg.reply( - `I can't perform this action due to **${user.username}#${user.discriminator}** being a server moderator.` - ); - - if (!Permissions.isMemberAbove(msg.member, member)) - return msg.reply(`User **${user.username}#${user.discriminator}** is the same or above as you.`); - - if (!Permissions.isMemberAbove(msg.self!, member)) - return msg.reply(`User **${user.username}#${user.discriminator}** is the same or above me.`); - } - - if (msg.member.voiceState.channelID === null) - return msg.reply('You must be in a voice channel to perform this action.'); - - const channel = this.discord.client.getChannel(msg.member.voiceState.channelID) as VoiceChannel; - if (channel.voiceMembers.size === 1) return msg.reply('You must be in an active voice channel.'); - - if (!channel.voiceMembers.has(user.id)) - return msg.reply(`Member **${user.username}#${user.discriminator}** is not in this voice channel.`); - - const voiceState = channel.voiceMembers.get(user.id)!.voiceState; - if (voiceState.mute === true) - return msg.reply(`Member **${user.username}#${user.discriminator}** is already server muted.`); - - const areason = reason.join(' '); - let actualReason: string | undefined = undefined; - let time: string | undefined = undefined; - - if (areason !== '') { - const [r, t] = areason.split(' | '); - actualReason = r; - time = t; - } - - // Nino needs to join the voice channel they're in. - await this.discord.client.joinVoiceChannel(msg.member.voiceState.channelID); - try { - await this.punishments.apply({ - moderator: msg.author, - publish: true, - reason: actualReason, - member: msg.guild.members.get(user.id) || { - id: user.id, - guild: msg.guild, - }, - type: PunishmentType.VOICE_MUTE, - time: time !== undefined ? ms(time!) : undefined, - }); - - this.discord.client.leaveVoiceChannel(msg.member.voiceState.channelID); - return msg.reply( - `:thumbsup: Member **${user.username}#${user.discriminator}** has been server muted in voice channels.${ - reason.length ? ` *for ${reason.join(' ')}${time !== undefined ? `, for ${time}*` : '*'}` : '.' - }` - ); - } catch (ex) { - if (ex instanceof DiscordRESTError && ex.code === 10007) { - return msg.reply( - `Member **${user.username}#${user.discriminator}** has left but been detected. Kinda weird if you ask me, to be honest.` - ); - } - - return msg.reply( - [ - 'Uh-oh! An internal error has occured while running this.', - 'Contact the developers in discord.gg/ATmjFH9kMH under <#824071651486335036>:', - '', - '```js', - (ex as any).stack ?? '<... no stacktrace? ...>', - '```', - ].join('\n') - ); - } - } -} diff --git a/src/commands/moderation/voice/VoiceUndeafenCommand.ts b/src/commands/moderation/voice/VoiceUndeafenCommand.ts deleted file mode 100644 index a13d8a3c..00000000 --- a/src/commands/moderation/voice/VoiceUndeafenCommand.ts +++ /dev/null @@ -1,157 +0,0 @@ -/** - * Copyright (c) 2019-2021 Nino - * - * 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. - */ - -import { DiscordRESTError, Member, User, VoiceChannel } from 'eris'; -import { Command, CommandMessage } from '../../../structures'; -import { PunishmentType } from '@prisma/client'; -import PunishmentService from '../../../services/PunishmentService'; -import { Categories } from '../../../util/Constants'; -import Permissions from '../../../util/Permissions'; -import { Inject } from '@augu/lilith'; -import Discord from '../../../components/Discord'; -import ms = require('ms'); - -export default class VoiceUndeafenCommand extends Command { - @Inject - private readonly punishments!: PunishmentService; - - @Inject - private readonly discord!: Discord; - - constructor() { - super({ - description: 'descriptions.voice_undeafen', - category: Categories.Moderation, - examples: ['vcundeaf <@256548545856545896>', 'vcundeaf 3 some reason!'], - aliases: ['undeafvc'], - name: 'vcundeaf', - }); - } - - async run(msg: CommandMessage, [userID, ...reason]: string[]) { - if (!userID) return msg.reply('No bot or user was specified.'); - - let user!: User | null; - try { - user = await this.discord.getUser(userID); - } catch (ex) { - if (ex instanceof DiscordRESTError && ex.code === 10013) - return msg.reply(`User or bot with ID "${userID}" was not found. (assuming it's a deleted bot or user)`); - - return msg.reply( - [ - 'Uh-oh! An internal error has occured while running this.', - 'Contact the developers in discord.gg/ATmjFH9kMH under <#824071651486335036>:', - '', - '```js', - (ex as any).stack ?? '<... no stacktrace? ...>', - '```', - ].join('\n') - ); - } - - if (user === null) return msg.reply('Bot or user was not found.'); - - const member = msg.guild.members.get(user.id) ?? { - id: user.id, - guild: msg.guild, - }; - if (member.id === msg.guild.ownerID) - return msg.reply("I don't think I can perform this action due to you banning the owner, you idiot."); - - if (member instanceof Member) { - if (member.permissions.has('administrator') || member.permissions.has('banMembers')) - return msg.reply( - `I can't perform this action due to **${user.username}#${user.discriminator}** being a server moderator.` - ); - - if (!Permissions.isMemberAbove(msg.member, member)) - return msg.reply(`User **${user.username}#${user.discriminator}** is the same or above as you.`); - - if (!Permissions.isMemberAbove(msg.self!, member)) - return msg.reply(`User **${user.username}#${user.discriminator}** is the same or above me.`); - } - - if (msg.member.voiceState.channelID === null) - return msg.reply('You must be in a voice channel to perform this action.'); - - const channel = this.discord.client.getChannel(msg.member.voiceState.channelID) as VoiceChannel; - if (channel.voiceMembers.size === 1) return msg.reply('You must be in an active voice channel.'); - - if (!channel.voiceMembers.has(user.id)) - return msg.reply(`Member **${user.username}#${user.discriminator}** is not in this voice channel.`); - - const voiceState = channel.voiceMembers.get(user.id)!.voiceState; - if (!voiceState.deaf) - return msg.reply(`Member **${user.username}#${user.discriminator}** is already not deafened.`); - - const areason = reason.join(' '); - let actualReason: string | undefined = undefined; - let time: string | undefined = undefined; - - if (areason !== '') { - const [r, t] = areason.split(' | '); - actualReason = r; - time = t; - } - - // Nino needs to join the voice channel they're in. - await this.discord.client.joinVoiceChannel(msg.member.voiceState.channelID); - try { - await this.punishments.apply({ - moderator: msg.author, - publish: true, - reason: actualReason, - member: msg.guild.members.get(user.id) || { - id: user.id, - guild: msg.guild, - }, - type: PunishmentType.VOICE_UNDEAFEN, - time: time !== undefined ? ms(time!) : undefined, - }); - - this.discord.client.leaveVoiceChannel(msg.member.voiceState.channelID); - return msg.reply( - `:thumbsup: Member **${user.username}#${user.discriminator}** has been server undeafen in voice channels.${ - reason.length ? ` *for ${reason.join(' ')}${time !== undefined ? `, for ${time}*` : '*'}` : '.' - }` - ); - } catch (ex) { - if (ex instanceof DiscordRESTError && ex.code === 10007) { - return msg.reply( - `Member **${user.username}#${user.discriminator}** has left but been detected. Kinda weird if you ask me, to be honest.` - ); - } - - return msg.reply( - [ - 'Uh-oh! An internal error has occured while running this.', - 'Contact the developers in discord.gg/ATmjFH9kMH under <#824071651486335036>:', - '', - '```js', - (ex as any).stack ?? '<... no stacktrace? ...>', - '```', - ].join('\n') - ); - } - } -} diff --git a/src/commands/moderation/voice/VoiceUnmuteCommand.ts b/src/commands/moderation/voice/VoiceUnmuteCommand.ts deleted file mode 100644 index 41a87a4a..00000000 --- a/src/commands/moderation/voice/VoiceUnmuteCommand.ts +++ /dev/null @@ -1,156 +0,0 @@ -/** - * Copyright (c) 2019-2021 Nino - * - * 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. - */ - -import { DiscordRESTError, Member, User, VoiceChannel } from 'eris'; -import { Command, CommandMessage } from '../../../structures'; -import { PunishmentType } from '@prisma/client'; -import PunishmentService from '../../../services/PunishmentService'; -import { Categories } from '../../../util/Constants'; -import Permissions from '../../../util/Permissions'; -import { Inject } from '@augu/lilith'; -import Discord from '../../../components/Discord'; -import ms = require('ms'); - -export default class VoiceUnmuteCommand extends Command { - @Inject - private readonly punishments!: PunishmentService; - - @Inject - private readonly discord!: Discord; - - constructor() { - super({ - description: 'descriptions.voice_mute', - category: Categories.Moderation, - examples: ['vcunmute <@256548545856545896>', 'vcunmute 3 some reason!'], - aliases: ['unmutevc'], - name: 'vcunmute', - }); - } - - async run(msg: CommandMessage, [userID, ...reason]: string[]) { - if (!userID) return msg.reply('No bot or user was specified.'); - - let user!: User | null; - try { - user = await this.discord.getUser(userID); - } catch (ex) { - if (ex instanceof DiscordRESTError && ex.code === 10013) - return msg.reply(`User or bot with ID "${userID}" was not found. (assuming it's a deleted bot or user)`); - - return msg.reply( - [ - 'Uh-oh! An internal error has occured while running this.', - 'Contact the developers in discord.gg/ATmjFH9kMH under <#824071651486335036>:', - '', - '```js', - (ex as any).stack ?? '<... no stacktrace? ...>', - '```', - ].join('\n') - ); - } - - if (user === null) return msg.reply('Bot or user was not found.'); - - const member = msg.guild.members.get(user.id) ?? { - id: user.id, - guild: msg.guild, - }; - if (member.id === msg.guild.ownerID) - return msg.reply("I don't think I can perform this action due to you banning the owner, you idiot."); - - if (member instanceof Member) { - if (member.permissions.has('administrator') || member.permissions.has('banMembers')) - return msg.reply( - `I can't perform this action due to **${user.username}#${user.discriminator}** being a server moderator.` - ); - - if (!Permissions.isMemberAbove(msg.member, member)) - return msg.reply(`User **${user.username}#${user.discriminator}** is the same or above as you.`); - - if (!Permissions.isMemberAbove(msg.self!, member)) - return msg.reply(`User **${user.username}#${user.discriminator}** is the same or above me.`); - } - - if (msg.member.voiceState.channelID === null) - return msg.reply('You must be in a voice channel to perform this action.'); - - const channel = this.discord.client.getChannel(msg.member.voiceState.channelID) as VoiceChannel; - if (channel.voiceMembers.size === 1) return msg.reply('You must be in an active voice channel.'); - - if (!channel.voiceMembers.has(user.id)) - return msg.reply(`Member **${user.username}#${user.discriminator}** is not in this voice channel.`); - - const voiceState = channel.voiceMembers.get(user.id)!.voiceState; - if (!voiceState.mute) return msg.reply(`Member **${user.username}#${user.discriminator}** is already unmuted.`); - - const areason = reason.join(' '); - let actualReason: string | undefined = undefined; - let time: string | undefined = undefined; - - if (areason !== '') { - const [r, t] = areason.split(' | '); - actualReason = r; - time = t; - } - - // Nino needs to join the voice channel they're in. - await this.discord.client.joinVoiceChannel(msg.member.voiceState.channelID); - try { - await this.punishments.apply({ - moderator: msg.author, - publish: true, - reason: actualReason, - member: msg.guild.members.get(user.id) || { - id: user.id, - guild: msg.guild, - }, - type: PunishmentType.VOICE_UNMUTE, - time: time !== undefined ? ms(time!) : undefined, - }); - - this.discord.client.leaveVoiceChannel(msg.member.voiceState.channelID); - return msg.reply( - `:thumbsup: Member **${user.username}#${user.discriminator}** has been unmuted in voice channels.${ - reason.length ? ` *for ${reason.join(' ')}${time !== undefined ? `, for ${time}*` : '*'}` : '.' - }` - ); - } catch (ex) { - if (ex instanceof DiscordRESTError && ex.code === 10007) { - return msg.reply( - `Member **${user.username}#${user.discriminator}** has left but been detected. Kinda weird if you ask me, to be honest.` - ); - } - - return msg.reply( - [ - 'Uh-oh! An internal error has occured while running this.', - 'Contact the developers in discord.gg/ATmjFH9kMH under <#824071651486335036>:', - '', - '```js', - (ex as any).stack ?? '<... no stacktrace? ...>', - '```', - ].join('\n') - ); - } - } -} diff --git a/src/commands/owner/BlacklistCommand.ts b/src/commands/owner/BlacklistCommand.ts deleted file mode 100644 index 95c884c2..00000000 --- a/src/commands/owner/BlacklistCommand.ts +++ /dev/null @@ -1,113 +0,0 @@ -/** - * Copyright (c) 2019-2021 Nino - * - * 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. - */ - -import { Command, CommandMessage, EmbedBuilder } from '../../structures'; -import { Categories, Color } from '../../util/Constants'; -import { BlacklistType } from '../../entities/BlacklistEntity'; -import { Inject } from '@augu/lilith'; -import Database from '../../components/Database'; -import Discord from '../../components/Discord'; -import Config from '../../components/Config'; - -export default class BlacklistCommand extends Command { - @Inject - private database!: Database; - - @Inject - private discord!: Discord; - - @Inject - private config!: Config; - - constructor() { - super({ - description: 'Blacklists a user or guild from Nino', - category: Categories.Owner, - hidden: true, - ownerOnly: true, - aliases: ['bl'], - usage: '["guild" | "user"] [id] [...reason]', - name: 'blacklist', - }); - } - - async run(msg: CommandMessage, [type, id, ...reason]: ['guild' | 'user', string, ...string[]]) { - if (!type) { - const guilds = await this.database.blacklists.getByType(BlacklistType.Guild); - const users = await this.database.blacklists.getByType(BlacklistType.User); - - return msg.reply( - new EmbedBuilder() - .setColor(Color) - .setDescription([ - `❯ **Guilds Blacklisted**: ${guilds.length.toLocaleString()}`, - `❯ **Users Blacklisted**: ${users.length.toLocaleString()}`, - ]) - ); - } - - if (!['guild', 'user'].includes(type)) - return msg.reply('Missing the type to blacklist. Available options: `user` and `guild`.'); - - if (type === 'guild') { - const guild = this.discord.client.guilds.get(id); - if (!guild) return msg.reply(`Guild **${id}** doesn't exist`); - - const entry = await this.database.blacklists.get(id); - if (entry !== undefined) return msg.reply(`Guild **${guild.name}** is already on the blacklist.`); - - await this.database.blacklists.create({ - issuer: msg.author.id, - reason: reason ? reason.join(' ') : undefined, - type: BlacklistType.Guild, - id, - }); - - return msg.reply(`:thumbsup: Blacklisted guild **${guild.name}** for *${reason ?? 'no reason provided'}*`); - } - - if (type === 'user') { - const owners = this.config.getProperty('owners') ?? []; - const user = await this.discord.getUser(id); - if (user === null) return msg.reply(`User ${id} doesn't exist.`); - - if (owners.includes(id)) return msg.reply('Cannot blacklist a owner'); - - const entry = await this.database.blacklists.get(user.id); - if (entry !== undefined) - return msg.reply(`User **${user.username}#${user.discriminator}** is already on the blacklist.`); - - await this.database.blacklists.create({ - issuer: msg.author.id, - reason: reason ? reason.join(' ') : undefined, - type: BlacklistType.User, - id: user.id, - }); - - return msg.reply( - `:thumbsup: Blacklisted user ${user.username}#${user.discriminator} for *${ - reason?.join(' ') ?? 'no reason, just felt like it.' - }*` - ); - } - } -} diff --git a/src/commands/owner/EvalCommand.ts b/src/commands/owner/EvalCommand.ts deleted file mode 100644 index 052de081..00000000 --- a/src/commands/owner/EvalCommand.ts +++ /dev/null @@ -1,139 +0,0 @@ -/** - * Copyright (c) 2019-2021 Nino - * - * 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. - */ - -import { Command, CommandMessage } from '../../structures'; -import { Categories } from '../../util/Constants'; -import { inspect } from 'util'; -import { Inject } from '@augu/lilith'; -import Stopwatch from '../../util/Stopwatch'; -import Config from '../../components/Config'; - -export default class EvalCommand extends Command { - @Inject - private config!: Config; - - constructor() { - super({ - description: 'Evaluates JavaScript code and return a clean output', - ownerOnly: true, - hidden: true, - category: Categories.Owner, - aliases: ['evl', 'ev', 'js'], - name: 'eval', - }); - } - - async run(msg: CommandMessage, args: string[]) { - if (!args.length) return msg.reply('What do you want me to evaluate?'); - - let script = args.join(' '); - let result: any; - let depth = 1; - - const flags = msg.flags<{ - depth?: string | true; - slient?: string | true; - s?: string | true; - }>(); - - if (flags.depth === true) return msg.reply('`--depth` flag requires a input. Example: `--depth 1`'); - - if (flags.depth !== undefined) { - depth = Number(flags.depth); - if (isNaN(depth)) return msg.reply('Depth value was not a number'); - - script = script.replace(`--depth=${depth}`, ''); - } - - if (script.startsWith('```js') && script.endsWith('```')) { - script = script.replace('```js', ''); - script = script.replace('```', ''); - } - - const stopwatch = new Stopwatch(); - const isAsync = script.includes('return') || script.includes('await'); - const slient = flags.slient === true || flags.s === true; - - stopwatch.start(); - try { - result = eval(isAsync ? `(async()=>{${script}})()` : script); - - const time = stopwatch.end(); - let asyncTimer: string | undefined = undefined; - if (result instanceof Promise) { - stopwatch.restart(); - result = await result; - - asyncTimer = stopwatch.end(); - } - - if (typeof result !== 'string') - result = inspect(result, { - depth, - showHidden: false, - }); - - if (slient) return; - - const res = this.redact(result); - return msg.reply( - [`:timer: **${asyncTimer !== undefined ? `${time}<${asyncTimer}>` : time}**`, '', '```js', res, '```'].join( - '\n' - ) - ); - } catch (ex) { - const time = stopwatch.end(); - return msg.reply( - [`:timer: **${time}**`, '', '```js', (ex as any).stack ?? '<... no stacktrace ...>', '```'].join('\n') - ); - } - } - - private redact(script: string) { - const rawConfig = this.config['config']; // yes we need the raw config cuz i dont feel like using .getProperty :woeme: - let tokens = [ - ...(rawConfig.redis.sentinels?.map((r) => r.host) ?? []), - rawConfig.database.username, - rawConfig.database.password, - rawConfig.redis.password, - rawConfig.database.host, - rawConfig.database.url, - rawConfig.redis.host, - rawConfig.sentryDsn, - rawConfig.ksoft, - rawConfig.token, - ]; - - if (rawConfig.botlists !== undefined) - tokens.push( - rawConfig.botlists.dservices, - rawConfig.botlists.dboats, - rawConfig.botlists.topgg, - rawConfig.botlists.delly, - rawConfig.botlists.dbots, - rawConfig.botlists.bfd - ); - - tokens = tokens.filter(Boolean); - return script.replace(new RegExp(tokens.join('|'), 'gi'), 'owo? nu!!!'); - } -} diff --git a/src/commands/owner/WhitelistCommand.ts b/src/commands/owner/WhitelistCommand.ts deleted file mode 100644 index 80d4775a..00000000 --- a/src/commands/owner/WhitelistCommand.ts +++ /dev/null @@ -1,71 +0,0 @@ -/** - * Copyright (c) 2019-2021 Nino - * - * 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. - */ - -import { Command, CommandMessage } from '../../structures'; -import { Categories } from '../../util/Constants'; -import { Inject } from '@augu/lilith'; -import Database from '../../components/Database'; -import Config from '../../components/Config'; - -export default class WhitelistCommand extends Command { - @Inject - private database!: Database; - - @Inject - private config!: Config; - - constructor() { - super({ - description: 'Whitelists a user or guild from Nino', - category: Categories.Owner, - ownerOnly: true, - hidden: true, - aliases: ['wl'], - usage: '["guild" | "user"] [id] [...reason]', - name: 'whitelist', - }); - } - - async run(msg: CommandMessage, [type, id]: ['guild' | 'user', string]) { - if (!['guild', 'user'].includes(type)) - return msg.reply('Missing the type to blacklist. Available options: `user` and `guild`.'); - - if (type === 'guild') { - const entry = await this.database.blacklists.get(id); - if (entry === undefined) return msg.reply(`Guild **${id}** is already whitelisted`); - - await this.database.blacklists.delete(id); - return msg.reply(`:thumbsup: Whitelisted guild **${id}**.`); - } - - if (type === 'user') { - const owners = this.config.getProperty('owners') ?? []; - if (owners.includes(id)) return msg.reply('Cannot whitelist a owner'); - - const entry = await this.database.blacklists.get(id); - if (entry === undefined) return msg.reply(`User **${id}** is already whitelisted`); - - await this.database.blacklists.delete(id); - return msg.reply(`:thumbsup: Whitelisted user ${id}.`); - } - } -} diff --git a/src/commands/settings/Automod.ts b/src/commands/settings/Automod.ts deleted file mode 100644 index 5e971761..00000000 --- a/src/commands/settings/Automod.ts +++ /dev/null @@ -1,202 +0,0 @@ -/** - * Copyright (c) 2019-2021 Nino - * - * 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. - */ - -import { Command, CommandMessage, Subcommand, EmbedBuilder } from '../../structures'; -import { Categories, Color } from '../../util/Constants'; -import { Inject } from '@augu/lilith'; -import Database from '../../components/Database'; -import Discord from '../../components/Discord'; -import { TextChannel } from 'eris'; - -export default class AutomodCommand extends Command { - @Inject - private readonly database!: Database; - - @Inject - private readonly discord!: Discord; - - constructor() { - super({ - userPermissions: 'manageGuild', - description: 'descriptions.automod', - category: Categories.Settings, - examples: ['automod', 'automod spam', 'automod blacklist uwu owo'], - name: 'automod', - }); - } - - async run(msg: CommandMessage) { - const settings = await this.database.automod.get(msg.guild.id); - const embed = new EmbedBuilder() - .setColor(Color) - .setDescription([ - `• ${settings!.shortLinks ? msg.successEmote : msg.errorEmote} **Short Links** (\`${ - msg.settings.prefixes[0] - }automod shortlinks\`)`, - `• ${settings!.blacklist ? msg.successEmote : msg.errorEmote} **Blacklist Words** (\`${ - msg.settings.prefixes[0] - }automod blacklist\`)`, - `• ${settings!.mentions ? msg.successEmote : msg.errorEmote} **Mentions** (\`${ - msg.settings.prefixes[0] - }automod mentions\`)`, - `• ${settings!.dehoist ? msg.successEmote : msg.errorEmote} **Dehoisting** (\`${ - msg.settings.prefixes[0] - }automod dehoist\`)`, - `• ${settings!.invites ? msg.successEmote : msg.errorEmote} **Invites** (\`${ - msg.settings.prefixes[0] - }automod invites\`)`, - `• ${settings!.raid ? msg.successEmote : msg.errorEmote} **Raids** (\`${ - msg.settings.prefixes[0] - }automod raid\`)`, - `• ${settings!.spam ? msg.successEmote : msg.errorEmote} **Spam** (\`${ - msg.settings.prefixes[0] - }automod spam\`)`, - ]); - - return msg.reply(embed); - } - - @Subcommand() - async shortlinks(msg: CommandMessage) { - const settings = await this.database.automod.get(msg.guild.id); - const enabled = !settings!.shortLinks; - - await this.database.automod.update(msg.guild.id, { - shortLinks: enabled, - }); - - return msg.reply(`${enabled ? msg.successEmote : msg.errorEmote} the Shortlinks automod feature`); - } - - @Subcommand('[...words | "remove" ...words | "list"]') - async blacklist(msg: CommandMessage, [...words]: [...string[]]) { - const settings = await this.database.automod.get(msg.guild.id); - - if (!words.length) { - const type = !settings!.blacklist; - const res = await this.database.automod.update(msg.guild.id, { - blacklist: type, - }); - - const suffix = res ? 'd' : ''; - return msg.reply( - `${res ? `${msg.successEmote} Successfully` : `${msg.errorEmote} Unable to`} **${ - type ? `enable${suffix}` : `disable${suffix}` - }** the Blacklist automod feature.` - ); - } - - if (words[0] === 'remove') { - // Remove argument "remove" from the words list - words.shift(); - - const all: string[] = settings!.blacklistWords; - for (const word of words) { - const index = all.indexOf(word); - if (index !== -1) all.splice(index, 1); - } - - await this.database.automod.update(msg.guild.id, { - blacklistWords: all, - }); - - return msg.success('Successfully removed the blacklisted words. :D'); - } - - if (words[0] === 'list') { - const automod = await this.database.automod.get(msg.guild.id); - const embed = EmbedBuilder.create() - .setTitle('Blacklisted Words') - .setDescription([ - `:eyes: Hi **${msg.author.tag}**, I would like to inform you that I'll be deleting this message in 10 seconds`, - "due to the words that are probably blacklisted, don't want to offend anyone. :c", - '', - automod!.blacklistWords.map((s) => `\`${s}\``).join(', '), - ]); - - return msg.reply(embed).then((m) => setTimeout(() => m.delete(), 10_000)); - } - - const curr = settings!.blacklistWords.concat(words); - await this.database.automod.update(msg.guild.id, { - blacklistWords: curr, - }); - - return msg.success('Successfully added the words to the blacklist. :3'); - } - - @Subcommand() - async mentions(msg: CommandMessage) { - const settings = await this.database.automod.get(msg.guild.id); - const type = !settings!.mentions; - - await this.database.automod.update(msg.guild.id, { - mentions: type, - }); - - return msg.reply( - `${type ? `${msg.successEmote} **Enabled**` : `${msg.errorEmote} **Disabled**`} Mentions automod feature.` - ); - } - - @Subcommand() - async invites(msg: CommandMessage) { - const settings = await this.database.automod.get(msg.guild.id); - const t = !settings!.invites; - - await this.database.automod.update(msg.guild.id, { - invites: !settings!.invites, - }); - - return msg.reply( - `${t ? `${msg.successEmote} **Enabled**` : `${msg.errorEmote} **Disabled**`} Invites automod feature.` - ); - } - - @Subcommand() - async dehoist(msg: CommandMessage) { - const settings = await this.database.automod.get(msg.guild.id); - const t = !settings!.dehoist; - - await this.database.automod.update(msg.guild.id, { - dehoist: !settings!.dehoist, - }); - - return msg.reply( - `${t ? `${msg.successEmote} **Enabled**` : `${msg.errorEmote} **Disabled**`} Dehoisting automod feature.` - ); - } - - @Subcommand() - async spam(msg: CommandMessage) { - const settings = await this.database.automod.get(msg.guild.id); - const t = !settings!.spam; - - await this.database.automod.update(msg.guild.id, { - spam: t, - }); - - return msg.reply( - `${t ? `${msg.successEmote} **Enabled**` : `${msg.errorEmote} **Disabled**`} Spam automod feature.` - ); - } -} diff --git a/src/commands/settings/Logging.ts b/src/commands/settings/Logging.ts deleted file mode 100644 index 8ecc9511..00000000 --- a/src/commands/settings/Logging.ts +++ /dev/null @@ -1,236 +0,0 @@ -/** - * Copyright (c) 2019-2021 Nino - * - * 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. - */ - -import { Command, CommandMessage, EmbedBuilder, Subcommand } from '../../structures'; -import type { AnyGuildChannel, TextChannel, User } from 'eris'; -import { LoggingEvents } from '../../entities/LoggingEntity'; -import { Categories } from '../../util/Constants'; -import { Inject } from '@augu/lilith'; -import Database from '../../components/Database'; -import Discord from '../../components/Discord'; - -const LOGGING_EVENTS = Object.values(LoggingEvents).map((event) => - event.replace('channel_', '').replace('left', 'leave').replace(/_/g, '.') -); -const humanizedEvents = { - [LoggingEvents.VoiceChannelSwitch]: 'Voice Channel Switch', - [LoggingEvents.VoiceChannelLeft]: 'Voice Channel Leave', - [LoggingEvents.VoiceChannelJoin]: 'Voice Channel Join', - [LoggingEvents.MessageUpdated]: 'Message Updated', - [LoggingEvents.MessageDeleted]: 'Message Deleted', -}; - -export default class ModLogCommand extends Command { - @Inject - private readonly database!: Database; - - @Inject - private readonly discord!: Discord; - - constructor() { - super({ - userPermissions: ['manageGuild'], - description: 'descriptions.logging', - category: Categories.Settings, - examples: [ - 'logging | Enable / Disable the feature', - 'logging list | List the current configuration', - 'logging reset | Reset the mod log channel', - 'logging events | Lists all logging events', - 'logging events * | Enables all logging events', - 'logging events -* | Disables all logging events', - 'logging event message.update | Enable / Disable a specific event', - 'logging 236987412589632587 | Specify a logs channel', - 'logging ignore 5246968653214587563 | Ignores a channel from displaying logs', - 'logging ignore 1532589645236985346 | Ignores a specific user from being displayed in logs', - ], - aliases: ['logs'], - usage: '[channelID]', - name: 'logging', - }); - } - - async run(msg: CommandMessage, [channel]: [string]) { - if (!channel) { - const settings = await this.database.logging.get(msg.guild.id); - const type = !settings.enabled; - - await this.database.logging.update(msg.guild.id, { - enabled: type, - }); - - return msg.reply( - `${type ? msg.successEmote : msg.errorEmote} Successfully **${ - type ? 'enabled' : 'disabled' - }** the Logging feature.` - ); - } - - const chan = await this.discord.getChannel(channel, msg.guild); - const settings = await this.database.logging.get(msg.guild.id); - - if (chan === null) return msg.reply(`Channel "${channel}" doesn't exist.`); - if (chan.type !== 0) return msg.reply(`Channel #${chan.name} was not a text channel`); - - const perms = chan.permissionsOf(this.discord.client.user.id); - if (!perms.has('sendMessages') || !perms.has('readMessages') || !perms.has('embedLinks')) - return msg.reply( - `I am missing the following permissions: **Send Messages**, **Read Messages**, and **Embed Links** in #${chan.name}.` - ); - - let updateEnabled = false; - // Enable on channel input (so it runs properly) - if (!settings.enabled) { - await this.database.logging.update(msg.guild.id, { enabled: true }); - updateEnabled = true; - } - - await this.database.logging.update(msg.guild.id, { - channelID: chan.id, - }); - - return msg.reply( - `Logs will be shown in #${chan.name}!${ - updateEnabled ? "\n:eyes: I saw it wasn't enabled. So, I enabled it myself." : '' - }` - ); - } - - @Subcommand() - async list(msg: CommandMessage) { - const settings = await this.database.logging.get(msg.guild.id); - - const embed = EmbedBuilder.create().setDescription([ - `• **Channels Ignored**: ${settings.ignoreChannels.length}`, - `• **Users Ignored**: ${settings.ignoreUsers.length}`, - `• **Channel**: ${settings.channelID !== null ? `<#${settings!.channelID}>` : 'None'}`, - `• **Enabled**: ${settings.enabled ? msg.successEmote : msg.errorEmote}`, - `• **Events**: ${settings.events.map((ev) => humanizedEvents[ev]).join(', ') || 'None'}`, - ]); - - return msg.reply(embed); - } - - @Subcommand() - async reset(msg: CommandMessage) { - const settings = await this.database.logging.get(msg.guild.id); - if (!settings.channelID) return msg.reply('No mod logs channel has been set.'); - - await this.database.logging.update(msg.guild.id, { - channelID: undefined, - }); - - return msg.reply('Resetted the mod log successfully.'); - } - - @Subcommand('', ['events']) - async event(msg: CommandMessage, [event]: string) { - const settings = await this.database.logging.get(msg.guild.id); - - if (!event) - return msg.reply( - `No event was listed, here is the list:\n\n\`\`\`apache\n${LOGGING_EVENTS.map( - (event) => `${event} | ${msg.settings.prefixes[0]}logging event ${event}` - ).join('\n')}\`\`\`` - ); - - if (event === '*') { - const events = Object.values(LoggingEvents); - settings.events = events; - await this.database.logging['repository'].save(settings); - - return msg.reply( - `:thumbsup: **Enabled** all logging events, to disable all of them, do \`${msg.settings.prefixes[0]}logging events -*\`.` - ); - } - - if (event === '-*') { - settings.events = []; - await this.database.logging['repository'].save(settings); - - return msg.reply(':thumbsup: **Disabled** all logging events.'); - } - - if (!LOGGING_EVENTS.includes(event)) - return msg.reply( - `Invalid event **${event}**, here is the list:\n\n\`\`\`apache\n${LOGGING_EVENTS.map( - (event) => `${event} | ${msg.settings.prefixes[0]}logging event ${event}` - ).join('\n')}\`\`\`` - ); - - const keyedEvent = Object.values(LoggingEvents).find( - (val) => val === event.replace('voice.', 'voice_channel_').replace('leave', 'left').replace('.', '_') - )!; - const disabled = settings.events.includes(keyedEvent); - - settings.events = !disabled ? [...settings.events, keyedEvent] : settings.events.filter((r) => r !== keyedEvent); - await this.database.logging['repository'].save(settings); - - return msg.reply(`:thumbsup: **${!disabled ? 'Enabled' : 'Disabled'}** logging event \`${keyedEvent}\`.`); - } - - @Subcommand('') - async ignore(msg: CommandMessage, [chanOrUserId]: [string]) { - if (!chanOrUserId) return msg.reply('Missing a channel/user ID or mention'); - - const settings = await this.database.logging.get(msg.guild.id); - const channel = await this.discord.getChannel(chanOrUserId); - let user: User | null = null; - - try { - user = await this.discord.getUser(chanOrUserId); - } catch { - // ignore - } - - if (channel !== null) { - if (![0, 5].includes(channel.type)) - return msg.reply(`Channel with ID ${channel.id} was not a Text or News channel`); - - const enabled = !settings.ignoreChannels.includes(channel.id); - settings.ignoreChannels = !settings.ignoreChannels.includes(channel.id) - ? [...settings.ignoreChannels, channel.id] - : settings.ignoreChannels.filter((chanID) => chanID !== channel.id); - await this.database.logging['repository'].save(settings); - - return msg.reply( - `:thumbsup: ${enabled ? 'Added' : 'Deleted'} entry for channel **#${channel.name}** to be excluded in logging.` - ); - } - - if (user !== null) { - const enabled = !settings.ignoreUsers.includes(user.id); - settings.ignoreUsers = !settings.ignoreUsers.includes(user.id) - ? [...settings.ignoreUsers, user.id] - : settings.ignoreUsers.filter((userID) => userID !== user!.id); - await this.database.logging['repository'].save(settings); - - return msg.reply( - `:thumbsup: ${enabled ? 'Added' : 'Deleted'} entry for user **${user.username}#${ - user.discriminator - }** to be excluded in logging.` - ); - } - - return msg.reply(`Channel or user \`${chanOrUserId}\` was not found.`); - } -} diff --git a/src/commands/settings/ModLog.ts b/src/commands/settings/ModLog.ts deleted file mode 100644 index dad4dcc9..00000000 --- a/src/commands/settings/ModLog.ts +++ /dev/null @@ -1,86 +0,0 @@ -/** - * Copyright (c) 2019-2021 Nino - * - * 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. - */ - -import { Command, CommandMessage, Subcommand } from '../../structures'; -import type { TextChannel } from 'eris'; -import { Categories } from '../../util/Constants'; -import { Inject } from '@augu/lilith'; -import Database from '../../components/Database'; -import Discord from '../../components/Discord'; - -export default class ModLogCommand extends Command { - @Inject - private database!: Database; - - @Inject - private discord!: Discord; - - constructor() { - super({ - userPermissions: ['manageGuild'], - description: 'descriptions.modlog', - category: Categories.Settings, - examples: ['modlog', 'modlog reset', 'modlog <#236987412589632587>'], - aliases: ['set-modlog'], - usage: '[channel]', - name: 'modlog', - }); - } - - async run(msg: CommandMessage, [channel]: [string]) { - if (!channel) { - const chan = - msg.settings.modlogChannelID !== null - ? await this.discord.getChannel(msg.settings.modlogChannelID!, msg.guild) - : null; - return msg.reply(chan === null ? 'No mod-log has been set.' : `Mod Logs are set in **#${chan.name}**`); - } - - const chan = await this.discord.getChannel(channel, msg.guild); - if (chan === null) return msg.reply(`Channel "${channel}" doesn't exist.`); - - if (chan.type !== 0) return msg.reply(`Channel #${chan.name} was not a text channel`); - - const perms = chan.permissionsOf(this.discord.client.user.id); - if (!perms.has('sendMessages') || !perms.has('readMessages') || !perms.has('embedLinks')) - return msg.reply( - `I am missing the following permissions: **Send Messages**, **Read Messages**, and **Embed Links** in #${chan.name}.` - ); - - await this.database.guilds.update(msg.guild.id, { - modlogChannelID: chan.id, - }); - - return msg.reply(`Mod Logs are now set in #${chan.name}!`); - } - - @Subcommand() - async reset(msg: CommandMessage) { - if (!msg.settings.modlogChannelID) return msg.reply('No mod logs channel has been set.'); - - await this.database.guilds.update(msg.guild.id, { - modlogChannelID: undefined, - }); - - return msg.reply('Resetted the mod log successfully.'); - } -} diff --git a/src/commands/settings/MutedRole.ts b/src/commands/settings/MutedRole.ts deleted file mode 100644 index f892ce23..00000000 --- a/src/commands/settings/MutedRole.ts +++ /dev/null @@ -1,69 +0,0 @@ -/** - * Copyright (c) 2019-2021 Nino - * - * 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. - */ - -import { Command, Subcommand, CommandMessage } from '../../structures'; -import { Categories } from '../../util/Constants'; -import { Inject } from '@augu/lilith'; -import Database from '../../components/Database'; -import Discord from '../../components/Discord'; - -export default class MutedRoleCommand extends Command { - @Inject - private database!: Database; - - @Inject - private discord!: Discord; - - constructor() { - super({ - userPermissions: 'manageGuild', - description: 'descriptions.muted_role', - category: Categories.Settings, - examples: [ - 'muterole | View the current Muted role in this server', - 'muterole reset | Resets the Muted role in this server', - 'muterole 3621587485965325 | Sets the current mute role', - ], - aliases: ['mutedrole', 'mute-role'], - name: 'muterole', - }); - } - - async run(msg: CommandMessage, [roleID]: [string]) { - if (!roleID) - return msg.settings.mutedRoleID !== null - ? msg.reply(`The muted role in this guild is <@&${msg.settings.mutedRoleID}>`) - : msg.reply('No muted role is set in this guild.'); - - const role = await this.discord.getRole(roleID, msg.guild); - if (role === null) return msg.reply(`\`${roleID}\` was not a role.`); - - await this.database.guilds.update(msg.guild.id, { mutedRoleID: role.id }); - return msg.reply(`The Muted role is now set to **${role.name}**`); - } - - @Subcommand() - async reset(msg: CommandMessage) { - await this.database.guilds.update(msg.guild.id, { mutedRoleID: undefined }); - return msg.reply(':thumbsup: Muted role has been reset.'); - } -} diff --git a/src/commands/settings/Prefix.ts b/src/commands/settings/Prefix.ts deleted file mode 100644 index 3e2a15bf..00000000 --- a/src/commands/settings/Prefix.ts +++ /dev/null @@ -1,155 +0,0 @@ -/** - * Copyright (c) 2019-2021 Nino - * - * 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. - */ - -import { Command, Subcommand, CommandMessage, EmbedBuilder } from '../../structures'; -import { Categories } from '../../util/Constants'; -import { Inject } from '@augu/lilith'; -import Database from '../../components/Database'; -import Config from '../../components/Config'; - -interface Flags { - user?: string | true; - u?: string | true; -} - -export default class PrefixCommand extends Command { - @Inject - private database!: Database; - - @Inject - private config!: Config; - - constructor() { - super({ - description: 'descriptions.prefix', - category: Categories.Settings, - examples: [ - 'prefix', - 'prefix set !', - 'prefix set ! --user', - 'prefix remove !', - 'prefix remove ! --user', - 'prefix remove 1', - 'prefix remove 1 --user', - ], - aliases: ['prefixes'], - usage: '[prefix] [--user]', - name: 'prefix', - }); - } - - async run(msg: CommandMessage) { - const flags = msg.flags(); - const entity = flags.user === true || flags.u === true ? msg.userSettings : msg.settings; - const defaultPrefixes = this.config.getProperty('prefixes') ?? []; - - const embed = EmbedBuilder.create().setDescription([ - `> **List of ${flags.user === true || flags.u === true ? 'user' : 'guild'} prefixes available**:`, - '', - '```apache', - entity.prefixes.map((prefix, index) => `- ${index}. : ${prefix}`).join('\n') || - `None (use ${msg.settings.prefixes[0]}prefix set -u to set one!)`, - '```', - ]); - - if (defaultPrefixes.length > 0) - embed.setFooter(`Prefixes ${defaultPrefixes.join(', ')} will always work no matter what.`); - - return msg.reply(embed); - } - - @Subcommand(' [--user | -u]') - async set(msg: CommandMessage, [...prefix]: [...string[]]) { - if (!prefix) - return msg.reply('Missing a prefix to set! You can use `"` to make spaced ones, example: `"nino "` -> `nino `.'); - - const pre = prefix.join(' ').replaceAll(/['"]/g, ''); - if (pre.length > 25) - return msg.reply( - `Prefix \`${pre}\` is over the limit of 25 characters, you went over ${ - pre.length - 25 - } characters (excluding \`"\`).` - ); - - const flags = msg.flags(); - const isUser = flags.user === true || flags.u === true; - const controller = isUser ? this.database.users : this.database.guilds; - const data = await controller.get(isUser ? msg.author.id : msg.guild.id); - const owners = this.config.getProperty('owners') ?? []; - - if (!isUser && (!msg.member.permissions.has('manageGuild') || !owners.includes(msg.author.id))) - return msg.reply('Missing the **Manage Guild** permission.'); - - if (data.prefixes.length > 5) - return msg.reply(`${isUser ? 'You' : 'The guild'} has exceeded the amount of prefixes.`); - - const index = data.prefixes.findIndex((prefix) => prefix.toLowerCase() === pre.toLowerCase()); - if (index !== -1) - return msg.reply(`Prefix \`${pre}\` already exists as a ${isUser ? 'your' : "the guild's"} prefix.`); - - data.prefixes.push(pre); - - // @ts-ignore Wow, our first ts-ignore in this project! (ts2349) - await controller.repository.save(data); - return msg.reply(`Prefix \`${pre}\` is now available`); - } - - @Subcommand(' [--user | -u]') - async reset(msg: CommandMessage, [...prefix]: [...string[]]) { - if (!prefix) - return msg.reply('Missing a prefix to set! You can use `"` to make spaced ones, example: `"nino "` -> `nino `.'); - - const pre = prefix.join(' ').replaceAll(/['"]/g, ''); - if (pre.length > 25) - return msg.reply( - `Prefix \`${pre}\` is over the limit of 25 characters, you went over ${ - pre.length - 25 - } characters (excluding \`"\`).` - ); - - const flags = msg.flags(); - const isUser = flags.user === true || flags.u === true; - const controller = isUser ? this.database.users : this.database.guilds; - const data = await controller.get(isUser ? msg.author.id : msg.guild.id); - const owners = this.config.getProperty('owners') ?? []; - - const canManageGuild = - msg.guild.ownerID === msg.author.id - ? true - : msg.member.permissions.has('manageGuild') || owners.includes(msg.author.id); - - if (!isUser && !canManageGuild) return msg.reply('Missing the **Manage Guild** permission.'); - - const index = data.prefixes.findIndex((prefix) => prefix.toLowerCase() === pre.toLowerCase()); - if (index === -1) return msg.reply('Prefix was not found'); - - data.prefixes.splice(index, 1); - - // @ts-ignore Check out the issue ID -> (ts2349) - await controller.repository.save(data); - return msg.reply( - `Prefix with index **${index}** (\`${prefix}\`) has been removed, ${isUser ? 'you' : 'the guild'} have ${ - data.prefixes.length - } prefix${data.prefixes.length === 1 ? 'es' : ''} left.` - ); - } -} diff --git a/src/commands/settings/Punishments.ts b/src/commands/settings/Punishments.ts deleted file mode 100644 index c07a99d5..00000000 --- a/src/commands/settings/Punishments.ts +++ /dev/null @@ -1,148 +0,0 @@ -/** - * Copyright (c) 2019-2021 Nino - * - * 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. - */ - -import { Command, CommandMessage, EmbedBuilder, Subcommand } from '../../structures'; -import { PunishmentType } from '../../entities/PunishmentsEntity'; -import { Categories } from '../../util/Constants'; -import { firstUpper } from '@augu/utils'; -import { Inject } from '@augu/lilith'; -import Database from '../../components/Database'; -import ms = require('ms'); - -// Punishments shouldn't be chained with warnings and voice-related shit -const TYPES = Object.values(PunishmentType).filter((w) => !w.startsWith('warning.') && !w.startsWith('voice.')); - -interface Flags { - soft?: string | true; - days?: string | true; - d?: string | true; - s?: string | true; -} - -export default class PunishmentsCommand extends Command { - @Inject - private database!: Database; - - constructor() { - super({ - userPermissions: 'manageGuild', - description: 'descriptions.punishments', - category: Categories.Settings, - examples: [ - 'punishments | List all the punishments in this guild', - 'punishments 3 mute | Add a punishment of 3 warnings to be a mute', - 'punishments 5 ban / 1d | Adds a punishment for a ban for a day', - 'punishments remove 3 | Remove a punishment by the index', - ], - aliases: ['punish'], - name: 'punishments', - }); - } - - async run(msg: CommandMessage, [index, type, time]: [string, string, string?]) { - const punishments = await this.database.punishments.getAll(msg.guild.id); - - if (!index) { - if (!punishments.length) return msg.reply('There are no punishments setup in this guild.'); - - const embed = EmbedBuilder.create() - .setTitle(`:pencil2: ~ Punishments for ${msg.guild.name}`) - .addFields( - punishments.map((punishment) => ({ - name: `❯ Punishment #${punishment.index}`, - value: [ - `• **Warnings**: ${punishment.warnings}`, - `• **Soft**: ${punishment.soft ? 'Yes' : 'No'}`, - `• **Time**: ${punishment.time !== null ? ms(punishment.time!) : 'No time duration'}`, - `• **Type**: ${firstUpper(punishment.type)}`, - ].join('\n'), - inline: true, - })) - ); - - return msg.reply(embed); - } - - if (punishments.length > 10) return msg.reply("Yea, I think you're fine with 10 punishments..."); - - if (isNaN(Number(index))) return msg.reply('The amount of warnings you specified was not a number'); - - if (Number(index) === 0) - return msg.reply("You need to specify an amount of warnings, `0` isn't gonna cut it you know."); - - if (Number(index) > 10) return msg.reply('Uh-oh! The guild has reached the maximum amount of 10 warnings, sorry.'); - - if (type === undefined || !TYPES.includes(type as any)) - return msg.reply( - `You haven't specified a punishment type or the one you provided is not a valid one.\n\n\`\`\`apache\n${TYPES.map( - (type) => `- ${type}` - ).join('\n')}\`\`\`` - ); - - const flags = msg.flags(); - const soft = flags.soft === true || flags.s === true; - const days = flags.days !== undefined ? flags.days : flags.d !== undefined ? flags.d : undefined; - let timeStamp: number | undefined = undefined; - - try { - if (time !== undefined) timeStamp = ms(time); - } catch { - // uwu - } - - if (type !== PunishmentType.Ban && soft === true) - return msg.reply(`The \`--soft\` argument only works on bans only, you specified \`${type}\`.`); - - const entry = await this.database.punishments.create({ - warnings: Number(index), - guildID: msg.guild.id, - time: timeStamp, - soft, - days: days ? Number(days) : undefined, - type: type as PunishmentType, - }); - - return msg.reply(`Punishment #**${entry.index}** has been created`); - } - - @Subcommand('') - async remove(msg: CommandMessage, [index]: [string]) { - if (!index) - return msg.reply( - `Missing an amount of warnings to be removed. Run \`${msg.settings.prefixes[0]}punishments\` to see which one you want removed.` - ); - - if (isNaN(Number(index))) return msg.reply(`\`${index}\` was not a number`); - - const punishment = await this.database.punishments.get(msg.guild.id, Number(index)); - if (punishment === undefined) - return msg.reply( - `Punishment #**${index}** warnings was not found. Run \`${msg.settings.prefixes[0]}punishments\` to see which one you want removed.` - ); - - await this.database.punishments['repository'].delete({ - guildID: msg.guild.id, - index: Number(index), - }); - return msg.reply(`:thumbsup: Punishment #**${punishment.index}** has been removed.`); - } -} diff --git a/src/commands/settings/Reset.ts b/src/commands/settings/Reset.ts deleted file mode 100644 index 52214e51..00000000 --- a/src/commands/settings/Reset.ts +++ /dev/null @@ -1,91 +0,0 @@ -/** - * Copyright (c) 2019-2021 Nino - * - * 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. - */ - -import { Command, CommandMessage } from '../../structures'; -import { Categories } from '../../util/Constants'; -import { Stopwatch } from '@augu/utils'; -import { Inject } from '@augu/lilith'; -import Database from '../../components/Database'; - -export default class ResetCommand extends Command { - @Inject - private readonly database!: Database; - - constructor() { - super({ - userPermissions: 'manageGuild', - description: 'descriptions.reset', - category: Categories.Settings, - name: 'reset', - }); - } - - async run(msg: CommandMessage) { - const response = await msg.awaitReply( - [ - 'Do you wish to reset all guild settings? __Y__es or __No__?', - 'If you wish to reset them, you will not be able to replace them back.', - ].join('\n'), - 60, - (m) => m.author.id === msg.author.id && ['y', 'yes', 'n', 'no'].includes(m.content) - ); - - if (['yes', 'y'].includes(response.content)) { - const message = await msg.reply('Deleting guild settings...'); - const stopwatch = new Stopwatch(); - stopwatch.start(); - - await Promise.all([ - this.database.guilds.update(msg.guild.id, { - mutedRoleID: undefined, - modlogChannelID: undefined, - }), - - this.database.logging.update(msg.guild.id, { - enabled: false, - events: [], - channelID: undefined, - ignoreChannels: [], - ignoreUsers: [], - }), - - this.database.automod.update(msg.guild.id, { - blacklistWords: [], - shortLinks: false, - blacklist: false, - mentions: false, - invites: false, - dehoist: false, - spam: false, - raid: false, - }), - ]); - - const endTime = stopwatch.end(); - await message.delete(); - - return msg.success(`Resetted guild settings in ~**${endTime}**`); - } - - return msg.reply('Will not reset guild settings on command.'); - } -} diff --git a/src/commands/settings/Settings.ts b/src/commands/settings/Settings.ts deleted file mode 100644 index 354d83b5..00000000 --- a/src/commands/settings/Settings.ts +++ /dev/null @@ -1,106 +0,0 @@ -/** - * Copyright (c) 2019-2021 Nino - * - * 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. - */ - -import { Command, CommandMessage, EmbedBuilder, Subcommand } from '../../structures'; -import { LoggingEvents } from '../../entities/LoggingEntity'; -import { Categories } from '../../util/Constants'; -import { Inject } from '@augu/lilith'; -import Database from '../../components/Database'; - -const humanizedEvents = { - [LoggingEvents.VoiceChannelSwitch]: 'Voice Channel Switch', - [LoggingEvents.VoiceChannelLeft]: 'Voice Channel Leave', - [LoggingEvents.VoiceChannelJoin]: 'Voice Channel Join', - [LoggingEvents.MessageUpdated]: 'Message Updated', - [LoggingEvents.MessageDeleted]: 'Message Deleted', -}; - -export default class SettingsCommand extends Command { - @Inject - private readonly database!: Database; - - constructor() { - super({ - userPermissions: ['manageGuild'], - description: 'descriptions.settings', - category: Categories.Settings, - aliases: ['config', 'conf'], - name: 'settings', - }); - } - - async run(msg: CommandMessage) { - // Bulk get all guild settings - const [settings, automod, logging] = await Promise.all([ - this.database.guilds.get(msg.guild.id), - this.database.automod.get(msg.guild.id), - this.database.logging.get(msg.guild.id), - ]); - - const embed = EmbedBuilder.create() - .setTitle(`[ :pencil2: Settings for ${msg.guild.name} (${msg.guild.id}) ]`) - .addFields([ - { - name: '❯ Settings', - value: [ - `• **Muted Role**: ${ - settings.mutedRoleID !== null ? `<@&${settings.mutedRoleID}> (**${settings.mutedRoleID}**)` : 'None' - }`, - `• **Mod Log**: ${ - settings.modlogChannelID !== null - ? `<#${settings.modlogChannelID}> (**${settings.modlogChannelID}**)` - : 'None' - }`, - ].join('\n'), - inline: true, - }, - { - name: '❯ Automod', - value: [ - `• ${automod!.shortLinks ? msg.successEmote : msg.errorEmote} **Short Links**`, - `• ${automod!.blacklist ? msg.successEmote : msg.errorEmote} **Blacklist Words**`, - `• ${automod!.mentions ? msg.successEmote : msg.errorEmote} **Mentions**`, - `• ${automod!.dehoist ? msg.successEmote : msg.errorEmote} **Dehoisting**`, - `• ${automod!.invites ? msg.successEmote : msg.errorEmote} **Invites**`, - `• ${automod!.raid ? msg.successEmote : msg.errorEmote} **Raid**`, - `• ${automod!.spam ? msg.successEmote : msg.errorEmote} **Spam**`, - ].join('\n'), - inline: true, - }, - { - name: '❯ Logging', - value: [ - `• **Channels Ignored**: ${logging.ignoreChannels.length}`, - `• **Users Ignored**: ${logging.ignoreUsers.length}`, - `• **Channel**: ${ - logging.channelID !== null ? `<#${logging.channelID}> (**${logging.channelID}**)` : 'None' - }`, - `• **Enabled**: ${logging.enabled ? msg.successEmote : msg.errorEmote}`, - `• **Events**: ${logging.events.map((ev) => humanizedEvents[ev]).join(', ') || 'None'}`, - ].join('\n'), - inline: true, - }, - ]); - - return msg.reply(embed); - } -} diff --git a/src/components/Config.ts b/src/components/Config.ts deleted file mode 100644 index 2595af7d..00000000 --- a/src/components/Config.ts +++ /dev/null @@ -1,176 +0,0 @@ -/** - * Copyright (c) 2019-2021 Nino - * - * 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. - */ - -/* eslint-disable camelcase */ - -import { writeFileSync, existsSync } from 'fs'; -import { Component, Inject } from '@augu/lilith'; -import { readFile } from 'fs/promises'; -import { Logger } from 'tslog'; -import { join } from 'path'; -import yaml from 'js-yaml'; - -const NOT_FOUND_SYMBOL = Symbol.for('$nino::config::not.found'); - -interface Configuration { - runPendingMigrations?: boolean; - prometheusPort?: number; - defaultLocale?: string; - environment: 'development' | 'production'; - sentryDsn?: string; - botlists?: BotlistConfig; - timeouts: TimeoutsConfig; - database: DatabaseConfig; - prefixes: string[]; - status: StatusConfig; - owners: string[]; - ksoft?: string; - redis: RedisConfig; - token: string; - api?: boolean; -} - -interface BotlistConfig { - dservices?: string; - dboats?: string; - topgg?: string; - delly?: string; - dbots?: string; - bfd?: string; -} - -interface DatabaseConfig { - username: string; - password: string; - database: string; - host: string; - port: number; - url?: string; -} - -interface RedisConfig { - sentinels?: RedisSentinelConfig[]; - password?: string; - master?: string; - index?: number; - host: string; - port: number; -} - -interface TimeoutsConfig { - host?: string; - auth: string; - port: number; -} - -interface StatusConfig { - presence?: 'online' | 'idle' | 'dnd' | 'offline'; - status: string; - type: 0 | 1 | 2 | 3 | 5; -} - -// eslint-disable-next-line -interface RedisSentinelConfig extends Pick {} - -@Component({ - priority: 0, - name: 'config', -}) -export default class Config { - private config!: Configuration; - - @Inject - private readonly logger!: Logger; - - async load() { - if (!existsSync(join(__dirname, '..', '..', 'config.yml'))) { - const config = yaml.dump( - { - runPendingMigrations: true, - defaultLocale: 'en_US', - environment: 'production', - prefixes: ['!'], - owners: [], - token: '-- replace me --', - }, - { - indent: 2, - noArrayIndent: false, - } - ); - - writeFileSync(join(__dirname, '..', '..', 'config.yml'), config); - return Promise.reject( - new SyntaxError( - "Weird, you didn't have a configuration file... So, I may have provided you a default one, if you don't mind... >W<" - ) - ); - } - - this.logger.info('Loading configuration...'); - const contents = await readFile(join(__dirname, '..', '..', 'config.yml'), 'utf8'); - const config = yaml.load(contents) as unknown as Configuration; - - this.config = { - runPendingMigrations: config.runPendingMigrations ?? false, - prometheusPort: config.prometheusPort, - defaultLocale: config.defaultLocale ?? 'en_US', - environment: config.environment ?? 'production', - sentryDsn: config.sentryDsn, - botlists: config.botlists, - database: config.database, - timeouts: config.timeouts, - prefixes: config.prefixes, - owners: config.owners, - status: config.status ?? { - type: 0, - status: '$prefix$help | $guilds$ Guilds', - presence: 'online', - }, - ksoft: config.ksoft, - redis: config.redis, - token: config.token, - api: config.api, - }; - - if (this.config.token === '-- replace me --') - return Promise.reject(new TypeError('Restore `token` in config with your discord bot token.')); - - // resolve the promise - return Promise.resolve(); - } - - getProperty>(key: K): KeyToPropType | undefined { - const nodes = key.split('.'); - let value: any = this.config; - - for (const frag of nodes) { - try { - value = value[frag]; - } catch { - value = NOT_FOUND_SYMBOL; - } - } - - return value === NOT_FOUND_SYMBOL ? undefined : value; - } -} diff --git a/src/components/Database.ts b/src/components/Database.ts deleted file mode 100644 index 1b005015..00000000 --- a/src/components/Database.ts +++ /dev/null @@ -1,208 +0,0 @@ -/** - * Copyright (c) 2019-2021 Nino - * - * 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. - */ - -import { createConnection, Connection, ConnectionOptions } from 'typeorm'; -import { Component, ComponentAPI, Inject } from '@augu/lilith'; -import { humanize, Stopwatch } from '@augu/utils'; -import { Logger } from 'tslog'; -import Config from './Config'; - -// Controllers -import GuildSettingsController from '../controllers/GuildSettingsController'; -import UserSettingsController from '../controllers/UserSettingsController'; -import PunishmentsController from '../controllers/PunishmentsController'; -import BlacklistController from '../controllers/BlacklistController'; -import WarningsController from '../controllers/WarningsController'; -import LoggingController from '../controllers/LoggingController'; -import AutomodController from '../controllers/AutomodController'; -import CasesController from '../controllers/CasesController'; - -// Import entities -import PunishmentEntity from '../entities/PunishmentsEntity'; -import BlacklistEntity from '../entities/BlacklistEntity'; -import LoggingEntity from '../entities/LoggingEntity'; -import WarningEntity from '../entities/WarningsEntity'; -import AutomodEntity from '../entities/AutomodEntity'; -import GuildEntity from '../entities/GuildEntity'; -import CaseEntity from '../entities/CaseEntity'; -import UserEntity from '../entities/UserEntity'; -import { collapseTextChangeRangesAcrossMultipleVersions } from 'typescript'; - -@Component({ - priority: 1, - name: 'database', -}) -export default class Database { - public punishments!: PunishmentsController; - public blacklists!: BlacklistController; - public connection!: Connection; - public warnings!: WarningsController; - public logging!: LoggingController; - public automod!: AutomodController; - public guilds!: GuildSettingsController; - public cases!: CasesController; - public users!: UserSettingsController; - - @Inject - private readonly logger!: Logger; - - @Inject - private readonly config!: Config; - private api!: ComponentAPI; - - async load() { - this.logger.info('Now connecting to the database...'); - - const url = this.config.getProperty('database.url'); - const entities = [ - // readability mmmmm - PunishmentEntity, - BlacklistEntity, - LoggingEntity, - WarningEntity, - AutomodEntity, - GuildEntity, - CaseEntity, - UserEntity, - ]; - - const config: ConnectionOptions = - url !== undefined - ? { - migrations: ['./migrations/*.ts'], - entities, - type: 'postgres', - name: 'Nino', - url, - } - : { - migrations: ['./migrations/*.ts'], - username: this.config.getProperty('database.username'), - password: this.config.getProperty('database.password'), - database: this.config.getProperty('database.database'), - entities, - host: this.config.getProperty('database.host'), - port: this.config.getProperty('database.port'), - type: 'postgres', - name: 'Nino', - }; - - this.connection = await createConnection(config); - this.initRepos(); - - const migrations = await this.connection.showMigrations(); - const shouldRun = this.config.getProperty('runPendingMigrations'); - if (migrations && (shouldRun === undefined || shouldRun === false)) { - this.logger.info( - 'There are pending migrations to be ran, but you have `runPendingMigrations` disabled! Run `npm run migrations` to migrate the database or set `runPendingMigrations` = true to run them at runtime.' - ); - } else if (migrations && shouldRun === true) { - this.logger.info('Found pending migrations and `runPendingMigrations` is enabled, now running...'); - - try { - const ran = await this.connection.runMigrations({ transaction: 'all' }); - this.logger.info(`Ran ${ran.length} migrations! You're all to go.`); - } catch (ex) { - if ((ex as Error).message.indexOf('already exists') !== -1) { - this.logger.warn('Seems like relations or indexes existed!'); - return Promise.resolve(); - } - - try { - this.logger.error('Rolling back changes...', ex); - await this.connection.undoLastMigration({ transaction: 'all' }); - } catch (ex2) { - return Promise.reject(ex2); - } - } - } else { - this.logger.info('No migrations needs to be ran and the connection to the database is healthy.'); - return Promise.resolve(); - } - - this.logger.info('All migrations has been migrated and the connection has been established correctly!'); - return Promise.resolve(); - } - - dispose() { - return this.connection.close(); - } - - async getStatistics() { - const stopwatch = new Stopwatch(); - stopwatch.start(); - await this.connection.query('SELECT * FROM guilds'); - const ping = stopwatch.end(); - - let dbName: string = 'nino'; - const url = this.config.getProperty('database.url'); - if (url !== undefined) { - const parts = url.split('/'); - dbName = parts[parts.length - 1]; - } else { - dbName = this.config.getProperty('database.database') ?? 'nino'; - } - - // collect shit - const data = await Promise.all([ - this.connection.query( - `SELECT tup_returned, tup_fetched, tup_inserted, tup_updated, tup_deleted FROM pg_stat_database WHERE datname = '${dbName}';` - ), - this.connection.query('SELECT version();'), - this.connection.query('SELECT extract(epoch FROM current_timestamp - pg_postmaster_start_time()) AS uptime;'), - ]); - - return { - inserted: Number(data[0]?.[0]?.tup_inserted ?? 0), - updated: Number(data[0]?.[0]?.tup_updated ?? 0), - deleted: Number(data[0]?.[0]?.tup_deleted ?? 0), - fetched: Number(data[0]?.[0]?.tup_fetched ?? 0), - version: data[1][0].version.split(', ').shift().replace('PostgreSQL ', '').trim(), - uptime: humanize(Math.floor(data[2][0].uptime * 1000), true), - ping, - }; - } - - private initRepos() { - this.punishments = new PunishmentsController(this); - this.blacklists = new BlacklistController(this); - this.warnings = new WarningsController(this); - this.logging = new LoggingController(this); - this.automod = new AutomodController(this); - this.guilds = new GuildSettingsController(this); - this.cases = new CasesController(this); - this.users = new UserSettingsController(this); - - for (const controller of [ - this.punishments, - this.blacklists, - this.warnings, - this.logging, - this.automod, - this.guilds, - this.cases, - this.users, - ]) { - this.api.container.addInjections(controller); - } - } -} diff --git a/src/components/Discord.ts b/src/components/Discord.ts deleted file mode 100644 index 31819a34..00000000 --- a/src/components/Discord.ts +++ /dev/null @@ -1,189 +0,0 @@ -/** - * Copyright (c) 2019-2021 Nino - * - * 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. - */ - -import { USER_MENTION_REGEX, USERNAME_DISCRIM_REGEX, ID_REGEX, CHANNEL_REGEX, ROLE_REGEX } from '../util/Constants'; - -import { Component, Inject, ComponentAPI, Subscribe } from '@augu/lilith'; -import { Client, Role, Guild, AnyChannel } from 'eris'; -import type { SlashCreator } from 'slash-create'; -import { Logger } from 'tslog'; -import Config from './Config'; - -@Component({ - priority: 0, - name: 'discord', -}) -export default class Discord { - public slashCreator!: SlashCreator; - public mentionRegex?: RegExp; - public client!: Client; - - api!: ComponentAPI; - - @Inject - private readonly config!: Config; - - @Inject - private readonly logger!: Logger; - - load() { - if (this.client !== undefined) { - this.logger.warn('A client has already been created.'); - return; - } - - const token = this.config.getProperty('token'); - if (token === undefined) { - this.logger.fatal("Property `token` doesn't exist in the config file, please populate it."); - return; - } - - this.logger.info('Booting up the bot...'); - this.client = new Client(token, { - getAllUsers: true, - maxShards: 'auto', - restMode: true, - intents: ['guilds', 'guildBans', 'guildMembers', 'guildMessages', 'guildVoiceStates'], - }); - - this.api.container.addEmitter('discord', this.client); - return this.client.connect(); - } - - dispose() { - return this.client.disconnect({ reconnect: false }); - } - - get emojis() { - return this.client.guilds - .map((guild) => guild.emojis.map((emoji) => `<${emoji.animated ? 'a:' : ':'}${emoji.name}:${emoji.id}>`)) - .flat(); - } - - async getUser(query: string) { - if (USER_MENTION_REGEX.test(query)) { - const match = USER_MENTION_REGEX.exec(query); - if (match === null) return null; - - const user = this.client.users.get(match[1]); - if (user !== undefined) { - return user; - } else { - return this.client.getRESTUser(match[1]).catch(() => null); - } - } - - if (USERNAME_DISCRIM_REGEX.test(query)) { - const match = query.match(USERNAME_DISCRIM_REGEX)!; - const users = this.client.users.filter( - (user) => user.username === match[1] && Number(user.discriminator) === Number(match[2]) - ); - - // TODO: pagination? - if (users.length > 0) return users[0]; - } - - if (ID_REGEX.test(query)) { - const user = this.client.users.get(query); - if (user !== undefined) return user; - else return this.client.getRESTUser(query); - } - - return null; - } - - getChannel(query: string, guild?: Guild) { - return new Promise((resolve) => { - if (CHANNEL_REGEX.test(query)) { - const match = CHANNEL_REGEX.exec(query); - if (match === null) return resolve(null); - - if (guild) { - return resolve(guild.channels.has(match[1]) ? (guild.channels.get(match[1])! as T) : null); - } else { - const channel = - match[1] in this.client.channelGuildMap && - this.client.guilds.get(this.client.channelGuildMap[match[1]])?.channels.get(match[1]); - return resolve((channel as T) || null); - } - } - - if (ID_REGEX.test(query)) { - if (guild) { - return resolve(guild.channels.has(query) ? (guild.channels.get(query)! as T) : null); - } else { - const channel = - query in this.client.channelGuildMap && - this.client.guilds.get(this.client.channelGuildMap[query])?.channels.get(query); - return resolve((channel as T) || null); - } - } - - if (guild !== undefined) { - const channels = guild.channels.filter((chan) => chan.name.toLowerCase().includes(query.toLowerCase())); - if (channels.length > 0) { - return resolve(channels[0] as T); - } - } - - resolve(null); - }); - } - - getRole(query: string, guild: Guild) { - return new Promise((resolve) => { - if (ROLE_REGEX.test(query)) { - const match = ROLE_REGEX.exec(query)!; - if (match === null) return resolve(null); - - const role = guild.roles.get(match[1]); - - if (role !== undefined) return resolve(role); - } - - if (ID_REGEX.test(query)) return resolve(guild.roles.has(query) ? guild.roles.get(query)! : null); - - const roles = guild.roles.filter((role) => role.name.toLowerCase() === query.toLowerCase()); - - // TODO: pagination? - resolve(roles.length > 0 ? roles[0] : null); - }); - } - - @Subscribe('shardReady', { emitter: 'discord' }) - private onShardReady(id: number) { - this.logger.info(`Shard #${id} is now ready.`); - } - - @Subscribe('shardDisconnect', { emitter: 'discord' }) - private onShardDisconnect(error: any, id: number) { - this.logger.fatal( - `Shard #${id} has disconnected from the universe\n`, - error || 'Connection has been reset by peer.' - ); - } - - @Subscribe('shardResume', { emitter: 'discord' }) - private onShardResume(id: number) { - this.logger.info(`Shard #${id} has resumed it's connection!`); - } -} diff --git a/src/components/Prometheus.ts b/src/components/Prometheus.ts deleted file mode 100644 index a054aae0..00000000 --- a/src/components/Prometheus.ts +++ /dev/null @@ -1,98 +0,0 @@ -/** - * Copyright (c) 2019-2021 Nino - * - * 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. - */ - -import { createServer, IncomingMessage, ServerResponse } from 'http'; -import { Component, Inject } from '@augu/lilith'; -import { Logger } from 'tslog'; -import Config from './Config'; -import prom from 'prom-client'; - -@Component({ - priority: 3, - name: 'prometheus', -}) -export default class Prometheus { - public commandsExecuted!: prom.Counter; - public messagesSeen!: prom.Counter; - public rawWSEvents!: prom.Counter; - public guildCount!: prom.Gauge; - #server!: ReturnType; // yes - - @Inject - private logger!: Logger; - - @Inject - private config!: Config; - - load() { - const port = this.config.getProperty('prometheusPort'); - if (port === undefined) { - this.logger.warn( - 'Prometheus will not be available! This is not recommended for private instances unless you want analytics.' - ); - return Promise.resolve(); - } - - prom.collectDefaultMetrics(); - this.commandsExecuted = new prom.Counter({ - labelNames: ['command'], - name: 'nino_commands_executed', - help: 'How many commands Nino has executed successfully', - }); - - this.messagesSeen = new prom.Counter({ - name: 'nino_messages_seen', - help: 'How many messages Nino has seen throughout the process lifespan', - }); - - this.rawWSEvents = new prom.Counter({ - labelNames: ['event'], - name: 'nino_discord_websocket_events', - help: 'Received WebSocket events from Discord and what they were', - }); - - this.guildCount = new prom.Gauge({ - name: 'nino_guild_count', - help: 'Number of guilds Nino is in', - }); - - this.#server = createServer(this.onRequest.bind(this)); - this.#server.once('listening', () => this.logger.info(`Prometheus: Listening at http://localhost:${port}`)); - this.#server.on('error', (error) => this.logger.fatal(error)); - this.#server.listen(port); - } - - private async onRequest(req: IncomingMessage, res: ServerResponse) { - if (req.url! === '/metrics') { - res.writeHead(200, { 'Content-Type': prom.register.contentType }); - res.write(await prom.register.metrics()); - } else if (req.url! === '/favicon.ico') { - res.writeHead(404, { 'Content-Type': 'application/json' }); - res.write('{"fuck":"you uwu"}'); - } else { - res.writeHead(400, { 'Content-Type': 'application/json' }); - res.write('{"uwu":"owo"}'); - } - - res.end(); - } -} diff --git a/src/components/Redis.ts b/src/components/Redis.ts deleted file mode 100644 index 34ebf06e..00000000 --- a/src/components/Redis.ts +++ /dev/null @@ -1,133 +0,0 @@ -/** - * Copyright (c) 2019-2021 Nino - * - * 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. - */ - -import { Component, Inject } from '@augu/lilith'; -import type { Timeout } from './timeouts/types'; -import { Stopwatch } from '@augu/utils'; -import { Logger } from 'tslog'; -import IORedis from 'ioredis'; -import Config from './Config'; - -@Component({ - priority: 4, - name: 'redis', -}) -export default class Redis { - public client!: IORedis.Redis; - - @Inject - private logger!: Logger; - - @Inject - private config!: Config; - - async load() { - this.logger.info('Connecting to Redis...'); - - const sentinels = this.config.getProperty('redis.sentinels'); - const password = this.config.getProperty('redis.password'); - const masterName = this.config.getProperty('redis.master'); - const index = this.config.getProperty('redis.index'); - const host = this.config.getProperty('redis.host'); - const port = this.config.getProperty('redis.port'); - - const config = - (sentinels ?? []).length > 0 - ? { - enableReadyCheck: true, - connectionName: 'Nino', - lazyConnect: true, - sentinels, - password: password, - name: masterName, - db: index, - } - : { - enableReadyCheck: true, - connectionName: 'Nino', - lazyConnect: true, - password: password, - host: host, - port: port, - db: index, - }; - - this.client = new IORedis(config); - - await this.client.client('SETNAME', 'Nino'); - this.client.on('ready', () => this.logger.info('Connected to Redis!')); - - this.client.on('error', this.logger.error); - return this.client.connect().catch(() => {}); // eslint-disable-line - } - - dispose() { - return this.client.disconnect(); - } - - getTimeouts(guild: string) { - return this.client - .hget('nino:timeouts', guild) - .then((value) => (value !== null ? JSON.parse(value) : [])) - .catch(() => [] as Timeout[]); - } - - async getStatistics() { - const stopwatch = new Stopwatch(); - stopwatch.start(); - await this.client.ping('Ice is cute as FUCK'); - - const ping = stopwatch.end(); - - // stole this from donny - // Credit: https://github.com/FurryBotCo/FurryBot/blob/master/src/commands/information/stats-cmd.ts#L22 - const [stats, server] = await Promise.all([ - this.client.info('stats').then( - (info) => - info - .split(/\n\r?/) - .slice(1, -1) - .map((item) => ({ - [item.split(':')[0]]: item.split(':')[1].trim(), - })) - .reduce((a, b) => ({ ...a, ...b })) as unknown as RedisInfo - ), - - this.client.info('server').then( - (info) => - info - .split(/\n\r?/) - .slice(1, -1) - .map((item) => ({ - [item.split(':')[0]]: item.split(':')[1].trim(), - })) - .reduce((a, b) => ({ ...a, ...b })) as unknown as RedisServerInfo - ), - ]); - - return { - server, - stats, - ping, - }; - } -} diff --git a/src/components/Sentry.ts b/src/components/Sentry.ts deleted file mode 100644 index 7ea91638..00000000 --- a/src/components/Sentry.ts +++ /dev/null @@ -1,75 +0,0 @@ -/** - * Copyright (c) 2019-2021 Nino - * - * 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. - */ - -import { version, commitHash } from '../util/Constants'; -import { Component, Inject } from '@augu/lilith'; -import { hostname } from 'os'; -import { Logger } from 'tslog'; -import * as sentry from '@sentry/node'; -import Config from './Config'; - -@Component({ - priority: 5, - name: 'sentry', -}) -export default class Sentry { - @Inject - private logger!: Logger; - - @Inject - private config!: Config; - - load() { - this.logger.info('Initializing Sentry...'); - - const dsn = this.config.getProperty('sentryDsn'); - if (dsn === undefined) { - this.logger.warn("Missing sentryDsn variable in config.yml! Don't worry, this is optional."); - return; - } - - sentry.init({ - tracesSampleRate: 1.0, - integrations: [new sentry.Integrations.Http({ tracing: true })], - environment: this.config.getProperty('environment')!, - serverName: hostname(), - release: version, - dsn, - }); - - sentry.configureScope((scope) => - scope.setTags({ - 'nino.environment': this.config.getProperty('environment')!, - 'nino.commitHash': commitHash, - 'nino.version': version, - 'system.user': require('os').userInfo().username, - 'system.os': process.platform, - }) - ); - - this.logger.info('Sentry has been installed.'); - } - - report(ex: Error) { - sentry.captureException(ex); - } -} diff --git a/src/components/timeouts/Timeouts.ts b/src/components/timeouts/Timeouts.ts deleted file mode 100644 index dd743b98..00000000 --- a/src/components/timeouts/Timeouts.ts +++ /dev/null @@ -1,224 +0,0 @@ -/** - * Copyright (c) 2019-2021 Nino - * - * 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. - */ - -import PunishmentService from '../../services/PunishmentService'; -import { Component, Inject } from '@augu/lilith'; -import * as types from './types'; -import { Logger } from 'tslog'; -import WebSocket from 'ws'; -import Discord from '../../components/Discord'; -import Config from '../../components/Config'; -import Redis from '../../components/Redis'; - -interface ApplyTimeoutOptions { - moderator: string; - reason?: string; - victim: string; - guild: string; - time: number; - type: types.PunishmentTimeoutType; -} - -@Component({ - priority: 6, - name: 'timeouts', -}) -export default class TimeoutsManager { - protected _reconnectTimeout?: NodeJS.Timeout; - protected _connectTimeout?: NodeJS.Timeout; - protected _readyPromise?: { resolve(): void; reject(error: Error): void }; - - private socket!: WebSocket; - public state: types.SocketState = types.SocketState.Unknown; - - @Inject - private readonly punishments!: PunishmentService; - - @Inject - private readonly discord!: Discord; - - @Inject - private readonly logger!: Logger; - - @Inject - private readonly redis!: Redis; - - @Inject - private readonly config!: Config; - - load() { - return new Promise((resolve, reject) => { - this._readyPromise = { resolve, reject }; - this.state = types.SocketState.Connecting; - this.logger.info( - this._reconnectTimeout !== undefined - ? 'Reconnecting to the timeouts service...' - : 'Connecting to the timeouts service!' - ); - - const host = this.config.getProperty('timeouts.host'); - const port = this.config.getProperty('timeouts.port'); - const auth = this.config.getProperty('timeouts.auth'); - - // @ts-ignore yes - if (this.config.getProperty('timeouts') === undefined) - return reject( - 'Missing `timeouts` configuration, refer to the Process section: https://github.com/NinoDiscord/Nino#config-timeouts' - ); - - this.socket = new WebSocket(`ws://${host ?? 'localhost'}:${port}`, { - headers: { - Authorization: auth, - }, - }); - - if (this._reconnectTimeout !== undefined) clearTimeout(this._reconnectTimeout); - - delete this._reconnectTimeout; - this.socket.on('open', this._onOpen.bind(this)); - this.socket.on('error', this._onError.bind(this)); - this.socket.on('close', this._onClose.bind(this)); - this.socket.on('message', this._onMessage.bind(this)); - - this._connectTimeout = setTimeout(() => { - clearTimeout(this._connectTimeout!); - - delete this._connectTimeout; - delete this._readyPromise; - return reject(new Error('Connection to timeouts service took too long.')); - }, 15000); - }); - } - - dispose() { - return this.socket.close(); - } - - send(op: types.OPCodes.Request, data: types.RequestPacket['d']): void; - send(op: types.OPCodes.Acknowledged, data: types.AcknowledgedPacket['d']): void; - send(op: types.OPCodes, d?: any) { - this.socket.send( - JSON.stringify({ - op, - d, - }) - ); - } - - async apply({ moderator, reason, victim, guild, time, type }: ApplyTimeoutOptions) { - const list = await this.redis.getTimeouts(guild); - list.push({ - moderator, - reason: reason === undefined ? null : reason, - expired: Date.now() + time, - issued: Date.now(), - guild, - user: victim, - type, - }); - - await this.redis.client.hmset('nino:timeouts', [guild, JSON.stringify(list)]); - this.send(types.OPCodes.Request, { - moderator, - reason: reason === undefined ? null : reason, - expired: Date.now() + time, - issued: Date.now(), - guild, - user: victim, - type, - }); - - return Promise.resolve(); - } - - private _onOpen() { - this.logger.info('Established a connection with the timeouts service.'); - this.state = types.SocketState.Connected; - - if (this._connectTimeout !== undefined) clearTimeout(this._connectTimeout); - this._readyPromise?.resolve(); - - delete this._readyPromise; - } - - private _onError(error: Error) { - this.logger.error(error); - } - - private _onClose(code: number, reason: string) { - this.logger.warn(`Timeouts service has closed our connection with "${code}: ${reason}"`); - - this._reconnectTimeout = setTimeout(() => { - this.logger.info('Attempting to reconnect...'); - this.load(); - }, 2500); - } - - private async _onMessage(message: string) { - const data: types.DataPacket = JSON.parse(message); - switch (data.op) { - case types.OPCodes.Ready: - { - this.logger.info('Authenicated successfully, now sending timeouts...'); - const timeouts = await this.redis.client - .hvals('nino:timeouts') - .then((value) => - value[0] !== '' ? value.map((val) => JSON.parse(val)).flat() : ([] as types.Timeout[]) - ); - this.logger.info(`Received ${timeouts.length} timeouts to relay`); - - this.send(types.OPCodes.Acknowledged, timeouts); - } - break; - - case types.OPCodes.Apply: - { - const packet = data as types.ApplyPacket; - this.logger.debug(`Told to apply a packet on user ${packet.d.user} in guild ${packet.d.guild}.`); - - const guild = this.discord.client.guilds.get(packet.d.guild); - if (guild === undefined) { - this.logger.warn(`Guild ${packet.d.guild} has pending timeouts but Nino isn't in the guild? Skipping...`); - break; - } - - const timeouts = await this.redis.getTimeouts(packet.d.guild); - const available = timeouts.filter( - (pkt) => - packet.d.user !== pkt.user && - packet.d.type.toLowerCase() !== pkt.type.toLowerCase() && - pkt.guild === packet.d.guild - ); - - await this.redis.client.hmset('nino:timeouts', [guild.id, JSON.stringify(available)]); - await this.punishments.apply({ - moderator: this.discord.client.user, - publish: true, - reason: packet.d.reason === null ? '[Automod] Time is up.' : packet.d.reason, - member: { id: packet.d.user, guild }, - type: packet.d.type, - }); - } - break; - } - } -} diff --git a/src/components/timeouts/types.ts b/src/components/timeouts/types.ts deleted file mode 100644 index 652e1cdd..00000000 --- a/src/components/timeouts/types.ts +++ /dev/null @@ -1,85 +0,0 @@ -/** - * Copyright (c) 2019-2021 Nino - * - * 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. - */ - -import type { PunishmentType } from '.prisma/client'; - -/** - * Current state of the WebSocket connection with the timeouts service - */ -export const enum SocketState { - Connecting = 'connecting', - Connected = 'connected', - Unknown = 'unknown', - Closed = 'closed', -} - -/** - * List of OPCodes to send or receive from - */ -export const enum OPCodes { - // receive - Ready, - Apply, - - // send - Request, - Acknowledged, -} - -/** - * Represents a data packet that is sent out - * @typeparam T - The data packet received - * @typeparam OP - The OPCode that this data represents - */ -export interface DataPacket { - op: OP; - d: T; -} - -export interface Timeout { - moderator: string; - expired: number; - issued: number; - reason: string | null; - guild: string; - user: string; - type: PunishmentTimeoutType; -} - -/** - * Represents that the service is ready and probably has - * incoming timeouts to take action on - */ -export type ReadyPacket = DataPacket; - -/** - * Represents what a request to send to the service - */ -export type RequestPacket = DataPacket; - -/** - * Represents the data payload when we acknowledged - */ -export type AcknowledgedPacket = DataPacket; - -export type ApplyPacket = DataPacket; -export type PunishmentTimeoutType = PunishmentType; diff --git a/src/container.ts b/src/container.ts deleted file mode 100644 index 41b87b67..00000000 --- a/src/container.ts +++ /dev/null @@ -1,44 +0,0 @@ -/** - * Copyright (c) 2019-2021 Nino - * - * 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. - */ - -import { Container } from '@augu/lilith'; -import { join } from 'path'; -import logger from './singletons/Logger'; -import http from './singletons/Http'; - -const app = new Container({ - componentsDir: join(__dirname, 'components'), - servicesDir: join(__dirname, 'services'), - singletons: [http, logger], -}); - -app.on('onBeforeChildInit', (cls, child) => logger.debug(`>> ${cls.name}->${child.constructor.name}: initializing...`)); -app.on('onAfterChildInit', (cls, child) => logger.debug(`>> ✔ ${cls.name}->${child.constructor.name}: initialized`)); -app.on('onBeforeInit', (cls) => logger.debug(`>> ${cls.name}: initializing...`)); -app.on('onAfterInit', (cls) => logger.debug(`>> ✔ ${cls.name}: initialized`)); -app.on('debug', (message) => logger.debug(`lilith: ${message}`)); - -app.on('initError', console.error); -app.on('childInitError', console.error); - -(global as any).app = app; -export default app; diff --git a/src/controllers/AutomodController.ts b/src/controllers/AutomodController.ts deleted file mode 100644 index bd72ff25..00000000 --- a/src/controllers/AutomodController.ts +++ /dev/null @@ -1,59 +0,0 @@ -/** - * Copyright (c) 2019-2021 Nino - * - * 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. - */ - -import type { QueryDeepPartialEntity } from 'typeorm/query-builder/QueryPartialEntity'; -import type Database from '../components/Database'; -import AutomodEntity from '../entities/AutomodEntity'; - -export default class AutomodController { - constructor(private database: Database) {} - - private get repository() { - return this.database.connection.getRepository(AutomodEntity); - } - - get(guildID: string) { - return this.repository.findOne({ guildID }); - } - - create(guildID: string) { - // all automod is disabled by default - const entry = new AutomodEntity(); - entry.blacklistWords = []; - entry.guildID = guildID; - - return this.repository.save(entry); - } - - delete(guildID: string) { - return this.repository.delete({ guildID }); - } - - update(guildID: string, values: QueryDeepPartialEntity) { - return this.database.connection - .createQueryBuilder() - .update(AutomodEntity) - .set(values) - .where('guild_id = :id', { id: guildID }) - .execute(); - } -} diff --git a/src/controllers/BlacklistController.ts b/src/controllers/BlacklistController.ts deleted file mode 100644 index 43a80dfa..00000000 --- a/src/controllers/BlacklistController.ts +++ /dev/null @@ -1,72 +0,0 @@ -/** - * Copyright (c) 2019-2021 Nino - * - * 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. - */ - -import BlacklistEntity, { BlacklistType } from '../entities/BlacklistEntity'; -import type { QueryDeepPartialEntity } from 'typeorm/query-builder/QueryPartialEntity'; -import type Database from '../components/Database'; - -interface CreateBlacklistOptions { - reason?: string; - issuer: string; - type: BlacklistType; - id: string; -} - -export default class BlacklistController { - constructor(private database: Database) {} - - private get repository() { - return this.database.connection.getRepository(BlacklistEntity); - } - - get(id: string) { - return this.repository.findOne({ id }); - } - - getByType(type: BlacklistType) { - return this.repository.find({ type }); - } - - create({ reason, issuer, type, id }: CreateBlacklistOptions) { - const entry = new BlacklistEntity(); - entry.issuer = issuer; - entry.type = type; - entry.id = id; - - if (reason !== undefined) entry.reason = reason; - - return this.repository.save(entry); - } - - delete(id: string) { - return this.repository.delete({ id }); - } - - update(id: string, values: QueryDeepPartialEntity) { - return this.database.connection - .createQueryBuilder() - .update(BlacklistEntity) - .set(values) - .where('id = :id', { id }) - .execute(); - } -} diff --git a/src/controllers/CasesController.ts b/src/controllers/CasesController.ts deleted file mode 100644 index 6c51bce8..00000000 --- a/src/controllers/CasesController.ts +++ /dev/null @@ -1,83 +0,0 @@ -/** - * Copyright (c) 2019-2021 Nino - * - * 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. - */ - -import type { QueryDeepPartialEntity } from 'typeorm/query-builder/QueryPartialEntity'; -import { PunishmentType } from '../entities/PunishmentsEntity'; -import type Database from '../components/Database'; -import CaseEntity from '../entities/CaseEntity'; - -interface CreateCaseOptions { - attachments: string[]; - moderatorID: string; - victimID: string; - guildID: string; - reason?: string; - soft?: boolean; - time?: number; - type: PunishmentType; -} - -export default class CasesController { - constructor(private database: Database) {} - - get repository() { - return this.database.connection.getRepository(CaseEntity); - } - - get(guildID: string, caseID: number) { - return this.repository.findOne({ guildID, index: caseID }); - } - - getAll(guildID: string) { - return this.repository.find({ guildID }); - } - - async create({ attachments, moderatorID, victimID, guildID, reason, soft, time, type }: CreateCaseOptions) { - const cases = await this.getAll(guildID); - const index = (cases[cases.length - 1]?.index ?? 0) + 1; - - const entry = new CaseEntity(); - entry.attachments = attachments; - entry.moderatorID = moderatorID; - entry.victimID = victimID; - entry.guildID = guildID; - entry.index = index; - entry.soft = soft === true; // if it's undefined, then it'll be false so no ternaries :crab: - entry.type = type; - - if (reason !== undefined) entry.reason = reason; - - if (time !== undefined) entry.time = String(time); - - return this.repository.save(entry); - } - - update(guildID: string, index: number, values: QueryDeepPartialEntity) { - return this.database.connection - .createQueryBuilder() - .update(CaseEntity) - .set(values) - .where('guild_id = :id', { id: guildID }) - .andWhere('index = :idx', { idx: index }) - .execute(); - } -} diff --git a/src/controllers/GuildSettingsController.ts b/src/controllers/GuildSettingsController.ts deleted file mode 100644 index c2446465..00000000 --- a/src/controllers/GuildSettingsController.ts +++ /dev/null @@ -1,73 +0,0 @@ -/** - * Copyright (c) 2019-2021 Nino - * - * 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. - */ - -import type { QueryDeepPartialEntity } from 'typeorm/query-builder/QueryPartialEntity'; -import type Database from '../components/Database'; -import GuildEntity from '../entities/GuildEntity'; -import { Inject } from '@augu/lilith'; -import Config from '../components/Config'; - -export default class GuildSettingsController { - @Inject - private readonly config!: Config; - - constructor(private database: Database) {} - - get repository() { - return this.database.connection.getRepository(GuildEntity); - } - - get(id: string, create?: true): Promise; - get(id: string, create?: false): Promise; - async get(id: string, create: boolean = true) { - const settings = await this.repository.findOne({ guildID: id }); - if (settings === undefined && create) return this.create(id); - - return settings; - } - - async create(id: string) { - const entry = new GuildEntity(); - entry.prefixes = this.config.getProperty('prefixes') ?? []; - entry.language = 'en_US'; - entry.guildID = id; - - await this.repository.save(entry); - await this.database.logging.create(id); - await this.database.automod.create(id); - - return entry; - } - - delete(id: string) { - return this.repository.delete({ guildID: id }); - } - - update(guildID: string, values: QueryDeepPartialEntity) { - return this.database.connection - .createQueryBuilder() - .update(GuildEntity) - .set(values) - .where('guild_id = :id', { id: guildID }) - .execute(); - } -} diff --git a/src/controllers/LoggingController.ts b/src/controllers/LoggingController.ts deleted file mode 100644 index ef53a741..00000000 --- a/src/controllers/LoggingController.ts +++ /dev/null @@ -1,60 +0,0 @@ -/** - * Copyright (c) 2019-2021 Nino - * - * 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. - */ - -import type { QueryDeepPartialEntity } from 'typeorm/query-builder/QueryPartialEntity'; -import LoggingEntity from '../entities/LoggingEntity'; -import type Database from '../components/Database'; - -export default class LoggingController { - constructor(private database: Database) {} - - private get repository() { - return this.database.connection.getRepository(LoggingEntity); - } - - async get(guildID: string) { - const entry = await this.repository.findOne({ guildID }); - if (entry === undefined) return this.create(guildID); - - return entry; - } - - create(guildID: string) { - const entry = new LoggingEntity(); - entry.ignoreChannels = []; - entry.ignoreUsers = []; - entry.enabled = false; - entry.events = []; - entry.guildID = guildID; - - return this.repository.save(entry); - } - - update(guildID: string, values: QueryDeepPartialEntity) { - return this.database.connection - .createQueryBuilder() - .update(LoggingEntity) - .set(values) - .where('guild_id = :id', { id: guildID }) - .execute(); - } -} diff --git a/src/controllers/PunishmentsController.ts b/src/controllers/PunishmentsController.ts deleted file mode 100644 index bc3abf61..00000000 --- a/src/controllers/PunishmentsController.ts +++ /dev/null @@ -1,76 +0,0 @@ -/** - * Copyright (c) 2019-2021 Nino - * - * 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. - */ - -import PunishmentEntity, { PunishmentType } from '../entities/PunishmentsEntity'; -import type { QueryDeepPartialEntity } from 'typeorm/query-builder/QueryPartialEntity'; -import Database from '../components/Database'; - -interface CreatePunishmentOptions { - warnings: number; - guildID: string; - soft?: boolean; - time?: number; - days?: number; - type: PunishmentType; -} - -export default class PunishmentsController { - constructor(private database: Database) {} - - private get repository() { - return this.database.connection.getRepository(PunishmentEntity); - } - - async create({ warnings, guildID, soft, time, days, type }: CreatePunishmentOptions) { - const all = await this.getAll(guildID); - const entry = new PunishmentEntity(); - entry.warnings = warnings; - entry.guildID = guildID; - entry.index = all.length + 1; // increment by 1 - entry.type = type; - - if (soft !== undefined && soft === true) entry.soft = true; - - if (time !== undefined) entry.time = time; - - if (days !== undefined) entry.days = days; - - return this.repository.save(entry); - } - - getAll(guildID: string) { - return this.repository.find({ guildID }); - } - - get(guildID: string, index: number) { - return this.repository.findOne({ guildID, index }); - } - - update(guildID: string, values: QueryDeepPartialEntity) { - return this.database.connection - .createQueryBuilder() - .update(PunishmentEntity) - .set(values) - .where('guild_id = :id', { id: guildID }) - .execute(); - } -} diff --git a/src/controllers/WarningsController.ts b/src/controllers/WarningsController.ts deleted file mode 100644 index 13ad0d8c..00000000 --- a/src/controllers/WarningsController.ts +++ /dev/null @@ -1,75 +0,0 @@ -/** - * Copyright (c) 2019-2021 Nino - * - * 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. - */ - -import type { QueryDeepPartialEntity } from 'typeorm/query-builder/QueryPartialEntity'; -import WarningEntity from '../entities/WarningsEntity'; -import type Database from '../components/Database'; - -interface CreateWarningOptions { - userID: string; - guildID: string; - reason?: string; - amount: number; -} - -export default class WarningsController { - constructor(private database: Database) {} - - private get repository() { - return this.database.connection.getRepository(WarningEntity); - } - - get(guildID: string, userID: string) { - return this.repository.findOne({ guildID, userID }); - } - - getAll(guildID: string, userID?: string) { - const filter = userID !== undefined ? { guildID, userID } : { guildID }; - return this.repository.find(filter); - } - - create({ guildID, userID, reason, amount }: CreateWarningOptions) { - if (amount < 0) throw new RangeError('amount index out of bounds'); - - const entry = new WarningEntity(); - entry.guildID = guildID; - entry.reason = reason; - entry.amount = amount; - entry.userID = userID; - - return this.repository.save(entry); - } - - update(guildID: string, userID: string, values: QueryDeepPartialEntity) { - return this.database.connection - .createQueryBuilder() - .update(WarningEntity) - .set(values) - .where('guild_id = :id', { id: guildID }) - .andWhere('user_id = :id', { id: userID }) - .execute(); - } - - clean(guildID: string, userID: string) { - return this.repository.delete({ guildID, userID }); - } -} diff --git a/src/listeners/GuildBansListener.ts b/src/listeners/GuildBansListener.ts deleted file mode 100644 index 9a547033..00000000 --- a/src/listeners/GuildBansListener.ts +++ /dev/null @@ -1,144 +0,0 @@ -/** - * Copyright (c) 2019-2021 Nino - * - * 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. - */ - -import PunishmentService, { PunishmentEntryType } from '../services/PunishmentService'; -import { Constants, Guild, User } from 'eris'; -import { Inject, Subscribe } from '@augu/lilith'; -import Database from '../components/Database'; -import Discord from '../components/Discord'; -import { PrismaClient, PunishmentType } from '.prisma/client'; - -export default class GuildBansListener { - @Inject - private readonly punishments!: PunishmentService; - - @Inject - private readonly database!: Database; - - @Inject - private readonly discord!: Discord; - - @Inject - private readonly prisma!: PrismaClient; - - @Subscribe('guildBanAdd', { emitter: 'discord' }) - async onGuildBanAdd(guild: Guild, user: User) { - if (!guild.members.get(this.discord.client.user.id)?.permissions.has('viewAuditLogs')) { - return; - } - - const audits = await guild.getAuditLog({ - actionType: Constants.AuditLogActions.MEMBER_BAN_ADD, - limit: 3, - }); - - const entry = audits.entries.find( - (entry) => entry.targetID === user.id && entry.user.id !== this.discord.client.user.id - ); - - if (entry === undefined) return; - - // get case index - const newest = await this.prisma.cases.findMany({ - where: { - guildId: guild.id, - }, - orderBy: { - index: 'asc', - }, - }); - - const index = newest[0] !== undefined ? newest[0].index + 1 : 1; - const caseModel = await this.prisma.cases.create({ - data: { - moderatorId: this.discord.client.user.id, - victimId: entry.user.id, - reason: entry.reason ?? '[Automod] Moderator has banned the invidiual with no reasoning.', - guildId: guild.id, - index, - soft: false, - type: PunishmentType.BAN, - }, - }); - - await this.punishments['publishToModLog']( - { - moderator: this.discord.client.users.get(entry.user.id)!, - victim: this.discord.client.users.get(entry.targetID)!, - reason: entry.reason ?? '[Automod] Moderator has banned the invidiual with no reasoning.', - guild: entry.guild, - type: PunishmentEntryType.Banned, - }, - caseModel - ); - } - - @Subscribe('guildBanRemove', { emitter: 'discord' }) - async onGuildBanRemove(guild: Guild, user: User) { - if (!guild.members.get(this.discord.client.user.id)?.permissions.has('viewAuditLogs')) return; - - const audits = await guild.getAuditLog({ - actionType: Constants.AuditLogActions.MEMBER_BAN_REMOVE, - limit: 3, - }); - - const entry = audits.entries.find( - (entry) => entry.targetID === user.id && entry.user.id !== this.discord.client.user.id - ); - - if (entry === undefined) return; - - // get case index - const newest = await this.prisma.cases.findMany({ - where: { - guildId: guild.id, - }, - orderBy: { - index: 'asc', - }, - }); - - const index = newest[0] !== undefined ? newest[0].index + 1 : 1; - const caseModel = await this.prisma.cases.create({ - data: { - moderatorId: this.discord.client.user.id, - victimId: entry.user.id, - reason: 'Moderator has unbanned on their own accord.', - guildId: guild.id, - index, - soft: false, - type: PunishmentType.UNBAN, - }, - }); - - await this.punishments['publishToModLog']( - { - moderator: this.discord.client.users.get(entry.user.id)!, - victim: this.discord.client.users.get(entry.targetID)!, - reason: 'Moderator has unbanned on their own accord.', - guild: entry.guild, - type: PunishmentEntryType.Unban, - }, - caseModel - ); - } -} diff --git a/src/listeners/GuildListener.ts b/src/listeners/GuildListener.ts deleted file mode 100644 index e719cda0..00000000 --- a/src/listeners/GuildListener.ts +++ /dev/null @@ -1,147 +0,0 @@ -/** - * Copyright (c) 2019-2021 Nino - * - * 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. - */ - -import type { Guild, TextChannel } from 'eris'; -import { Inject, Subscribe } from '@augu/lilith'; -import { EmbedBuilder } from '../structures'; -import BotlistsService from '../services/BotlistService'; -import { Logger } from 'tslog'; -import Database from '../components/Database'; -import Discord from '../components/Discord'; -import Config from '../components/Config'; -import Prom from '../components/Prometheus'; - -export default class VoidListener { - @Inject - private readonly prometheus?: Prom; - - @Inject - private readonly database!: Database; - - @Inject - private readonly discord!: Discord; - - @Inject - private readonly botlists?: BotlistsService; - - @Inject - private readonly logger!: Logger; - - @Inject - private readonly config!: Config; - - @Subscribe('guildCreate', { emitter: 'discord' }) - async onGuildCreate(guild: Guild) { - if (guild.name === undefined) return; - - this.logger.info(`✔ New Guild: ${guild.name} (${guild.id})`); - await this.database.guilds.create(guild.id); - this.prometheus?.guildCount?.inc(); - await this.botlists?.post(); - - const channel = this.discord.client.getChannel('844410521599737878') as TextChannel; - const owner = this.discord.client.users.get(guild.ownerID); - const bots = guild.members.filter((r) => r.bot).length; - const humans = guild.members.filter((r) => !r.bot).length; - - const prefixes = this.config.getProperty('prefixes') ?? ['x!']; - const statusType = this.config.getProperty('status.type'); - const status = this.config.getProperty('status.status')!; - - for (const shard of this.discord.client.shards.values()) { - this.discord.client.editStatus(this.config.getProperty('status.presence') ?? 'online', { - name: status - .replace('$prefix$', prefixes[Math.floor(Math.random() * prefixes.length)]) - .replace('$guilds$', this.discord.client.guilds.size.toLocaleString()) - .replace('$shard$', `#${shard.id}`), - - type: statusType ?? 0, - }); - } - - if (channel !== undefined && channel.type === 0) { - const embed = EmbedBuilder.create() - .setAuthor( - `[ Joined ${guild.name} (${guild.id}) ]`, - undefined, - this.discord.client.user.dynamicAvatarURL('png', 1024) - ) - .setDescription([ - `• **Members [Bots / Total]**: ${humans.toLocaleString()} members with ${bots} bots (large?: ${ - guild.large ? 'Yes' : 'No' - })`, - `• **Owner**: ${owner ? `${owner.username}#${owner.discriminator} (${owner.id})` : 'Not cached'}`, - ]) - .setFooter(`✔ Now at ${this.discord.client.guilds.size.toLocaleString()} Guilds`); - - return channel.createMessage({ embed: embed.build() }); - } - } - - @Subscribe('guildDelete', { emitter: 'discord' }) - async onGuildDelete(guild: Guild) { - if (guild.name === undefined) return; - - this.logger.info(`❌ Left Guild: ${guild.name} (${guild.id})`); - await this.database.guilds.delete(guild.id); - this.prometheus?.guildCount?.dec(); - await this.botlists?.post(); - - const channel = this.discord.client.getChannel('844410521599737878') as TextChannel; - const owner = this.discord.client.users.get(guild.ownerID); - const bots = guild.members.filter((r) => r.bot).length; - const humans = guild.members.filter((r) => !r.bot).length; - - const prefixes = this.config.getProperty('prefixes') ?? ['x!']; - const statusType = this.config.getProperty('status.type'); - const status = this.config.getProperty('status.status')!; - - for (const shard of this.discord.client.shards.values()) { - this.discord.client.editStatus(this.config.getProperty('status.presence') ?? 'online', { - name: status - .replace('$prefix$', prefixes[Math.floor(Math.random() * prefixes.length)]) - .replace('$guilds$', this.discord.client.guilds.size.toLocaleString()) - .replace('$shard$', `#${shard.id}`), - - type: statusType ?? 0, - }); - } - - if (channel !== undefined && channel.type === 0) { - const embed = EmbedBuilder.create() - .setAuthor( - `[ Left ${guild.name} (${guild.id}) ]`, - undefined, - this.discord.client.user.dynamicAvatarURL('png', 1024) - ) - .setDescription([ - `• **Members [Bots / Total]**: ${humans.toLocaleString()} members with ${bots} bots (large?: ${ - guild.large ? 'Yes' : 'No' - })`, - `• **Owner**: ${owner ? `${owner.username}#${owner.discriminator} (${owner.id})` : 'Not cached'}`, - ]) - .setFooter(`✔ Now at ${this.discord.client.guilds.size.toLocaleString()} Guilds`); - - return channel.createMessage({ embed: embed.build() }); - } - } -} diff --git a/src/listeners/GuildMemberListener.ts b/src/listeners/GuildMemberListener.ts deleted file mode 100644 index d5417f6c..00000000 --- a/src/listeners/GuildMemberListener.ts +++ /dev/null @@ -1,178 +0,0 @@ -/** - * Copyright (c) 2019-2021 Nino - * - * 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. - */ - -import PunishmentService, { PunishmentEntryType } from '../services/PunishmentService'; -import { Constants, Guild, Member } from 'eris'; -import { Inject, Subscribe } from '@augu/lilith'; -import { PunishmentType } from '@prisma/client'; -import AutomodService from '../services/AutomodService'; -import Database from '../components/Database'; -import Discord from '../components/Discord'; - -interface OldMember { - premiumSince: number | null; - pending: boolean; - nick?: string; - roles: string[]; -} - -export default class GuildMemberListener { - @Inject - private readonly punishments!: PunishmentService; - - @Inject - private readonly database!: Database; - - @Inject - private readonly discord!: Discord; - - @Inject - private readonly automod!: AutomodService; - - private async findAuditLog(guild: Guild, member: Member) { - if (!guild.members.get(this.discord.client.user.id)?.permissions.has('viewAuditLogs')) return undefined; - - try { - const audits = await guild.getAuditLog({ - limit: 3, - actionType: Constants.AuditLogActions.MEMBER_ROLE_UPDATE, - }); - return audits.entries - .sort((a, b) => b.createdAt - a.createdAt) - .find( - (entry) => - entry.user.id !== this.discord.client.user.id && // Check if the user that did it was not Nino - entry.targetID === member.id && // Check if the target ID is the member - entry.user.id !== member.id // Check if the user isn't thereselves - ); - } catch { - return undefined; - } - } - - @Subscribe('guildMemberUpdate', { emitter: 'discord' }) - async onGuildMemberUpdate(guild: Guild, member: Member, old: OldMember) { - const settings = await this.database.automod.get(guild.id); - const gSettings = await this.database.guilds.get(guild.id); - - // cannot really do anything if `old` = null - if (old === null) return; - - if (old.hasOwnProperty('nick') && (old.nick !== undefined || old.nick !== null) && member.nick !== old.nick) { - if (settings !== undefined && settings.dehoist === false) return; - - const result = await this.automod.run('memberNick', member); - if (result) return; - } - - if (member.user.bot) return; - - if (gSettings.mutedRoleID === undefined) return; - - // taken away - if (!member.roles.includes(gSettings.mutedRoleID) && old.roles.includes(gSettings.mutedRoleID)) { - const entry = await this.findAuditLog(guild, member); - if (!entry) return; - - await this.punishments.apply({ - moderator: entry.user, - member, - reason: '[Automod] Moderator has removed the Muted role', - type: PunishmentType.UNMUTE, - }); - } - - // added it - if (member.roles.includes(gSettings.mutedRoleID) && !old.roles.includes(gSettings.mutedRoleID)) { - const entry = await this.findAuditLog(guild, member); - if (!entry) return; - - await this.punishments.apply({ - moderator: entry.user, - member, - reason: '[Automod] Moderator has added the Muted role', - type: PunishmentType.MUTE, - }); - } - } - - @Subscribe('guildMemberAdd', { emitter: 'discord' }) - async onGuildMemberJoin(guild: Guild, member: Member) { - const result = await this.automod.run('memberJoin', member); - if (result) return; - - const cases = await this.database.cases.getAll(guild.id); - const all = cases.filter((c) => c.victimID === member.id).sort((c) => c.index); - - if (all.length > 0 && all[all.length - 1]?.type === PunishmentType.MUTE.toLowerCase()) { - await this.punishments.apply({ - moderator: this.discord.client.user, - member, - reason: '[Automod] Mute Evading', - type: PunishmentType.MUTE, - }); - } - } - - @Subscribe('guildMemberRemove', { emitter: 'discord' }) - async onGuildMemberRemove(guild: Guild, member: Member) { - const logs = await guild - .getAuditLog({ - limit: 3, - actionType: Constants.AuditLogActions.MEMBER_KICK, - }) - .catch(() => undefined); - - if (logs === undefined) return; - - if (!logs.entries.length) return; - - const entry = logs.entries.find( - (entry) => entry.targetID === member.id && entry.user.id !== this.discord.client.user.id - ); - - if (!entry) return; - - const model = await this.database.cases.create({ - attachments: [], - moderatorID: entry.user.id, - victimID: entry.targetID, - guildID: guild.id, - reason: '[Automod] User was kicked by moderator', - // @ts-ignore - type: PunishmentType.KICK.toLowerCase(), - }); - - await this.punishments['publishToModLog']( - { - moderator: entry.user, - victim: this.discord.client.users.get(entry.targetID)!, - reason: `[Automod] Automatic kick: ${entry.reason ?? 'unknown'}`, - guild, - type: PunishmentEntryType.Kicked, - }, - - // @ts-ignore - model - ); - } -} diff --git a/src/listeners/GuildRoleListener.ts b/src/listeners/GuildRoleListener.ts deleted file mode 100644 index 41af1f28..00000000 --- a/src/listeners/GuildRoleListener.ts +++ /dev/null @@ -1,44 +0,0 @@ -/** - * Copyright (c) 2019-2021 Nino - * - * 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. - */ - -import { Inject, Subscribe } from '@augu/lilith'; -import type { Guild, Role } from 'eris'; -import { Logger } from 'tslog'; -import Database from '../components/Database'; - -export default class GuildRoleListener { - @Inject - private readonly database!: Database; - - @Inject - private readonly logger!: Logger; - - @Subscribe('guildRoleDelete', { emitter: 'discord' }) - async onGuildRoleDelete(guild: Guild, role: Role) { - const settings = await this.database.guilds.get(guild.id); - if (!settings.mutedRoleID) return; - if (role.id !== settings.mutedRoleID) return; - - this.logger.warn(`Muted role ${settings.mutedRoleID} was accidently deleted, so I deleted it in the database. :)`); - await this.database.guilds.update(guild.id, { mutedRoleID: role.id }); - } -} diff --git a/src/listeners/MessageListener.ts b/src/listeners/MessageListener.ts deleted file mode 100644 index af0e0986..00000000 --- a/src/listeners/MessageListener.ts +++ /dev/null @@ -1,328 +0,0 @@ -/** - * Copyright (c) 2019-2021 Nino - * - * 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. - */ - -import { - Constants, - Message, - OldMessage, - TextChannel -} from 'eris'; - -import { Inject, Subscribe } from '@augu/lilith'; -import { LoggingEvents } from '../entities/LoggingEntity'; -import { EmbedBuilder } from '../structures'; -import CommandService from '../services/CommandService'; -import AutomodService from '../services/AutomodService'; -import { Color } from '../util/Constants'; -import Database from '../components/Database'; -import Discord from '../components/Discord'; - -const HTTP_REGEX = /^https?:\/\/(.*)/; - -export default class MessageListener { - @Inject - private readonly commands!: CommandService; - - @Inject - private readonly automod!: AutomodService; - - @Inject - private readonly database!: Database; - - @Inject - private readonly discord!: Discord; - - // @Subscribe('interactionCreate', { emitter: 'discord' }) - // async onInteractionCreate(interaction: Interaction) { - // // We don't care about interaction pings D: - // if (interaction.type === 1) return; - - // // We care about command interactions! - // if (interaction.type === 2) { - // // If we haven't been ready, let's not initialize - // // slash commands. - // if (!this.discord.slashCreator) - // return (interaction as CommandInteraction).createMessage({ - // content: 'Client is not ready to receive slash commands, use the normal commands!', - // flags: 64, - // }); - // } - - // // slash-create will handle the rest. - // } - - @Subscribe('messageCreate', { emitter: 'discord' }) - onMessageCreate(msg: Message) { - return this.commands.handleCommand(msg); - } - - @Subscribe('messageDelete', { emitter: 'discord' }) - async onMessageDelete(msg: Message) { - if (!msg.author || ![0, 5].includes(msg.channel.type)) return; - - const settings = await this.database.logging.get(msg.guildID); - if (!settings.enabled || !settings.events.includes(LoggingEvents.MessageDeleted)) return; - - if (settings.ignoreChannels.length > 0 && settings.ignoreChannels.includes(msg.channel.id)) return; - - if (settings.ignoreUsers.length > 0 && settings.ignoreUsers.includes(msg.author.id)) return; - - if ( - settings.channelID !== undefined && - (!msg.channel.guild.channels.has(settings.channelID) || - !msg.channel.guild.channels - .get(settings.channelID) - ?.permissionsOf(this.discord.client.user.id) - .has('sendMessages')) - ) - return; - - if (msg.content.indexOf('pinned a message') !== -1) return; - - if (msg.author.id === this.discord.client.user.id) return; - - if (msg.author.system) return; - - // It's in a closure so we don't have to use `return;` on the outer scope - const auditLog = await (async () => { - if (!msg.channel.guild.members.get(this.discord.client.user.id)?.permissions.has('viewAuditLogs')) - return undefined; - - const audits = await msg.channel.guild.getAuditLog({ - limit: 3, - actionType: Constants.AuditLogActions.MESSAGE_DELETE, - }); - return audits.entries.find( - (entry) => - entry.targetID === msg.author.id && - entry.user.id !== msg.author.id && - entry.user.id !== this.discord.client.user.id - ); - })(); - - const channel = msg.channel.guild.channels.get(settings.channelID!); - const author = msg.author.system ? 'System' : `${msg.author.username}#${msg.author.discriminator}`; - const embed = new EmbedBuilder().setColor(Color); - - if (auditLog !== undefined) - embed.setFooter( - `Message was actually deleted by ${auditLog.user.username}#${auditLog.user.discriminator} (${auditLog.user.id})` - ); - - if (msg.embeds.length > 0) { - const em = msg.embeds[0]; - if (em.author) embed.setAuthor(em.author.name, em.author.url, em.author.icon_url); - if (em.description) - embed.setDescription(em.description.length > 2000 ? `${em.description.slice(0, 1993)}...` : em.description); - if (em.fields && em.fields.length > 0) { - for (const field of em.fields) embed.addField(field.name, field.value, field.inline || false); - } - - if (em.footer) { - const footer = embed.footer; - embed.setFooter( - footer !== undefined ? `${em.footer.text} (${footer.text})` : em.footer.text, - em.footer.icon_url - ); - } - - if (em.title) embed.setTitle(em.title); - if (em.url) embed.setURL(em.url); - } else { - embed.setDescription( - msg.content.length > 1997 - ? `${msg.content.slice(0, 1995)}...` - : msg.content || 'Nothing was provided (probably attachments)' - ); - } - - return channel.createMessage({ - content: `**[** A message was deleted by **${author}** (⁄ ⁄•⁄ω⁄•⁄ ⁄) in <#${msg.channel.id}> **]**`, - embed: embed.build(), - }); - } - - @Subscribe('messageUpdate', { emitter: 'discord' }) - async onMessageUpdate(msg: Message, old: OldMessage | null) { - await this.automod.run('message', msg); - - if (old === null) return; - - if (old.content === msg.content) return; - - // discord is shit send help please - if (old.pinned && !msg.pinned) return; - - if (msg.content !== old.content) await this.commands.handleCommand(msg); - - const result = await this.automod.run('message', msg); - if (result) return; - - const settings = await this.database.logging.get(msg.channel.guild.id); - if (!settings.enabled || !settings.events.includes(LoggingEvents.MessageUpdated)) return; - - if (settings.ignoreChannels.length > 0 && settings.ignoreChannels.includes(msg.channel.id)) return; - - if (settings.ignoreUsers.length > 0 && settings.ignoreUsers.includes(msg.author.id)) return; - - if ( - settings.channelID !== undefined && - (!msg.channel.guild.channels.has(settings.channelID) || - !msg.channel.guild.channels - .get(settings.channelID) - ?.permissionsOf(this.discord.client.user.id) - .has('sendMessages')) - ) - return; - - if (msg.content.indexOf('pinned a message') !== -1) return; - - if (msg.author.id === this.discord.client.user.id) return; - - // discord being shit part 2 - if (HTTP_REGEX.test(old.content)) return; - - const channel = msg.channel.guild.channels.get(settings.channelID!); - const author = msg.author.system ? 'System' : `${msg.author.username}#${msg.author.discriminator}`; - const jumpUrl = `https://discord.com/channels/${msg.guildID}/${msg.channel.id}/${msg.id}`; - const embed = new EmbedBuilder() - .setColor(Color) - .setDescription(`**[[Jump Here]](${jumpUrl})**`) - .addFields([ - { - name: '❯ Old Message Content', - value: old.content || 'Nothing was provided (probably attachments?)', - inline: false, - }, - { - name: '❯ Message Content', - value: msg.content || 'Nothing was provided?', - }, - ]); - - return channel.createMessage({ - content: `**[** A message was updated by **${author}** (⁄ ⁄•⁄ω⁄•⁄ ⁄) in <#${msg.channel.id}> **]**`, - embed: embed.build(), - }); - } - - @Subscribe('messageDeleteBulk', { emitter: 'discord' }) - async onMessageDeleteBulk(messages: Message[]) { - const allMsgs = messages - .filter((msg) => msg.guildID !== undefined) - .sort((a, b) => new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime()); - - const msg = allMsgs[0]; - const settings = await this.database.logging.get(msg.channel.guild.id); - if (!settings.enabled || !settings.events.includes(LoggingEvents.MessageUpdated)) return; - - if (!settings.channelID) return; - - if (!msg.channel.guild) return; - - if ( - !msg.channel.guild.channels.has(settings.channelID) || - !msg.channel.guild.channels - .get(settings.channelID) - ?.permissionsOf(this.discord.client.user.id) - .has('sendMessages') - ) - return; - - const buffers: Buffer[] = []; - for (let i = 0; i < allMsgs.length; i++) { - const msg = allMsgs[i]; - - // skip all that don't have an author - if (!msg.author) continue; - - const contents = [ - `♥*♡∞:。.。 [ Message #${i + 1} / ${allMsgs.length} ] 。.。:∞♡*♥`, - `❯ Created At: ${new Date(msg.createdAt).toUTCString()}`, - `❯ Author : ${msg.author.username}#${msg.author.discriminator}`, - `❯ Channel : #${msg.channel.name} (${msg.channel.id})`, - '', - ]; - - if (msg.embeds.length > 0) { - contents.push(msg.content); - contents.push('\n'); - - for (let j = 0; j < msg.embeds.length; j++) { - const embed = msg.embeds[j]; - let content = `[ Embed ${j + 1}/${msg.embeds.length} ]\n`; - if (embed.author !== undefined) - content += `❯ ${embed.author.name}${embed.author.url !== undefined ? ` (${embed.author.url})` : ''}\n`; - - if (embed.title !== undefined) - content += `❯ ${embed.title}${embed.url !== undefined ? ` (${embed.url})` : ''}\n`; - - if (embed.description !== undefined) content += `${embed.description}\n\n`; - - if (embed.fields !== undefined) - content += embed.fields.map((field) => `• ${field.name}: ${field.value}`).join('\n') + '\n'; - - if (embed.footer !== undefined) - content += `${embed.footer.text}${ - embed.timestamp !== undefined - ? ` (${(embed.timestamp instanceof Date ? embed.timestamp : new Date(embed.timestamp)).toUTCString()})` - : '' - }`; - - contents.push(content, '\n'); - } - } else { - contents.push(msg.content, '\n'); - } - - buffers.push(Buffer.from(contents.join('\n'))); - } - - // Don't do anything if we can't create a message - if (buffers.length > 0) return; - - const buffer = Buffer.concat(buffers); - const channel = msg.channel.guild.channels.get(settings.channelID!); - const users: string[] = [...new Set(allMsgs.map((m) => `${m.author.username}#${m.author.discriminator}`))]; - const embed = new EmbedBuilder() - .setColor(Color) - .setDescription([ - `${allMsgs.length} messages were deleted in ${msg.channel.mention}, view the file below to read all messages`, - '', - '```apache', - `❯ Messages Deleted ~> ${allMsgs.length}/${messages.length} (${( - (allMsgs.length / messages.length) * - 100 - ).toFixed(1)}% cached)`, - `❯ Affected Users ~> ${users.join(', ')}`, - '```', - ]); - - await Promise.all([ - channel.createMessage({ embed: embed.build() }), - channel.createMessage('', { - file: buffer, - name: `trace_${Date.now()}.txt`, - }), - ]); - } -} diff --git a/src/listeners/VoiceStateListener.ts b/src/listeners/VoiceStateListener.ts deleted file mode 100644 index 8e43bf92..00000000 --- a/src/listeners/VoiceStateListener.ts +++ /dev/null @@ -1,114 +0,0 @@ -/** - * Copyright (c) 2019-2021 Nino - * - * 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. - */ - -import { Guild, Member, TextChannel, VoiceChannel } from 'eris'; -import { Inject, Subscribe } from '@augu/lilith'; -import { LoggingEvents } from '../entities/LoggingEntity'; -import Database from '../components/Database'; -import Discord from '../components/Discord'; - -export default class VoiceStateListener { - @Inject - private database!: Database; - - @Inject - private discord!: Discord; - - private async getAuditLog(guild: Guild, actionType: number, condition?: string) { - if (!guild.members.get(this.discord.client.user.id)?.permissions.has('viewAuditLogs')) return undefined; - - try { - const audits = await guild.getAuditLog({ limit: 3, actionType }); - return audits.entries - .sort((a, b) => b.createdAt - a.createdAt) - .find( - (entry) => - entry.user.id === this.discord.client.user.id && // If Nino has done this action - condition !== undefined && - entry.reason?.startsWith(condition) - ); - } catch { - return undefined; - } - } - - @Subscribe('voiceChannelJoin', { emitter: 'discord' }) - async onVoiceChannelJoin(member: Member, voice: VoiceChannel) { - const settings = await this.database.logging.get(member.guild.id); - if (!settings.enabled || !settings.events.includes(LoggingEvents.VoiceChannelJoin)) return; - - const channel = - settings.channelID !== undefined ? await this.discord.getChannel(settings.channelID) : null; - if ( - channel === null || - !member.guild.channels.has(settings.channelID!) || - !member.guild.channels.get(settings.channelID!)!.permissionsOf(this.discord.client.user.id).has('sendMessages') - ) - return; - - return channel.createMessage( - `:loudspeaker: **${member.user.username}#${member.user.discriminator}** (${member.user.id}) has joined channel **${voice.name}** with ${voice.voiceMembers.size} members.` - ); - } - - @Subscribe('voiceChannelLeave', { emitter: 'discord' }) - async onVoiceChannelLeave(member: Member, voice: VoiceChannel) { - const settings = await this.database.logging.get(member.guild.id); - if (!settings.enabled || !settings.events.includes(LoggingEvents.VoiceChannelJoin)) return; - - // Don't log entries if Nino has kicked them - const entry = await this.getAuditLog(member.guild, 27, '[Voice Kick]'); - if (entry !== undefined) return; - - const channel = - settings.channelID !== undefined ? await this.discord.getChannel(settings.channelID) : null; - if ( - channel === null || - !member.guild.channels.has(settings.channelID!) || - !member.guild.channels.get(settings.channelID!)!.permissionsOf(this.discord.client.user.id).has('sendMessages') - ) - return; - - return channel.createMessage( - `:bust_in_silhouette: **${member.username}#${member.discriminator}** (${member.id}) has left channel **${voice.name}**` - ); - } - - @Subscribe('voiceChannelSwitch', { emitter: 'discord' }) - async onVoiceChannelSwitch(member: Member, voice: VoiceChannel, old: VoiceChannel) { - const settings = await this.database.logging.get(member.guild.id); - if (!settings.enabled || !settings.events.includes(LoggingEvents.VoiceChannelJoin)) return; - - const channel = - settings.channelID !== undefined ? await this.discord.getChannel(settings.channelID) : null; - if ( - channel === null || - !member.guild.channels.has(settings.channelID!) || - !member.guild.channels.get(settings.channelID!)!.permissionsOf(this.discord.client.user.id).has('sendMessages') - ) - return; - - return channel.createMessage( - `:radio_button: **${member.username}#${member.discriminator}** (${member.id}) has switch from channel ${old.name} to ${voice.name}.` - ); - } -} diff --git a/src/listeners/VoidListener.ts b/src/listeners/VoidListener.ts deleted file mode 100644 index 26a85e12..00000000 --- a/src/listeners/VoidListener.ts +++ /dev/null @@ -1,98 +0,0 @@ -/** - * Copyright (c) 2019-2021 Nino - * - * 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. - */ - -/* eslint-disable camelcase */ - -import { SlashCreator, GatewayServer } from 'slash-create'; -import { Inject, Subscribe } from '@augu/lilith'; -import type { RawPacket } from 'eris'; -import BotlistsService from '../services/BotlistService'; -import { Logger } from 'tslog'; -import Discord from '../components/Discord'; -import Config from '../components/Config'; -import Prom from '../components/Prometheus'; -import { join } from 'path'; - -export default class VoidListener { - @Inject - private readonly prometheus?: Prom; - - @Inject - private readonly discord!: Discord; - - @Inject - private readonly botlists?: BotlistsService; - - @Inject - private readonly logger!: Logger; - - @Inject - private readonly config!: Config; - - @Subscribe('rawWS', { emitter: 'discord' }) - async onRawWS(packet: RawPacket) { - if (!packet.t) return; - - this.prometheus?.rawWSEvents?.labels(packet.t).inc(); - } - - @Subscribe('ready', { emitter: 'discord' }) - async onReady() { - this.logger.info( - `Connected as ${this.discord.client.user.username}#${this.discord.client.user.discriminator} (ID: ${this.discord.client.user.id})` - ); - this.logger.info( - `Guilds: ${this.discord.client.guilds.size.toLocaleString()} | Users: ${this.discord.client.users.size.toLocaleString()}` - ); - - this.prometheus?.guildCount?.set(this.discord.client.guilds.size); - await this.botlists?.post(); - this.discord.mentionRegex = new RegExp(`^<@!?${this.discord.client.user.id}> `); - - const prefixes = this.config.getProperty('prefixes') ?? ['x!']; - const statusType = this.config.getProperty('status.type'); - const status = this.config.getProperty('status.status')!; - - const slash = new SlashCreator({ - applicationID: this.discord.client.user.id, - token: this.config.getProperty('token')!, - }); - - slash - .withServer(new GatewayServer((handler) => this.discord.client.on('interactionCreate', handler))) - .registerCommandsIn(join(process.cwd(), 'slash')) - .syncCommands({ deleteCommands: true, syncGuilds: true, syncPermissions: true }); - - this.discord.slashCreator = slash; - - for (const shard of this.discord.client.shards.values()) { - this.discord.client.editStatus(this.config.getProperty('status.presence') ?? 'online', { - name: status - .replace('$prefix$', prefixes[Math.floor(Math.random() * prefixes.length)]) - .replace('$guilds$', this.discord.client.guilds.size.toLocaleString()) - .replace('$shard$', `#${shard.id}`), - - type: statusType ?? 0, - }); - } - } -} diff --git a/src/main.ts b/src/main.ts deleted file mode 100644 index 08f11965..00000000 --- a/src/main.ts +++ /dev/null @@ -1,105 +0,0 @@ -/** - * Copyright (c) 2019-2021 Nino - * - * 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. - */ - -import './util/patches/RequirePatch'; -import 'reflect-metadata'; - -(require('@augu/dotenv') as typeof import('@augu/dotenv')).parse({ - populate: true, - delimiter: ',', - file: require('path').join(process.cwd(), '..', '.env'), - schema: { - NODE_ENV: { - oneOf: ['production', 'development'], - default: 'development', - type: 'string', - }, - }, -}); - -import { commitHash, version } from './util/Constants'; -import Discord from './components/Discord'; -import Sentry from './components/Sentry'; -import logger from './singletons/Logger'; -import app from './container'; -import Api from './api/API'; -import ts from 'typescript'; - -(async () => { - logger.info(`Loading Nino v${version} (${commitHash ?? ''})`); - logger.info(`-> TypeScript: ${ts.version}`); - logger.info(`-> Node.js: ${process.version}`); - if (process.env.REGION !== undefined) logger.info(`-> Region: ${process.env.REGION}`); - - try { - await app.load(); - await import('./util/patches/ErisPatch'); - await app.addComponent(Api); - } catch (ex) { - logger.fatal('Unable to load container'); - console.error(ex); - process.exit(1); - } - - logger.info('✔ Nino has started successfully'); - process.on('SIGINT', () => { - logger.warn('Received CTRL+C call!'); - - app.dispose(); - process.exit(0); - }); -})(); - -const ReconnectCodes = [ - 1001, // Going Away (re-connect now) - 1006, // Connection reset by peer -]; - -const OtherPossibleReconnectCodes = [ - 'WebSocket was closed before the connection was established', - "Server didn't acknowledge previous heartbeat, possible lost connection", -]; - -process.on('unhandledRejection', (error) => { - const sentry = app.$ref(Sentry); - if (error !== null || error !== undefined) { - logger.fatal('Received unhandled Promise rejection:', error); - if (error instanceof Error) sentry?.report(error); - } -}); - -process.on('uncaughtException', async (error) => { - const sentry = app.$ref(Sentry); - - if ((error as any).code !== undefined) { - if (ReconnectCodes.includes((error as any).code) || OtherPossibleReconnectCodes.includes(error.message)) { - logger.fatal('Disconnected due to peer to peer connection ended, restarting client...'); - - const discord = app.$ref(Discord); - discord.client.disconnect({ reconnect: false }); - await discord.client.connect(); - } - } else { - sentry?.report(error); - logger.fatal('Uncaught exception has occured\n', error); - } -}); diff --git a/src/migrations/1617170164138-initialization.ts b/src/migrations/1617170164138-initialization.ts deleted file mode 100644 index 6d074bd7..00000000 --- a/src/migrations/1617170164138-initialization.ts +++ /dev/null @@ -1,79 +0,0 @@ -/** - * Copyright (c) 2019-2021 Nino - * - * 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. - */ - -import { MigrationInterface, QueryRunner } from 'typeorm'; - -export class initialization1617170164138 implements MigrationInterface { - name = 'initialization1617170164138'; - - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query( - 'CREATE TABLE "automod" ("blacklistWords" text array NOT NULL, "blacklist" boolean NOT NULL DEFAULT false, "mentions" boolean NOT NULL DEFAULT false, "invites" boolean NOT NULL DEFAULT false, "dehoist" boolean NOT NULL DEFAULT false, "guild_id" character varying NOT NULL, "spam" boolean NOT NULL DEFAULT false, "raid" boolean NOT NULL DEFAULT false, CONSTRAINT "PK_8592ba75741fd8ff52adde5de53" PRIMARY KEY ("guild_id"))' - ); - await queryRunner.query("CREATE TYPE \"blacklists_type_enum\" AS ENUM('0', '1')"); - await queryRunner.query( - 'CREATE TABLE "blacklists" ("reason" character varying, "issuer" character varying NOT NULL, "type" "blacklists_type_enum" NOT NULL, "id" character varying NOT NULL, CONSTRAINT "PK_69894f41b74b226aae9ea763bc2" PRIMARY KEY ("id"))' - ); - await queryRunner.query( - "CREATE TYPE \"punishments_type_enum\" AS ENUM('warning.removed', 'voice.undeafen', 'warning.added', 'voice.unmute', 'voice.deafen', 'voice.mute', 'unmute', 'unban', 'kick', 'mute', 'ban')" - ); - await queryRunner.query( - 'CREATE TABLE "punishments" ("warnings" integer NOT NULL DEFAULT \'1\', "guild_id" character varying NOT NULL, "index" SERIAL NOT NULL, "soft" boolean NOT NULL DEFAULT false, "time" integer, "type" "punishments_type_enum" NOT NULL, CONSTRAINT "PK_b08854374ef88515861c1bf6cd8" PRIMARY KEY ("guild_id", "index"))' - ); - await queryRunner.query( - "CREATE TYPE \"cases_type_enum\" AS ENUM('warning.removed', 'voice.undeafen', 'warning.added', 'voice.unmute', 'voice.deafen', 'voice.mute', 'unmute', 'unban', 'kick', 'mute', 'ban')" - ); - await queryRunner.query( - 'CREATE TABLE "cases" ("moderator_id" character varying NOT NULL, "message_id" character varying, "victim_id" character varying NOT NULL, "guild_id" character varying NOT NULL, "reason" character varying, "index" integer NOT NULL, "type" "cases_type_enum" NOT NULL, "soft" boolean NOT NULL DEFAULT false, "time" integer, CONSTRAINT "PK_70fc7fe12ee1488af12aaea83af" PRIMARY KEY ("guild_id", "index"))' - ); - await queryRunner.query( - 'CREATE TABLE "guilds" ("modlog_channel_id" character varying DEFAULT null, "muted_role_id" character varying DEFAULT null, "prefixes" text array NOT NULL, "language" character varying NOT NULL DEFAULT \'en_US\', "guild_id" character varying NOT NULL, CONSTRAINT "PK_e8887ee637b1f465673e957dd0a" PRIMARY KEY ("guild_id"))' - ); - await queryRunner.query( - "CREATE TYPE \"logging_events_enum\" AS ENUM('voice_channel_switch', 'voice_channel_left', 'voice_channel_join', 'message_delete', 'message_update', 'settings_update')" - ); - await queryRunner.query( - 'CREATE TABLE "logging" ("ignoreChannels" text array NOT NULL DEFAULT \'{}\'::text[], "ignoreUsers" text array NOT NULL DEFAULT \'{}\'::text[], "channel_id" character varying, "enabled" boolean NOT NULL DEFAULT false, "events" "logging_events_enum" array NOT NULL DEFAULT \'{}\', "guild_id" character varying NOT NULL, CONSTRAINT "PK_cbd7eb1495206472bb71b7a6d68" PRIMARY KEY ("guild_id"))' - ); - await queryRunner.query( - 'CREATE TABLE "users" ("language" character varying NOT NULL DEFAULT \'en_US\', "prefixes" text array NOT NULL, "user_id" character varying NOT NULL, CONSTRAINT "PK_96aac72f1574b88752e9fb00089" PRIMARY KEY ("user_id"))' - ); - await queryRunner.query( - 'CREATE TABLE "warnings" ("guild_id" character varying NOT NULL, "reason" character varying, "amount" integer NOT NULL DEFAULT \'0\', "user_id" character varying NOT NULL, CONSTRAINT "PK_7a14eba00a6aaf0dc04f76aff02" PRIMARY KEY ("user_id"))' - ); - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query('DROP TABLE "warnings"'); - await queryRunner.query('DROP TABLE "users"'); - await queryRunner.query('DROP TABLE "logging"'); - await queryRunner.query('DROP TYPE "logging_events_enum"'); - await queryRunner.query('DROP TABLE "guilds"'); - await queryRunner.query('DROP TABLE "cases"'); - await queryRunner.query('DROP TYPE "cases_type_enum"'); - await queryRunner.query('DROP TABLE "punishments"'); - await queryRunner.query('DROP TYPE "punishments_type_enum"'); - await queryRunner.query('DROP TABLE "blacklists"'); - await queryRunner.query('DROP TYPE "blacklists_type_enum"'); - await queryRunner.query('DROP TABLE "automod"'); - } -} diff --git a/src/migrations/1617402812079-firstMigration.ts b/src/migrations/1617402812079-firstMigration.ts deleted file mode 100644 index a90745f9..00000000 --- a/src/migrations/1617402812079-firstMigration.ts +++ /dev/null @@ -1,55 +0,0 @@ -/** - * Copyright (c) 2019-2021 Nino - * - * 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. - */ - -import { MigrationInterface, QueryRunner } from 'typeorm'; - -export class firstMigration1617402812079 implements MigrationInterface { - name = 'firstMigration1617402812079'; - - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query('ALTER TABLE "automod" ADD "shortLinks" boolean NOT NULL DEFAULT false'); - await queryRunner.query('ALTER TYPE "logging_events_enum" RENAME TO "logging_events_enum_old"'); - await queryRunner.query( - "CREATE TYPE \"logging_events_enum\" AS ENUM('voice_channel_switch', 'voice_channel_left', 'voice_channel_join', 'message_delete', 'message_update')" - ); - await queryRunner.query('ALTER TABLE "logging" ALTER COLUMN "events" DROP DEFAULT'); - await queryRunner.query( - 'ALTER TABLE "logging" ALTER COLUMN "events" TYPE "logging_events_enum"[] USING "events"::"text"::"logging_events_enum"[]' - ); - await queryRunner.query('ALTER TABLE "logging" ALTER COLUMN "events" SET DEFAULT \'{}\''); - await queryRunner.query('DROP TYPE "logging_events_enum_old"'); - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query( - "CREATE TYPE \"logging_events_enum_old\" AS ENUM('voice_channel_switch', 'voice_channel_left', 'voice_channel_join', 'message_delete', 'message_update', 'settings_update')" - ); - await queryRunner.query('ALTER TABLE "logging" ALTER COLUMN "events" DROP DEFAULT'); - await queryRunner.query( - 'ALTER TABLE "logging" ALTER COLUMN "events" TYPE "logging_events_enum_old"[] USING "events"::"text"::"logging_events_enum_old"[]' - ); - await queryRunner.query('ALTER TABLE "logging" ALTER COLUMN "events" SET DEFAULT \'{}\''); - await queryRunner.query('DROP TYPE "logging_events_enum"'); - await queryRunner.query('ALTER TYPE "logging_events_enum_old" RENAME TO "logging_events_enum"'); - await queryRunner.query('ALTER TABLE "automod" DROP COLUMN "shortLinks"'); - } -} diff --git a/src/migrations/1618173354506-fixPrimaryColumnInWarnings.ts b/src/migrations/1618173354506-fixPrimaryColumnInWarnings.ts deleted file mode 100644 index c8d59d78..00000000 --- a/src/migrations/1618173354506-fixPrimaryColumnInWarnings.ts +++ /dev/null @@ -1,49 +0,0 @@ -/** - * Copyright (c) 2019-2021 Nino - * - * 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. - */ - -import { MigrationInterface, QueryRunner } from 'typeorm'; - -export class fixPrimaryColumnInWarnings1618173354506 implements MigrationInterface { - name = 'fixPrimaryColumnInWarnings1618173354506'; - - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query('ALTER TABLE "warnings" DROP CONSTRAINT "PK_7a14eba00a6aaf0dc04f76aff02"'); - await queryRunner.query( - 'ALTER TABLE "warnings" ADD CONSTRAINT "PK_cb17dc8ac1439c8d9bfb89ea41a" PRIMARY KEY ("user_id", "guild_id")' - ); - await queryRunner.query('ALTER TABLE "warnings" DROP CONSTRAINT "PK_cb17dc8ac1439c8d9bfb89ea41a"'); - await queryRunner.query( - 'ALTER TABLE "warnings" ADD CONSTRAINT "PK_acfe1e5e5e9ba6b9b0fa3f591fa" PRIMARY KEY ("guild_id")' - ); - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query('ALTER TABLE "warnings" DROP CONSTRAINT "PK_acfe1e5e5e9ba6b9b0fa3f591fa"'); - await queryRunner.query( - 'ALTER TABLE "warnings" ADD CONSTRAINT "PK_cb17dc8ac1439c8d9bfb89ea41a" PRIMARY KEY ("guild_id", "user_id")' - ); - await queryRunner.query('ALTER TABLE "warnings" DROP CONSTRAINT "PK_cb17dc8ac1439c8d9bfb89ea41a"'); - await queryRunner.query( - 'ALTER TABLE "warnings" ADD CONSTRAINT "PK_7a14eba00a6aaf0dc04f76aff02" PRIMARY KEY ("user_id")' - ); - } -} diff --git a/src/migrations/1618173954276-addIdPropToWarnings.ts b/src/migrations/1618173954276-addIdPropToWarnings.ts deleted file mode 100644 index 105df105..00000000 --- a/src/migrations/1618173954276-addIdPropToWarnings.ts +++ /dev/null @@ -1,43 +0,0 @@ -/** - * Copyright (c) 2019-2021 Nino - * - * 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. - */ - -import { MigrationInterface, QueryRunner } from 'typeorm'; - -export class addIdPropToWarnings1618173954276 implements MigrationInterface { - name = 'addIdPropToWarnings1618173954276'; - - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query('ALTER TABLE "warnings" ADD "id" SERIAL NOT NULL'); - await queryRunner.query('ALTER TABLE "warnings" DROP CONSTRAINT "PK_acfe1e5e5e9ba6b9b0fa3f591fa"'); - await queryRunner.query( - 'ALTER TABLE "warnings" ADD CONSTRAINT "PK_1a1c969d7e8d8aad2231021420f" PRIMARY KEY ("guild_id", "id")' - ); - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query('ALTER TABLE "warnings" DROP CONSTRAINT "PK_1a1c969d7e8d8aad2231021420f"'); - await queryRunner.query( - 'ALTER TABLE "warnings" ADD CONSTRAINT "PK_acfe1e5e5e9ba6b9b0fa3f591fa" PRIMARY KEY ("guild_id")' - ); - await queryRunner.query('ALTER TABLE "warnings" DROP COLUMN "id"'); - } -} diff --git a/src/migrations/1618174668865-snakeCaseColumnNames.ts b/src/migrations/1618174668865-snakeCaseColumnNames.ts deleted file mode 100644 index 7808317c..00000000 --- a/src/migrations/1618174668865-snakeCaseColumnNames.ts +++ /dev/null @@ -1,49 +0,0 @@ -/** - * Copyright (c) 2019-2021 Nino - * - * 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. - */ - -import { MigrationInterface, QueryRunner } from 'typeorm'; - -export class snakeCaseColumnNames1618174668865 implements MigrationInterface { - name = 'snakeCaseColumnNames1618174668865'; - - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query('ALTER TABLE "automod" DROP COLUMN "blacklistWords"'); - await queryRunner.query('ALTER TABLE "automod" DROP COLUMN "shortLinks"'); - await queryRunner.query('ALTER TABLE "logging" DROP COLUMN "ignoreChannels"'); - await queryRunner.query('ALTER TABLE "logging" DROP COLUMN "ignoreUsers"'); - await queryRunner.query('ALTER TABLE "automod" ADD "blacklist_words" text array NOT NULL DEFAULT \'{}\''); - await queryRunner.query('ALTER TABLE "automod" ADD "short_links" boolean NOT NULL DEFAULT false'); - await queryRunner.query('ALTER TABLE "logging" ADD "ignore_channels" text array NOT NULL DEFAULT \'{}\''); - await queryRunner.query('ALTER TABLE "logging" ADD "ignore_users" text array NOT NULL DEFAULT \'{}\''); - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query('ALTER TABLE "logging" DROP COLUMN "ignore_users"'); - await queryRunner.query('ALTER TABLE "logging" DROP COLUMN "ignore_channels"'); - await queryRunner.query('ALTER TABLE "automod" DROP COLUMN "short_links"'); - await queryRunner.query('ALTER TABLE "automod" DROP COLUMN "blacklist_words"'); - await queryRunner.query('ALTER TABLE "logging" ADD "ignoreUsers" text array NOT NULL DEFAULT \'{}\''); - await queryRunner.query('ALTER TABLE "logging" ADD "ignoreChannels" text array NOT NULL DEFAULT \'{}\''); - await queryRunner.query('ALTER TABLE "automod" ADD "shortLinks" boolean NOT NULL DEFAULT false'); - await queryRunner.query('ALTER TABLE "automod" ADD "blacklistWords" text array NOT NULL'); - } -} diff --git a/src/migrations/1621720227973-addNewLogTypes.ts b/src/migrations/1621720227973-addNewLogTypes.ts deleted file mode 100644 index 20297262..00000000 --- a/src/migrations/1621720227973-addNewLogTypes.ts +++ /dev/null @@ -1,60 +0,0 @@ -/** - * Copyright (c) 2019-2021 Nino - * - * 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. - */ - -import { MigrationInterface, QueryRunner } from 'typeorm'; -import { LoggingEvents } from '../entities/LoggingEntity'; - -const NewTypes = ['voice_member_muted', 'voice_member_deafened']; - -export class addNewLogTypes1621720227973 implements MigrationInterface { - name = 'addNewLogTypes1621720227973'; - - public async up(runner: QueryRunner) { - await runner.query('ALTER TYPE "logging_events_enum" RENAME TO "logging_events_enum_old";'); - await runner.query( - `CREATE TYPE "logging_events_enum" AS ENUM(${Object.values(LoggingEvents) - .map((value) => `'${value}'`) - .join(', ')});` - ); - await runner.query('ALTER TABLE "logging" ALTER COLUMN "events" DROP DEFAULT;'); - await runner.query( - 'ALTER TABLE "logging" ALTER COLUMN "events" TYPE "logging_events_enum"[] USING "events"::"text"::"logging_events_enum"[];' - ); - await runner.query('ALTER TABLE "logging" ALTER COLUMN "events" SET DEFAULT \'{}\';'); - await runner.query('DROP TYPE "logging_events_enum_old";'); - } - - public async down(runner: QueryRunner) { - await runner.query( - `CREATE TYPE "logging_events_enum_old" AS ENUM(${Object.values(LoggingEvents) - .filter((v) => !NewTypes.includes(v)) - .map((value) => `'${value}'`) - .join(', ')});` - ); - await runner.query('ALTER TABLE "logging" ALTER COLUMN "events" DROP DEFAULT;'); - await runner.query( - 'ALTER TABLE "logging" ALTER COLUMN "events" TYPE "logging_events_enum_old"[] USING "events"::"text"::"logging_events_enum_old"[];' - ); - await runner.query('DROP TYPE "logging_events_enum";'); - await runner.query('ALTER TYPE "logging_events_enum_old" RENAME TO "logging_events_enum";'); - } -} diff --git a/src/migrations/1621895533962-fixCaseTimeType.ts b/src/migrations/1621895533962-fixCaseTimeType.ts deleted file mode 100644 index 4d08b244..00000000 --- a/src/migrations/1621895533962-fixCaseTimeType.ts +++ /dev/null @@ -1,37 +0,0 @@ -/** - * Copyright (c) 2019-2021 Nino - * - * 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. - */ - -import { MigrationInterface, QueryRunner } from 'typeorm'; - -export class fixCaseTimeType1621895533962 implements MigrationInterface { - name = 'fixCaseTimeType1621895533962'; - - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query('ALTER TABLE "cases" DROP COLUMN "time"'); - await queryRunner.query('ALTER TABLE "cases" ADD "time" bigint'); - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query('ALTER TABLE "cases" DROP COLUMN "time"'); - await queryRunner.query('ALTER TABLE "cases" ADD "time" integer'); - } -} diff --git a/src/migrations/1625456992070-fixPunishmentIndex.ts b/src/migrations/1625456992070-fixPunishmentIndex.ts deleted file mode 100644 index 7dff8b5e..00000000 --- a/src/migrations/1625456992070-fixPunishmentIndex.ts +++ /dev/null @@ -1,71 +0,0 @@ -/** - * Copyright (c) 2019-2021 Nino - * - * 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. - */ - -import { MigrationInterface, QueryRunner } from 'typeorm'; - -export class fixPunishmentIndex1625456992070 implements MigrationInterface { - name = 'fixPunishmentIndex1625456992070'; - - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query('ALTER TABLE "punishments" ALTER COLUMN "index" DROP DEFAULT'); - await queryRunner.query('DROP SEQUENCE "punishments_index_seq"'); - await queryRunner.query('ALTER TYPE "punishments_type_enum" RENAME TO "punishments_type_enum_old"'); - await queryRunner.query( - "CREATE TYPE \"punishments_type_enum\" AS ENUM('warning.removed', 'voice.undeafen', 'warning.added', 'voice.unmute', 'voice.deafen', 'voice.mute', 'unmute', 'unban', 'kick', 'mute', 'ban')" - ); - await queryRunner.query( - 'ALTER TABLE "punishments" ALTER COLUMN "type" TYPE "punishments_type_enum" USING "type"::"text"::"punishments_type_enum"' - ); - await queryRunner.query('DROP TYPE "punishments_type_enum_old"'); - await queryRunner.query('ALTER TYPE "cases_type_enum" RENAME TO "cases_type_enum_old"'); - await queryRunner.query( - "CREATE TYPE \"cases_type_enum\" AS ENUM('warning.removed', 'voice.undeafen', 'warning.added', 'voice.unmute', 'voice.deafen', 'voice.mute', 'unmute', 'unban', 'kick', 'mute', 'ban')" - ); - await queryRunner.query( - 'ALTER TABLE "cases" ALTER COLUMN "type" TYPE "cases_type_enum" USING "type"::"text"::"cases_type_enum"' - ); - await queryRunner.query('DROP TYPE "cases_type_enum_old"'); - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query( - "CREATE TYPE \"cases_type_enum_old\" AS ENUM('warning.removed', 'voice.undeafen', 'warning.added', 'voice.unmute', 'voice.deafen', 'voice.kick', 'voice.mute', 'unmute', 'unban', 'kick', 'mute', 'ban')" - ); - await queryRunner.query( - 'ALTER TABLE "cases" ALTER COLUMN "type" TYPE "cases_type_enum_old" USING "type"::"text"::"cases_type_enum_old"' - ); - await queryRunner.query('DROP TYPE "cases_type_enum"'); - await queryRunner.query('ALTER TYPE "cases_type_enum_old" RENAME TO "cases_type_enum"'); - await queryRunner.query( - "CREATE TYPE \"punishments_type_enum_old\" AS ENUM('warning.removed', 'voice.undeafen', 'warning.added', 'voice.unmute', 'voice.deafen', 'voice.kick', 'voice.mute', 'unmute', 'unban', 'kick', 'mute', 'ban')" - ); - await queryRunner.query( - 'ALTER TABLE "punishments" ALTER COLUMN "type" TYPE "punishments_type_enum_old" USING "type"::"text"::"punishments_type_enum_old"' - ); - await queryRunner.query('DROP TYPE "punishments_type_enum"'); - await queryRunner.query('ALTER TYPE "punishments_type_enum_old" RENAME TO "punishments_type_enum"'); - await queryRunner.query('CREATE SEQUENCE "punishments_index_seq" OWNED BY "punishments"."index"'); - await queryRunner.query( - 'ALTER TABLE "punishments" ALTER COLUMN "index" SET DEFAULT nextval(\'punishments_index_seq\')' - ); - } -} diff --git a/src/migrations/1625457655665-addDaysColumn.ts b/src/migrations/1625457655665-addDaysColumn.ts deleted file mode 100644 index 5a0aaab2..00000000 --- a/src/migrations/1625457655665-addDaysColumn.ts +++ /dev/null @@ -1,73 +0,0 @@ -/** - * Copyright (c) 2019-2021 Nino - * - * 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. - */ - -import { MigrationInterface, QueryRunner } from 'typeorm'; - -export class addDaysColumn1625457655665 implements MigrationInterface { - name = 'addDaysColumn1625457655665'; - - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query('ALTER TABLE "punishments" ADD "days" integer'); - await queryRunner.query('ALTER TABLE "punishments" ALTER COLUMN "index" DROP DEFAULT'); - await queryRunner.query('DROP SEQUENCE IF EXISTS "punishments_index_seq"'); - await queryRunner.query('ALTER TYPE "punishments_type_enum" RENAME TO "punishments_type_enum_old"'); - await queryRunner.query( - "CREATE TYPE \"punishments_type_enum\" AS ENUM('warning.removed', 'voice.undeafen', 'warning.added', 'voice.unmute', 'voice.deafen', 'voice.mute', 'unmute', 'unban', 'kick', 'mute', 'ban')" - ); - await queryRunner.query( - 'ALTER TABLE "punishments" ALTER COLUMN "type" TYPE "punishments_type_enum" USING "type"::"text"::"punishments_type_enum"' - ); - await queryRunner.query('DROP TYPE "punishments_type_enum_old"'); - await queryRunner.query('ALTER TYPE "cases_type_enum" RENAME TO "cases_type_enum_old"'); - await queryRunner.query( - "CREATE TYPE \"cases_type_enum\" AS ENUM('warning.removed', 'voice.undeafen', 'warning.added', 'voice.unmute', 'voice.deafen', 'voice.mute', 'unmute', 'unban', 'kick', 'mute', 'ban')" - ); - await queryRunner.query( - 'ALTER TABLE "cases" ALTER COLUMN "type" TYPE "cases_type_enum" USING "type"::"text"::"cases_type_enum"' - ); - await queryRunner.query('DROP TYPE "cases_type_enum_old"'); - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query( - "CREATE TYPE \"cases_type_enum_old\" AS ENUM('warning.removed', 'voice.undeafen', 'warning.added', 'voice.unmute', 'voice.deafen', 'voice.kick', 'voice.mute', 'unmute', 'unban', 'kick', 'mute', 'ban')" - ); - await queryRunner.query( - 'ALTER TABLE "cases" ALTER COLUMN "type" TYPE "cases_type_enum_old" USING "type"::"text"::"cases_type_enum_old"' - ); - await queryRunner.query('DROP TYPE "cases_type_enum"'); - await queryRunner.query('ALTER TYPE "cases_type_enum_old" RENAME TO "cases_type_enum"'); - await queryRunner.query( - "CREATE TYPE \"punishments_type_enum_old\" AS ENUM('warning.removed', 'voice.undeafen', 'warning.added', 'voice.unmute', 'voice.deafen', 'voice.kick', 'voice.mute', 'unmute', 'unban', 'kick', 'mute', 'ban')" - ); - await queryRunner.query( - 'ALTER TABLE "punishments" ALTER COLUMN "type" TYPE "punishments_type_enum_old" USING "type"::"text"::"punishments_type_enum_old"' - ); - await queryRunner.query('DROP TYPE "punishments_type_enum"'); - await queryRunner.query('ALTER TYPE "punishments_type_enum_old" RENAME TO "punishments_type_enum"'); - await queryRunner.query('CREATE SEQUENCE "punishments_index_seq" OWNED BY "punishments"."index"'); - await queryRunner.query( - 'ALTER TABLE "punishments" ALTER COLUMN "index" SET DEFAULT nextval(\'punishments_index_seq\')' - ); - await queryRunner.query('ALTER TABLE "punishments" DROP COLUMN "days"'); - } -} diff --git a/src/migrations/1625605609322-addWhitelistChannelsAutomod.ts b/src/migrations/1625605609322-addWhitelistChannelsAutomod.ts deleted file mode 100644 index 7b8e992b..00000000 --- a/src/migrations/1625605609322-addWhitelistChannelsAutomod.ts +++ /dev/null @@ -1,75 +0,0 @@ -/** - * Copyright (c) 2019-2021 Nino - * - * 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. - */ - -import { MigrationInterface, QueryRunner } from 'typeorm'; - -export class addWhitelistChannelsAutomod1625605609322 implements MigrationInterface { - name = 'addWhitelistChannelsAutomod1625605609322'; - - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query( - 'ALTER TABLE "automod" ADD "whitelist_channels_during_raid" text array NOT NULL DEFAULT \'{}\'' - ); - await queryRunner.query('ALTER TABLE "punishments" ALTER COLUMN "index" DROP DEFAULT'); - await queryRunner.query('DROP SEQUENCE IF EXISTS "punishments_index_seq"'); - await queryRunner.query('ALTER TYPE "punishments_type_enum" RENAME TO "punishments_type_enum_old"'); - await queryRunner.query( - "CREATE TYPE \"punishments_type_enum\" AS ENUM('warning.removed', 'voice.undeafen', 'warning.added', 'voice.unmute', 'voice.deafen', 'voice.mute', 'unmute', 'unban', 'kick', 'mute', 'ban')" - ); - await queryRunner.query( - 'ALTER TABLE "punishments" ALTER COLUMN "type" TYPE "punishments_type_enum" USING "type"::"text"::"punishments_type_enum"' - ); - await queryRunner.query('DROP TYPE "punishments_type_enum_old"'); - await queryRunner.query('ALTER TYPE "cases_type_enum" RENAME TO "cases_type_enum_old"'); - await queryRunner.query( - "CREATE TYPE \"cases_type_enum\" AS ENUM('warning.removed', 'voice.undeafen', 'warning.added', 'voice.unmute', 'voice.deafen', 'voice.mute', 'unmute', 'unban', 'kick', 'mute', 'ban')" - ); - await queryRunner.query( - 'ALTER TABLE "cases" ALTER COLUMN "type" TYPE "cases_type_enum" USING "type"::"text"::"cases_type_enum"' - ); - await queryRunner.query('DROP TYPE "cases_type_enum_old"'); - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query( - "CREATE TYPE \"cases_type_enum_old\" AS ENUM('warning.removed', 'voice.undeafen', 'warning.added', 'voice.unmute', 'voice.deafen', 'voice.kick', 'voice.mute', 'unmute', 'unban', 'kick', 'mute', 'ban')" - ); - await queryRunner.query( - 'ALTER TABLE "cases" ALTER COLUMN "type" TYPE "cases_type_enum_old" USING "type"::"text"::"cases_type_enum_old"' - ); - await queryRunner.query('DROP TYPE "cases_type_enum"'); - await queryRunner.query('ALTER TYPE "cases_type_enum_old" RENAME TO "cases_type_enum"'); - await queryRunner.query( - "CREATE TYPE \"punishments_type_enum_old\" AS ENUM('warning.removed', 'voice.undeafen', 'warning.added', 'voice.unmute', 'voice.deafen', 'voice.kick', 'voice.mute', 'unmute', 'unban', 'kick', 'mute', 'ban')" - ); - await queryRunner.query( - 'ALTER TABLE "punishments" ALTER COLUMN "type" TYPE "punishments_type_enum_old" USING "type"::"text"::"punishments_type_enum_old"' - ); - await queryRunner.query('DROP TYPE "punishments_type_enum"'); - await queryRunner.query('ALTER TYPE "punishments_type_enum_old" RENAME TO "punishments_type_enum"'); - await queryRunner.query('CREATE SEQUENCE "punishments_index_seq" OWNED BY "punishments"."index"'); - await queryRunner.query( - 'ALTER TABLE "punishments" ALTER COLUMN "index" SET DEFAULT nextval(\'punishments_index_seq\')' - ); - await queryRunner.query('ALTER TABLE "automod" DROP COLUMN "whitelist_channels_during_raid"'); - } -} diff --git a/src/services/AutomodService.ts b/src/services/AutomodService.ts deleted file mode 100644 index af63c32d..00000000 --- a/src/services/AutomodService.ts +++ /dev/null @@ -1,94 +0,0 @@ -/** - * Copyright (c) 2019-2021 Nino - * - * 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. - */ - -import { ComponentOrServiceHooks, Inject, Service } from '@augu/lilith'; -import type { Member, Message, TextChannel, User } from 'eris'; -import type { Automod } from '../structures'; -import { Collection } from '@augu/collections'; -import { Logger } from 'tslog'; -import { join } from 'path'; - -@Service({ - priority: 1, - children: join(process.cwd(), 'automod'), - name: 'automod', -}) -export default class AutomodService extends Collection implements ComponentOrServiceHooks { - @Inject - private logger!: Logger; - - onChildLoad(automod: Automod) { - this.logger.info(`✔ Loaded automod ${automod.name}!`); - this.set(automod.name, automod); - } - - run(type: 'userUpdate', user: User): Promise; - run(type: 'memberNick', member: Member): Promise; - run(type: 'memberJoin', member: Member): Promise; - run(type: 'message', msg: Message): Promise; - async run(type: string, ...args: any[]) { - switch (type) { - case 'userUpdate': { - const automod = this.filter((am) => am.onUserUpdate !== undefined); - for (const am of automod) { - const res = await am.onUserUpdate!(args[0]); - if (res === true) return true; - } - - return false; - } - - case 'memberNick': { - const automod = this.filter((am) => am.onMemberNickUpdate !== undefined); - for (const am of automod) { - const res = await am.onMemberNickUpdate!(args[0]); - if (res === true) return true; - } - - return false; - } - - case 'memberJoin': { - const automod = this.filter((am) => am.onMemberJoin !== undefined); - for (const am of automod) { - const res = await am.onMemberJoin!(args[0]); - if (res === true) return true; - } - - return false; - } - - case 'message': { - const automod = this.filter((am) => am.onMessage !== undefined); - for (const am of automod) { - const res = await am.onMessage!(args[0]); - if (res === true) return true; - } - - return false; - } - - default: - return true; - } - } -} diff --git a/src/services/BotlistService.ts b/src/services/BotlistService.ts deleted file mode 100644 index cb34953e..00000000 --- a/src/services/BotlistService.ts +++ /dev/null @@ -1,250 +0,0 @@ -/** - * Copyright (c) 2019-2021 Nino - * - * 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. - */ - -/* eslint-disable camelcase */ - -import { Service, Inject } from '@augu/lilith'; -import { HttpClient } from '@augu/orchid'; -import { Logger } from 'tslog'; -import Discord from '../components/Discord'; -import Config from '../components/Config'; - -@Service({ - priority: 1, - name: 'botlists', -}) -export default class BotlistsService { - @Inject - private readonly discord!: Discord; - - @Inject - private readonly logger!: Logger; - - @Inject - private readonly config!: Config; - - @Inject - private readonly http!: HttpClient; - - #interval?: NodeJS.Timer; - - async load() { - const botlists = this.config.getProperty('botlists'); - if (botlists === undefined) { - this.logger.warn("`botlists` is missing, don't need to add it if running privately."); - return Promise.resolve(); - } - - this.logger.info('Built scheduler for posting to botlists!'); - this.#interval = setInterval(this.post.bind(this), 86400000).unref(); - } - - dispose() { - if (this.#interval) clearInterval(this.#interval); - } - - async post() { - const list: { - name: 'Discord Services' | 'Discord Boats' | 'Discord Bots' | 'top.gg' | 'Delly' | 'Bots for Discord'; - success: boolean; - data: Record; - }[] = []; - - let success = 0; - let errored = 0; - const botlists = this.config.getProperty('botlists')!; - - if (botlists === undefined) return; - - if (botlists.dservices !== undefined) { - this.logger.info('Found Discord Services token, now posting...'); - - await this.http - .request({ - url: `https://api.discordservices.net/bot/${this.discord.client.user.id}/stats`, - method: 'POST', - data: { - server_count: this.discord.client.guilds.size, - }, - headers: { - 'Content-Type': 'application/json', - Authorization: botlists.dservices, - }, - }) - .then((res) => { - res.statusCode === 200 ? success++ : errored++; - list.push({ - name: 'Discord Services', - success: res.statusCode === 200, - data: res.json(), - }); - }) - .catch((ex) => this.logger.warn('Unable to parse JSON [discordservices.net]:', ex)); - } - - if (botlists.dboats !== undefined) { - this.logger.info('Found Discord Boats token, now posting...'); - - await this.http - .request({ - data: { - server_count: this.discord.client.guilds.size, - }, - method: 'POST', - url: `https://discord.boats/api/bot/${this.discord.client.user.id}`, - headers: { - 'Content-Type': 'application/json', - Authorization: botlists.dboats, - }, - }) - .then((res) => { - res.statusCode === 200 ? success++ : errored++; - list.push({ - name: 'Discord Boats', - success: res.statusCode === 200, - data: res.json(), - }); - }) - .catch((ex) => this.logger.warn('Unable to parse JSON [discord.boats]:', ex)); - } - - if (botlists.dbots !== undefined) { - this.logger.info('Found Discord Bots token, now posting...'); - - await this.http - .request({ - url: `https://discord.bots.gg/api/v1/bots/${this.discord.client.user.id}/stats`, - method: 'POST', - data: { - shardCount: this.discord.client.shards.size, - guildCount: this.discord.client.guilds.size, - }, - headers: { - 'Content-Type': 'application/json', - Authorization: botlists.dbots, - }, - }) - .then((res) => { - res.statusCode === 200 ? success++ : errored++; - list.push({ - name: 'Discord Bots', - success: res.statusCode === 200, - data: res.json(), - }); - }) - .catch((ex) => this.logger.warn('Unable to parse JSON [discord.bots.gg]:', ex)); - } - - if (botlists.topgg !== undefined) { - this.logger.info('Found top.gg token, now posting...'); - - await this.http - .request({ - url: `https://top.gg/api/bots/${this.discord.client.user.id}/stats`, - method: 'POST', - data: { - server_count: this.discord.client.guilds.size, - shard_count: this.discord.client.shards.size, - }, - headers: { - 'Content-Type': 'application/json', - Authorization: botlists.topgg, - }, - }) - .then((res) => { - res.statusCode === 200 ? success++ : errored++; - list.push({ - name: 'top.gg', - success: res.statusCode === 200, - data: res.json(), - }); - }) - .catch((ex) => this.logger.warn('Unable to parse JSON [top.gg]:', ex)); - } - - // Ice is a cute boyfriend btw <3 - if (botlists.delly !== undefined) { - this.logger.info('Found Discord Extreme List token, now posting...'); - - await this.http - .request({ - url: `https://api.discordextremelist.xyz/v2/bot/${this.discord.client.user.id}/stats`, - method: 'POST', - data: { - guildCount: this.discord.client.guilds.size, - shardCount: this.discord.client.shards.size, - }, - headers: { - 'Content-Type': 'application/json', - Authorization: botlists.delly, - }, - }) - .then((res) => { - res.statusCode === 200 ? success++ : errored++; - list.push({ - name: 'Delly', - success: res.statusCode === 200, - data: res.json(), - }); - }) - .catch((ex) => this.logger.warn('Unable to parse JSON [Delly]:', ex)); - } - - if (botlists.bfd !== undefined) { - this.logger.info('Found Bots for Discord token, now posting...'); - - await this.http - .request({ - method: 'POST', - url: `https://discords.com/bots/api/bot/${this.discord.client.user.id}`, - data: { - server_count: this.discord.client.guilds.size, - }, - headers: { - 'Content-Type': 'application/json', - Authorization: botlists.bfd, - }, - }) - .then((res) => { - res.statusCode === 200 ? success++ : errored++; - list.push({ - name: 'Bots for Discord', - success: res.statusCode === 200, - data: res.json(), - }); - }) - .catch((ex) => this.logger.warn('Unable to parse JSON [Bots for Discord]:', ex)); - } - - const successRate = ((errored / success) * 100).toFixed(2); - this.logger.info( - [ - `ℹ️ Successfully posted to ${list.length} botlists with a success rate of ${successRate}%`, - 'Serialized output will be displayed:', - ].join('\n') - ); - - for (const botlist of list) { - this.logger.info(`${botlist.success ? '✔' : '❌'} ${botlist.name}`, botlist.data); - } - } -} diff --git a/src/services/CommandService.ts b/src/services/CommandService.ts deleted file mode 100644 index 27825b63..00000000 --- a/src/services/CommandService.ts +++ /dev/null @@ -1,352 +0,0 @@ -/** - * Copyright (c) 2019-2021 Nino - * - * 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. - */ - -import { EmbedBuilder, CommandMessage } from '../structures'; -import type { Message, TextChannel } from 'eris'; -import { Service, Inject } from '@augu/lilith'; -import LocalizationService from './LocalizationService'; -import type NinoCommand from '../structures/Command'; -import { PrismaClient } from '.prisma/client'; -import AutomodService from './AutomodService'; -import { Collection } from '@augu/collections'; -import Subcommand from '../structures/Subcommand'; -import Prometheus from '../components/Prometheus'; -import { Logger } from 'tslog'; -import Database from '../components/Database'; -import { join } from 'path'; -import Discord from '../components/Discord'; -import Config from '../components/Config'; -import Sentry from '../components/Sentry'; - -const FLAG_REGEX = /(?:--?|—)([\w]+)(=?(\w+|['"].*['"]))?/gi; - -@Service({ - priority: 1, - children: join(process.cwd(), 'commands'), - name: 'commands', -}) -export default class CommandService extends Collection { - public commandsExecuted: number = 0; - public messagesSeen: number = 0; - public cooldowns: Collection> = new Collection(); - - @Inject - private readonly config!: Config; - - @Inject - private readonly logger!: Logger; - - @Inject - private readonly discord!: Discord; - - @Inject - private readonly database!: Database; - - @Inject - private readonly prometheus!: Prometheus; - - @Inject - private readonly automod!: AutomodService; - - @Inject - private readonly localization!: LocalizationService; - - @Inject - private readonly sentry?: Sentry; - - @Inject - private readonly prisma!: PrismaClient; - - onChildLoad(command: NinoCommand) { - if (!command.name) { - this.logger.warn(`Unfinished command: ${command.constructor.name}`); - return; - } - - this.logger.info(`✔ Loaded command ${command.name}!`); - this.set(command.name, command); - } - - async handleCommand(msg: Message) { - this.prometheus.messagesSeen?.inc(); - this.messagesSeen++; - - if ((await this.automod.run('message', msg)) === true) return; - - if (msg.author.bot) return; - if (![0, 5].includes(msg.channel.type)) return; - - const settings = await this.prisma.guild.findFirst({ - where: { - guildId: msg.guildID, - }, - }); - - let userSettings = await this.prisma.user.findFirst({ - where: { - userId: msg.author.id, - }, - }); - - if (userSettings === null) { - userSettings = await this.prisma.user.create({ - data: { - language: 'en_US', - prefixes: [], - userId: msg.author.id, - }, - }); - } - - const _prefixes = ([] as string[]) - .concat(settings!.prefixes, userSettings.prefixes, this.config.getProperty('prefixes')!) - .filter(Boolean); - - const mentionRegex = this.discord.mentionRegex ?? new RegExp(`<@!?${this.discord.client.user.id}> `); - const mention = mentionRegex.exec(msg.content); - if (mention !== null) _prefixes.push(`${mention}`); - - // remove duplicates - if (this.discord.client.user.id === '531613242473054229') _prefixes.push('nino '); - - // Removes any duplicates - const prefixes = [...new Set(_prefixes)]; - const prefix = prefixes.find((prefix) => msg.content.startsWith(prefix)); - if (prefix === undefined) return; - - let rawArgs = msg.content.slice(prefix.length).trim().split(/ +/g); - const name = rawArgs.shift()!; - const command = this.find((command) => command.name === name || command.aliases.includes(name)); - - if (command === null) return; - - // Check global ban list - const guildBan = await this.prisma.globalBans.findFirst({ - where: { - id: msg.guildID, - }, - }); - - if (guildBan !== null) { - const issuer = this.discord.client.users.get(guildBan.issuer) ?? { - username: 'Unknown User', - discriminator: '0000', - id: '0', - }; - - const embed = EmbedBuilder.create() - .setTitle(`Guild ${msg.channel.guild.name} is globally banned!`) - .setDescription([ - `This guild was detected as a non-safe guild by **${issuer.username}#${issuer.discriminator}**`, - `I think... their reasoning was **${guildBan.reason ?? '(no reason provided)'}**!`, - '', - issuer.id === '0' - ? 'Unfortunately, the issuer is not available at this moment.' - : `You can contact <@${issuer.id}> in the Noelware server under <#824071651486335036>!`, - issuer.id !== '0' ? 'https://discord.gg/ATmjFH9kMH' : '', - ]) - .setFooter('I will now leave this guild, ohayo!'); - - await msg.channel.createMessage({ embeds: [embed.build()] }); - await msg.channel.guild.leave(); - return; - } - - // Check if the user was globally banned - const userBan = await this.prisma.globalBans.findFirst({ - where: { - id: msg.author.id, - }, - }); - - if (userBan !== null) { - const issuer = this.discord.client.users.get(userBan.issuer) ?? { - username: 'Unknown User', - discriminator: '0000', - id: '0', - }; - - const embed = EmbedBuilder.create() - .setTitle(`Oh... ${msg.author.tag}, you've been globally banned. :<`) - .setDescription([ - `You were globally banned by **${issuer.username}#${issuer.discriminator}**`, - `I think... their reasoning was **${userBan.reason ?? '(no reason provided)'}**!`, - '', - issuer.id === '0' - ? 'Unfortunately, the issuer is not available at this moment.' - : `You can contact <@${issuer.id}> in the Noelware server under <#824071651486335036>!`, - issuer.id !== '0' ? 'https://discord.gg/ATmjFH9kMH' : '', - ]); - - await msg.channel.createMessage({ embeds: [embed.build()] }); - return; - } - - const locale = this.localization.get(settings!.language, userSettings.language); - // @ts-ignore - const message = new CommandMessage(msg, locale, settings!, userSettings); - app.addInjections(message); - - const owners = this.config.getProperty('owners') ?? []; - if (command.ownerOnly && !owners.includes(msg.author.id)) - return message.reply(`Command **${command.name}** is a developer-only command, nice try...`); - - // Check for permissions of Nino - if (command.botPermissions.length) { - const permissions = msg.channel.permissionsOf(this.discord.client.user.id); - const missing = command.botPermissions.filter((perm) => !permissions.has(perm)); - - if (missing.length > 0) return message.reply(`I am missing the following permissions: **${missing.join(', ')}**`); - } - - // Check for the user's permissions - if (command.userPermissions.length) { - const permissions = msg.channel.permissionsOf(msg.author.id); - const missing = command.userPermissions.filter((perm) => !permissions.has(perm)); - - if (missing.length > 0 && !owners.includes(msg.author.id)) - return message.reply(`You are missing the following permission: **${missing.join(', ')}**`); - } - - // Cooldowns - if (!this.cooldowns.has(command.name)) this.cooldowns.set(command.name, new Collection()); - - const now = Date.now(); - const timestamps = this.cooldowns.get(command.name)!; - const amount = command.cooldown * 1000; - - if (!owners.includes(msg.author.id) && timestamps.has(msg.author.id)) { - const time = timestamps.get(msg.author.id)! + amount; - if (now < time) { - const left = (time - now) / 1000; - return message.reply(`Please wait **${left.toFixed()}** seconds before executing this command.`); - } - } - - timestamps.set(msg.author.id, now); - setTimeout(() => timestamps.delete(msg.author.id), amount); - - // Figure out the subcommand - let methodName = 'run'; - let subcommand: Subcommand | undefined = undefined; - for (const arg of rawArgs) { - if (command.subcommands.length > 0) { - if (command.subcommands.find((r) => r.aliases.includes(arg)) !== undefined) { - subcommand = command.subcommands.find((r) => r.aliases.includes(arg))!; - methodName = subcommand.name; - break; - } - - if (command.subcommands.find((r) => r.name === arg) !== undefined) { - subcommand = command.subcommands.find((r) => r.name === arg)!; - methodName = subcommand.name; - break; - } - } - } - - if (subcommand !== undefined) rawArgs.shift(); - - message['_flags'] = this.parseFlags(rawArgs.join(' ')); - if (command.name !== 'eval') { - rawArgs = rawArgs.filter((arg) => !FLAG_REGEX.test(arg)); - } - - if (subcommand !== undefined) { - if (subcommand.permissions !== undefined) { - const perms = msg.channel.permissionsOf(msg.author.id); - if (!perms.has(subcommand.permissions)) - return message.reply(`You are missing the **${subcommand.permissions}** permission.`); - } - } - - try { - const executor = Reflect.get(command, methodName); - if (typeof executor !== 'function') - throw new SyntaxError( - `${subcommand ? 'Subc' : 'C'}ommand "${subcommand ? methodName : command.name}" was not a function.` - ); - - this.prometheus.commandsExecuted?.labels(command.name).inc(); - this.commandsExecuted++; - await executor.call(command, message, rawArgs); - this.logger.info( - `Command "${command.name}" has been ran by ${msg.author.username}#${msg.author.discriminator} in guild ${msg.channel.guild.name} (${msg.channel.guild.id})` - ); - } catch (ex) { - const _owners = await Promise.all( - owners.map((id) => { - const user = this.discord.client.users.get(id); - if (user === undefined) return this.discord.client.getRESTUser(id); - else return Promise.resolve(user); - }) - ); - - const contact = _owners - .map((r, index) => `${index + 1 === owners.length ? 'or ' : ''}**${r.username}#${r.discriminator}**`) - .join(', '); - - const codeblock = - process.env.NODE_ENV === 'development' - ? ['```js', (ex as Error).stack ?? '// (... no stacktrace ...)', '```'] - : []; - - const embed = new EmbedBuilder() - .setColor(0xdaa2c6) - .setDescription([ - `${ - subcommand !== undefined - ? `Subcommand **${methodName}** (parent **${command.name}**)` - : `Command **${command.name}**` - } has failed to execute.`, - `If this is a re-occuring issue, contact ${contact} at , under the <#824071651486335036> channel.`, - ...codeblock, - ]) - .build(); - - await msg.channel.createMessage({ embed }); - this.logger.error( - `${subcommand !== undefined ? `Subcommand ${methodName}` : `Command ${command.name}`} has failed to execute:`, - ex - ); - - this.sentry?.report(ex as Error); - } - } - - async onSlashMessage(message: Message) { - // todo: this - } - - // credit for regex: Ice <3 - private parseFlags(content: string): Record { - const record: Record = {}; - content.replaceAll(FLAG_REGEX, (_, key: string, value: string) => { - record[key.trim()] = value ? value.replaceAll(/(^[='"]+|['"]+$)/g, '').trim() : true; - return value; - }); - - // keep it immutable so - // the application doesn't mutate its state - return Object.freeze(record); - } -} diff --git a/src/services/ListenerService.ts b/src/services/ListenerService.ts deleted file mode 100644 index 5ed6b32f..00000000 --- a/src/services/ListenerService.ts +++ /dev/null @@ -1,45 +0,0 @@ -/** - * Copyright (c) 2019-2021 Nino - * - * 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. - */ - -import { Inject, Service } from '@augu/lilith'; -import { Collection } from '@augu/collections'; -import { firstUpper } from '@augu/utils'; -import { Logger } from 'tslog'; -import { join } from 'path'; - -@Service({ - priority: 0, - children: join(process.cwd(), 'listeners'), - name: 'listeners', -}) -// a noop service to register all listeners -export default class ListenerService extends Collection { - @Inject - private readonly logger!: Logger; - - onChildLoad(listener: any) { - const name = firstUpper(listener.constructor.name.replace('Listener', '')); - this.logger.info(`Registered listener ${listener.constructor.name}`); - - this.set(name, listener); - } -} diff --git a/src/services/LocalizationService.ts b/src/services/LocalizationService.ts deleted file mode 100644 index 43433eb7..00000000 --- a/src/services/LocalizationService.ts +++ /dev/null @@ -1,97 +0,0 @@ -/** - * Copyright (c) 2019-2021 Nino - * - * 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. - */ - -import { Service, Inject } from '@augu/lilith'; -import { readFileSync } from 'fs'; -import { Collection } from '@augu/collections'; -import { readdir } from '@augu/utils'; -import { Logger } from 'tslog'; -import { join } from 'path'; -import Locale from '../structures/Locale'; -import Config from '../components/Config'; - -@Service({ - priority: 1, - name: 'localization', -}) -export default class LocalizationService { - public defaultLocale!: Locale; - public locales: Collection = new Collection(); - - @Inject - private readonly logger!: Logger; - - @Inject - private readonly config!: Config; - - async load() { - this.logger.info('Loading in localization files...'); - - const directory = join(process.cwd(), '..', 'locales'); - const files = await readdir(directory); - - if (!files.length) { - this.logger.fatal('Missing localization files, did you clone the wrong commit?'); - process.exit(1); - } - - for (let i = 0; i < files.length; i++) { - const contents = readFileSync(files[i], 'utf-8'); - const lang = JSON.parse>(contents); - - this.logger.info(`✔ Found language ${lang.meta.full} (${lang.meta.code}) by ${lang.meta.translator}`); - this.locales.set(lang.meta.code, new Locale(lang as { meta: LocalizationMeta; strings: LocalizationStrings })); - } - - const defaultLocale = this.config.getProperty('defaultLocale') ?? 'en_US'; - this.logger.info(`Default localization language was set to ${defaultLocale}, applying...`); - - const locale = this.locales.find((locale) => locale.code === defaultLocale); - if (locale === null) { - this.logger.fatal(`Localization "${defaultLocale}" was not found, defaulting to en_US...`); - this.defaultLocale = this.locales.get('en_US')!; - - this.logger.warn( - `Due to locale "${defaultLocale}" not being found and want to translate, read up on our translating guide:` - ); - } else { - this.logger.info(`Localization "${defaultLocale}" was found!`); - this.defaultLocale = locale; - } - } - - /** - * Gets the localization for the [CommandService], determined by the [guild] and [user]'s locale. - * @param guild The guild's localization code - * @param user The user's localization code - */ - get(guild: string, user: string) { - // this shouldn't happen but you never know - if (!this.locales.has(guild) || !this.locales.has(user)) return this.defaultLocale; - - // committing yanderedev over here - if (user === this.defaultLocale.code && guild === this.defaultLocale.code) return this.defaultLocale; - else if (user !== this.defaultLocale.code && guild === this.defaultLocale.code) return this.locales.get(user)!; - else if (guild !== this.defaultLocale.code && user === this.defaultLocale.code) return this.locales.get(guild)!; - else return this.defaultLocale; - } -} diff --git a/src/services/PunishmentService.ts b/src/services/PunishmentService.ts deleted file mode 100644 index 3d21cd85..00000000 --- a/src/services/PunishmentService.ts +++ /dev/null @@ -1,808 +0,0 @@ -/** - * Copyright (c) 2019-2021 Nino - * - * 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. - */ - -import { Constants, Guild, Member, User, VoiceChannel, TextChannel, Message, Attachment } from 'eris'; -import { PunishmentType, Guild as NinoGuild, PrismaClient, Cases } from '@prisma/client'; -import { Inject, Service } from '@augu/lilith'; -import { EmbedBuilder } from '../structures'; -import TimeoutsManager from '../components/timeouts/Timeouts'; -import Permissions from '../util/Permissions'; -import { Logger } from 'tslog'; -import Database from '../components/Database'; -import Discord from '../components/Discord'; -import ms = require('ms'); - -type MemberLike = Member | { id: string; guild: Guild }; - -export enum PunishmentEntryType { - WarningRemoved = 'Warning Removed', - WarningAdded = 'Warning Added', - VoiceUndeafen = 'Voice Undeafen', - VoiceUnmute = 'Voice Unmute', - VoiceMute = 'Voice Mute', - VoiceDeaf = 'Voice Deafen', - Unban = 'Unban', - Unmuted = 'Unmuted', - Muted = 'Muted', - Kicked = 'Kicked', - Banned = 'Banned', -} - -interface ApplyPunishmentOptions { - attachments?: Attachment[]; - moderator: User; - publish?: boolean; - reason?: string; - member: MemberLike; - soft?: boolean; - time?: number; - days?: number; - type: PunishmentType; -} - -interface PublishModLogOptions { - warningsRemoved?: number | 'all'; - warningsAdded?: number; - attachments?: string[]; - moderator: User; - channel?: VoiceChannel; - reason?: string | null; - victim: User; - guild: Guild; - time?: number; - type: PunishmentEntryType; -} - -interface ApplyGenericMuteOptions extends ApplyActionOptions { - moderator: User; - settings: NinoGuild; -} - -interface ApplyActionOptions { - reason?: string; - member: Member; - guild: Guild; - time?: number; - self: Member; -} - -interface ApplyGenericVoiceAction extends Exclude { - statement: PublishModLogOptions; - moderator: User; -} - -interface ApplyBanActionOptions extends ApplyActionOptions { - moderator: User; - soft: boolean; - days: number; -} - -function stringifyDBType(type: PunishmentType): PunishmentEntryType | null { - switch (type) { - case PunishmentType.VOICE_UNDEAFEN: - return PunishmentEntryType.VoiceUndeafen; - case PunishmentType.VOICE_UNMUTE: - return PunishmentEntryType.VoiceUnmute; - case PunishmentType.VOICE_DEAFEN: - return PunishmentEntryType.VoiceDeaf; - case PunishmentType.VOICE_MUTE: - return PunishmentEntryType.VoiceMute; - case PunishmentType.UNMUTE: - return PunishmentEntryType.Unmuted; - case PunishmentType.UNBAN: - return PunishmentEntryType.Unban; - case PunishmentType.MUTE: - return PunishmentEntryType.Muted; - case PunishmentType.KICK: - return PunishmentEntryType.Kicked; - case PunishmentType.BAN: - return PunishmentEntryType.Banned; - - default: - return null; // shouldn't come here but oh well - } -} - -const emojis: { [P in PunishmentEntryType]: string } = { - [PunishmentEntryType.WarningRemoved]: ':pencil:', - [PunishmentEntryType.VoiceUndeafen]: ':speaking_head:', - [PunishmentEntryType.WarningAdded]: ':pencil:', - [PunishmentEntryType.VoiceUnmute]: ':loudspeaker:', - [PunishmentEntryType.VoiceMute]: ':mute:', - [PunishmentEntryType.VoiceDeaf]: ':mute:', - [PunishmentEntryType.Unmuted]: ':loudspeaker:', - [PunishmentEntryType.Kicked]: ':boot:', - [PunishmentEntryType.Banned]: ':hammer:', - [PunishmentEntryType.Unban]: ':bust_in_silhouette:', - [PunishmentEntryType.Muted]: ':mute:', -}; - -@Service({ - priority: 1, - name: 'punishments', -}) -export default class PunishmentService { - @Inject - private readonly database!: Database; - - @Inject - private readonly discord!: Discord; - - @Inject - private readonly prisma!: PrismaClient; - - @Inject - private readonly logger!: Logger; - - private async resolveMember(member: MemberLike, rest: boolean = true) { - return member instanceof Member - ? member - : member.guild.members.has(member.id) - ? member.guild.members.get(member.id)! - : rest - ? await this.discord.client - .getRESTGuildMember(member.guild.id, member.id) - .catch(() => new Member({ id: member.id }, member.guild, this.discord.client)) - : new Member({ id: member.id }, member.guild, this.discord.client); - } - - get timeouts(): TimeoutsManager { - return app.$ref(TimeoutsManager); - } - - permissionsFor(type: PunishmentType) { - switch (type) { - case PunishmentType.UNMUTE: - case PunishmentType.UNBAN: - return Constants.Permissions.manageRoles; - - case PunishmentType.VOICE_UNDEAFEN: - case PunishmentType.VOICE_DEAFEN: - return Constants.Permissions.voiceDeafenMembers; - - case PunishmentType.VOICE_UNMUTE: - case PunishmentType.VOICE_MUTE: - return Constants.Permissions.voiceMuteMembers; - - // what the fuck eslint - case PunishmentType.UNBAN: // eslint-disable-line - case PunishmentType.BAN: - return Constants.Permissions.banMembers; - - case PunishmentType.KICK: - return Constants.Permissions.kickMembers; - - default: - return 0n; - } - } - - async createWarning(member: Member, reason?: string, amount?: number) { - const self = member.guild.members.get(this.discord.client.user.id)!; - const warnings = await this.prisma.warning.findMany({ - where: { - guildId: member.guild.id, - userId: member.user.id, - }, - }); - - const current = warnings.reduce((acc, curr) => acc + curr.amount, 0); - const count = amount !== undefined ? current + amount : current + 1; - - if (count < 0) throw new RangeError('amount out of bounds'); - - const punishments = await this.prisma.punishments.findMany({ - where: { - guildId: member.guild.id, - }, - }); - - const results = punishments.filter((x) => x.warnings === count); - - await this.prisma.warning.create({ - data: { - guildId: member.guild.id, - reason, - amount: amount ?? 1, - userId: member.id, - }, - }); - - // run the actual punishments - for (let i = 0; i < results.length; i++) { - const result = results[i]; - await this.apply({ - moderator: this.discord.client.users.get(this.discord.client.user.id)!, - publish: false, - member, - type: result.type, - }); - } - - // get case index - const newest = await this.prisma.cases.findMany({ - where: { - guildId: member.guild.id, - }, - orderBy: { - index: 'asc', - }, - }); - - console.log(newest); - - const index = newest[0] !== undefined ? newest[0].index + 1 : 1; - const model = await this.prisma.cases.create({ - data: { - attachments: [], - moderatorId: this.discord.client.user.id, - victimId: member.id, - guildId: member.guild.id, - reason, - index, - type: PunishmentType.WARNING_ADDED, - soft: false, - }, - }); - - return results.length > 0 - ? Promise.resolve() - : this.publishToModLog( - { - warningsAdded: amount ?? 1, - moderator: self.user, - reason, - victim: member.user, - guild: member.guild, - type: PunishmentEntryType.WarningAdded, - }, - model - ); - } - - async removeWarning(member: Member, reason?: string, amount?: number | 'all') { - const self = member.guild.members.get(this.discord.client.user.id)!; - const warnings = await this.database.warnings.getAll(member.guild.id, member.id); - - if (warnings.length === 0) throw new SyntaxError("user doesn't have any punishments to be removed"); - - const count = warnings.reduce((acc, curr) => acc + curr.amount, 0); - if (amount === 'all') { - await this.database.warnings.clean(member.guild.id, member.id); - - // get case index - const newest = await this.prisma.cases.findMany({ - where: { - guildId: member.guild.id, - }, - orderBy: { - index: 'asc', - }, - }); - - console.log(newest); - - const index = newest[0] !== undefined ? newest[0].index + 1 : 1; - const model = await this.prisma.cases.create({ - data: { - attachments: [], - moderatorId: this.discord.client.user.id, - victimId: member.id, - guildId: member.guild.id, - reason, - index, - type: PunishmentType.WARNING_REMOVED, - soft: false, - }, - }); - - return this.publishToModLog( - { - warningsRemoved: 'all', - moderator: self.user, - victim: member.user, - reason, - guild: member.guild, - type: PunishmentEntryType.WarningRemoved, - }, - model - ); - } else { - // get case index - const newest = await this.prisma.cases.findMany({ - where: { - guildId: member.guild.id, - }, - orderBy: { - index: 'asc', - }, - }); - - console.log(newest); - - const index = newest[0] !== undefined ? newest[0].index + 1 : 1; - const model = await this.prisma.cases.create({ - data: { - attachments: [], - moderatorId: this.discord.client.user.id, - victimId: member.id, - guildId: member.guild.id, - reason, - index, - type: PunishmentType.WARNING_REMOVED, - soft: false, - }, - }); - - await this.prisma.warning.create({ - data: { - guildId: member.guild.id, - userId: member.user.id, - amount: -1, - reason, - }, - }); - - return this.publishToModLog( - { - warningsRemoved: count, - moderator: self.user, - victim: member.user, - reason, - guild: member.guild, - type: PunishmentEntryType.WarningRemoved, - }, - model - ); - } - } - - async apply({ attachments, moderator, publish, reason, member, soft, type, days, time }: ApplyPunishmentOptions) { - this.logger.info( - `Told to apply punishment ${type} on member ${member.id}${reason ? `, with reason: ${reason}` : ''}${ - publish ? ', publishing to modlog!' : '' - }` - ); - - const settings = await this.prisma.guild.findFirst({ - where: { - guildId: member.guild.id, - }, - }); - - const self = member.guild.members.get(this.discord.client.user.id)!; - - if ( - (member instanceof Member && !Permissions.isMemberAbove(self, member)) || - (BigInt(self.permissions.allow) & this.permissionsFor(type)) === 0n - ) - return; - - let user!: Member; - if (type === PunishmentType.UNBAN || (type === PunishmentType.BAN && member.guild.members.has(member.id))) { - user = await this.resolveMember(member, false); - } else { - user = await this.resolveMember(member, true); - } - - const modlogStatement: PublishModLogOptions = { - attachments: attachments?.map((s) => s.url) ?? [], - moderator, - reason, - victim: user.user, - guild: member.guild, - type: stringifyDBType(type)!, - time, - }; - - switch (type) { - case PunishmentType.BAN: - await this.applyBan({ - moderator, - member: user, - reason, - guild: member.guild, - self, - days: days ?? 7, - soft: soft === true, - time, - }); - break; - - case PunishmentType.KICK: - await user.kick(reason ? encodeURIComponent(reason) : 'No reason was specified.'); - break; - - case PunishmentType.MUTE: - await this.applyMute({ - moderator, - settings: settings!, // cannot be null :3 - member: user, - reason, - guild: member.guild, - self, - time, - }); - - break; - - case PunishmentType.UNBAN: - await member.guild.unbanMember(member.id, reason ? encodeURIComponent(reason) : 'No reason was specified.'); - break; - - case PunishmentType.UNMUTE: - await this.applyUnmute({ - moderator, - settings: settings!, - member: user, - reason, - guild: member.guild, - self, - time, - }); - - break; - - case PunishmentType.VOICE_MUTE: - await this.applyVoiceMute({ - moderator, - statement: modlogStatement, - member: user, - reason, - guild: member.guild, - self, - time, - }); - - break; - - case PunishmentType.VOICE_DEAFEN: - await this.applyVoiceDeafen({ - moderator, - statement: modlogStatement, - member: user, - reason, - guild: member.guild, - self, - time, - }); - - break; - - case PunishmentType.VOICE_UNMUTE: - await this.applyVoiceUnmute({ - moderator, - statement: modlogStatement, - member: user, - reason, - guild: member.guild, - self, - }); - - break; - - case PunishmentType.VOICE_UNDEAFEN: - await this.applyVoiceUndeafen({ - moderator, - statement: modlogStatement, - member: user, - reason, - guild: member.guild, - self, - }); - - break; - } - - // get case index - const newest = await this.prisma.cases.findMany({ - where: { - guildId: member.guild.id, - }, - orderBy: { - index: 'asc', - }, - }); - - console.log(newest); - - const index = newest[0] !== undefined ? newest[0].index + 1 : 1; - const model = await this.prisma.cases.create({ - data: { - attachments: attachments?.slice(0, 5).map((v) => v.url) ?? [], - moderatorId: moderator.id, - victimId: member.id, - guildId: member.guild.id, - reason, - index, - soft: soft === true, - time, - type, - }, - }); - - if (publish) { - await this.publishToModLog(modlogStatement, model); - } - } - - private async applyBan({ moderator, reason, member, guild, days, soft, time }: ApplyBanActionOptions) { - await guild.banMember(member.id, days, reason); - if (soft) await guild.unbanMember(member.id, reason); - if (!soft && time !== undefined && time > 0) { - if (this.timeouts.state !== 'connected') - this.logger.warn('Timeouts service is not connected! Will relay once done...'); - - await this.timeouts.apply({ - moderator: moderator.id, - victim: member.id, - guild: guild.id, - type: PunishmentType.UNBAN, - time, - }); - } - } - - private async applyUnmute({ settings, reason, member, guild }: ApplyGenericMuteOptions) { - const role = guild.roles.get(settings.mutedRoleId!)!; - if (member.roles.includes(role.id)) - await member.removeRole(role.id, reason ? encodeURIComponent(reason) : 'No reason was specified.'); - } - - private async applyMute({ moderator, settings, reason, member, guild, time }: ApplyGenericMuteOptions) { - const roleID = await this.getOrCreateMutedRole(guild, settings); - - if (reason) reason = encodeURIComponent(reason); - if (!member.roles.includes(roleID)) { - await member.addRole(roleID, reason ?? 'No reason was specified.'); - } - - if (time !== undefined && time > 0) { - if (this.timeouts.state !== 'connected') - this.logger.warn('Timeouts service is not connected! Will relay once done...'); - - await this.timeouts.apply({ - moderator: moderator.id, - victim: member.id, - guild: guild.id, - type: PunishmentType.UNMUTE, - time, - }); - } - } - - private async applyVoiceMute({ moderator, reason, member, guild, statement, time }: ApplyGenericVoiceAction) { - if (reason) reason = encodeURIComponent(reason); - if (member.voiceState.channelID !== null && !member.voiceState.mute) - await member.edit({ mute: true }, reason ?? 'No reason was specified.'); - - statement.channel = (await this.discord.client.getRESTChannel(member.voiceState.channelID!)) as VoiceChannel; - if (time !== undefined && time > 0) { - if (this.timeouts.state !== 'connected') - this.logger.warn('Timeouts service is not connected! Will relay once done...'); - - await this.timeouts.apply({ - moderator: moderator.id, - victim: member.id, - guild: guild.id, - type: PunishmentType.VOICE_UNMUTE, - time, - }); - } - } - - private async applyVoiceDeafen({ moderator, reason, member, guild, statement, time }: ApplyGenericVoiceAction) { - if (reason) reason = encodeURIComponent(reason); - if (member.voiceState.channelID !== null && !member.voiceState.deaf) - await member.edit({ deaf: true }, reason ?? 'No reason was specified.'); - - statement.channel = (await this.discord.client.getRESTChannel(member.voiceState.channelID!)) as VoiceChannel; - if (time !== undefined && time > 0) { - if (this.timeouts.state !== 'connected') - this.logger.warn('Timeouts service is not connected! Will relay once done...'); - - await this.timeouts.apply({ - moderator: moderator.id, - victim: member.id, - guild: guild.id, - type: PunishmentType.VOICE_UNDEAFEN, - time, - }); - } - } - - private async applyVoiceUnmute({ reason, member, statement }: ApplyGenericVoiceAction) { - if (reason) reason = encodeURIComponent(reason); - if (member.voiceState !== undefined && member.voiceState.mute) - await member.edit({ mute: false }, reason ?? 'No reason was specified.'); - - statement.channel = (await this.discord.client.getRESTChannel(member.voiceState.channelID!)) as VoiceChannel; - } - - private async applyVoiceUndeafen({ reason, member, statement }: ApplyGenericVoiceAction) { - if (reason) reason = encodeURIComponent(reason); - if (member.voiceState !== undefined && member.voiceState.deaf) - await member.edit({ deaf: false }, reason ?? 'No reason was specified.'); - - statement.channel = (await this.discord.client.getRESTChannel(member.voiceState.channelID!)) as VoiceChannel; - } - - private async publishToModLog( - { - warningsRemoved, - warningsAdded, - moderator, - attachments, - channel, - reason, - victim, - guild, - time, - type, - }: PublishModLogOptions, - caseModel: Cases - ) { - const settings = await this.database.guilds.get(guild.id); - if (!settings.modlogChannelID) return; - - const modlog = guild.channels.get(settings.modlogChannelID) as TextChannel; - if (!modlog) return; - - if ( - !modlog.permissionsOf(this.discord.client.user.id).has('sendMessages') || - !modlog.permissionsOf(this.discord.client.user.id).has('embedLinks') - ) - return; - - const embed = this.getModLogEmbed(caseModel.index, { - attachments, - warningsRemoved, - warningsAdded, - moderator, - channel, - reason, - victim, - guild, - time, - type: stringifyDBType(caseModel.type)!, - }).build(); - const content = `**[** ${emojis[type] ?? ':question:'} **~** Case #**${caseModel.index}** (${type}) ]`; - const message = await modlog.createMessage({ - embed, - content, - }); - - await this.database.cases.update(guild.id, caseModel.index, { - messageID: message.id, - }); - } - - async editModLog(model: Cases, message: Message) { - const warningRemovedField = message.embeds[0].fields?.find((field) => field.name.includes('Warnings Removed')); - const warningsAddField = message.embeds[0].fields?.find((field) => field.name.includes('Warnings Added')); - - const obj: Record = {}; - if (warningsAddField !== undefined) obj.warningsAdded = Number(warningsAddField.value); - - if (warningRemovedField !== undefined) - obj.warningsRemoved = warningRemovedField.value === 'All' ? 'All' : Number(warningRemovedField.value); - - return message.edit({ - content: `**[** ${emojis[stringifyDBType(model.type)!] ?? ':question:'} ~ Case #**${model.index}** (${ - stringifyDBType(model.type) ?? '... unknown ...' - }) **]**`, - embed: this.getModLogEmbed(model.index, { - moderator: this.discord.client.users.get(model.moderatorId)!, - victim: this.discord.client.users.get(model.victimId)!, - reason: model.reason, - guild: this.discord.client.guilds.get(model.guildId)!, - time: model.time !== undefined ? Number(model.time) : undefined, - type: stringifyDBType(model.type)!, - - ...obj, - }).build(), - }); - } - - private async getOrCreateMutedRole(guild: Guild, settings: NinoGuild) { - let muteRole = settings.mutedRoleId; - if (muteRole) return muteRole; - - let role = guild.roles.find((x) => x.name.toLowerCase() === 'muted'); - if (!role) { - role = await guild.createRole( - { - mentionable: false, - permissions: 0, - hoist: false, - name: 'Muted', - }, - `[${this.discord.client.user.username}#${this.discord.client.user.discriminator}] Created "Muted" role` - ); - - muteRole = role.id; - - const topRole = Permissions.getTopRole(guild.members.get(this.discord.client.user.id)!); - if (topRole !== undefined) { - await role.editPosition(topRole.position - 1); - for (const channel of guild.channels.values()) { - const permissions = channel.permissionsOf(this.discord.client.user.id); - if (permissions.has('manageChannels')) - await channel.editPermission( - /* overwriteID */ role.id, - /* allowed */ 0, - /* denied */ Constants.Permissions.sendMessages, - /* type */ 0, - /* reason */ `[${this.discord.client.user.username}#${this.discord.client.user.discriminator}] Overrided permissions for new Muted role` - ); - } - } - } - - await this.database.guilds.update(guild.id, { mutedRoleID: role.id }); - return role.id; - } - - getModLogEmbed( - caseID: number, - { warningsRemoved, warningsAdded, attachments, moderator, channel, reason, victim, time }: PublishModLogOptions - ) { - const embed = new EmbedBuilder() - .setColor(0xdaa2c6) - .setAuthor( - `${victim.username}#${victim.discriminator} (${victim.id})`, - undefined, - victim.dynamicAvatarURL('png', 1024) - ) - .addField('• Moderator', `${moderator.username}#${moderator.discriminator} (${moderator.id})`, true); - - const _reason = - reason !== undefined - ? Array.isArray(reason) - ? reason.join(' ') - : reason - : ` - • No reason was provided. Use \`reason ${caseID} \` to update it! - `; - - const _attachments = attachments?.map((url, index) => `• [**\`Attachment #${index}\`**](${url})`).join('\n') ?? ''; - - embed.setDescription([_reason ?? '', _attachments]); - - if (warningsRemoved !== undefined) - embed.addField('• Warnings Removed', warningsRemoved === 'all' ? 'All' : warningsRemoved.toString(), true); - - if (warningsAdded !== undefined) embed.addField('• Warnings Added', warningsAdded.toString(), true); - - if (channel !== undefined) embed.addField('• Voice Channel', `${channel.name} (${channel.id})`, true); - - if (time !== undefined || time !== null) { - try { - embed.addField('• Time', ms(time!, { long: true }), true); - } catch { - // ignore since fuck you - } - } - - return embed; - } -} diff --git a/src/singletons/Logger.ts b/src/singletons/Logger.ts deleted file mode 100644 index 443c99d5..00000000 --- a/src/singletons/Logger.ts +++ /dev/null @@ -1,36 +0,0 @@ -/** - * Copyright (c) 2019-2021 Nino - * - * 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. - */ - -import { hostname } from 'os'; -import { Logger } from 'tslog'; - -export default new Logger({ - displayFunctionName: false, - exposeErrorCodeFrame: true, - displayInstanceName: true, - dateTimePattern: '[hour:minute:second @ day/month/year]', - displayFilePath: 'hideNodeModulesOnly', - displayTypes: false, - instanceName: hostname(), - minLevel: process.env.NODE_ENV === 'production' ? 'info' : 'silly', - name: 'Nino', -}); diff --git a/src/slash/core/About.ts b/src/slash/core/About.ts deleted file mode 100644 index 8372c734..00000000 --- a/src/slash/core/About.ts +++ /dev/null @@ -1,34 +0,0 @@ -/** - * Copyright (c) 2019-2021 Nino - * - * 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. - */ - -import { CommandContext, SlashCommand, SlashCommandOptions, SlashCreator } from 'slash-create'; -import { SlashCommandInfo } from '../../structures'; - -@SlashCommandInfo({ - description: 'Shows a bit information about myself!', - name: 'about', -}) -export default class AboutCommand extends SlashCommand { - override async run(ctx: CommandContext) { - return ctx.send('ur mom!!!', { ephemeral: true }); - } -} diff --git a/src/structures/Command.ts b/src/structures/Command.ts deleted file mode 100644 index 2cf11827..00000000 --- a/src/structures/Command.ts +++ /dev/null @@ -1,104 +0,0 @@ -/** - * Copyright (c) 2019-2021 Nino - * - * 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. - */ - -import { Categories, MetadataKeys } from '../util/Constants'; -import { getSubcommandsIn } from './decorators/Subcommand'; -import type CommandMessage from './CommandMessage'; -import type { Constants } from 'eris'; -import Subcommand from './Subcommand'; - -export type PermissionField = keyof Constants['Permissions']; -export interface CommandInfo { - userPermissions?: PermissionField | PermissionField[]; - botPermissions?: PermissionField | PermissionField[]; - description?: string; - ownerOnly?: boolean; - examples?: string[]; - category?: Categories; - cooldown?: number; - aliases?: string[]; - hidden?: boolean; - usage?: string; - name: string; -} - -export default abstract class NinoCommand { - public userPermissions: PermissionField[]; - public botPermissions: PermissionField[]; - public description: ObjectKeysWithSeperator; - public ownerOnly: boolean; - public examples: string[]; - public category: Categories; - public cooldown: number; - public aliases: string[]; - public hidden: boolean; - public usage: string; - public name: string; - - constructor(info: CommandInfo) { - this.userPermissions = - typeof info.userPermissions === 'string' - ? [info.userPermissions] - : Array.isArray(info.userPermissions) - ? info.userPermissions - : []; - - this.botPermissions = - typeof info.botPermissions === 'string' - ? [info.botPermissions] - : Array.isArray(info.botPermissions) - ? info.botPermissions - : []; - - this.description = - (info.description as unknown as ObjectKeysWithSeperator) ?? 'descriptions.unknown'; - this.ownerOnly = info.ownerOnly ?? false; - this.examples = info.examples ?? []; - this.category = info.category ?? Categories.Core; - this.cooldown = info.cooldown ?? 5; - this.aliases = info.aliases ?? []; - this.hidden = info.hidden ?? false; - this.usage = info.usage ?? ''; - this.name = info.name; - } - - /** - * Returns the list of subcommands available. - */ - get subcommands() { - return getSubcommandsIn(this).map((sub) => new Subcommand(sub)); - } - - /** - * Returns if this base command is a slash command also - */ - get hasSlashVariant() { - return Reflect.getMetadata(MetadataKeys.HasSlashVariant, this) === true; - } - - get format() { - const subcommands = this.subcommands.map((sub) => `[${sub.name} ${sub.usage.trim()}]`.trim()).join(' | '); - return `${this.name}${this.usage !== '' ? ` ${this.usage.trim()}` : ''} ${subcommands}`; - } - - abstract run(msg: CommandMessage, ...args: any[]): any; -} diff --git a/src/structures/CommandMessage.ts b/src/structures/CommandMessage.ts deleted file mode 100644 index b1b86781..00000000 --- a/src/structures/CommandMessage.ts +++ /dev/null @@ -1,149 +0,0 @@ -/** - * Copyright (c) 2019-2021 Nino - * - * 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. - */ - -import type { AdvancedMessageContent, Message, TextChannel } from 'eris'; -import type GuildEntity from '../entities/GuildEntity'; -import { EmbedBuilder } from '.'; -import type { Filter } from './MessageCollector'; -import type UserEntity from '../entities/UserEntity'; -import type Locale from './Locale'; -import { Inject } from '@augu/lilith'; -import Discord from '../components/Discord'; - -export default class CommandMessage { - public userSettings: UserEntity; - public settings: GuildEntity; - private _flags: any = {}; - public locale: Locale; - #message: Message; - - @Inject - private discord!: Discord; - - constructor(message: Message, locale: Locale, settings: GuildEntity, userSettings: UserEntity) { - this.userSettings = userSettings; - this.settings = settings; - this.#message = message; - this.locale = locale; - } - - get attachments() { - return this.#message.attachments; - } - - get channel() { - return this.#message.channel; - } - - get author() { - return this.#message.author; - } - - get member() { - return this.#message.member; - } - - get guild() { - return this.#message.channel.guild; - } - - get self() { - return this.guild.members.get(this.discord.client.user.id); - } - - get successEmote() { - return this.discord.emojis.find((e) => e === '<:success:464708611260678145>') ?? ':black_check_mark:'; - } - - get errorEmote() { - return this.discord.emojis.find((e) => e === '<:xmark:464708589123141634>') ?? ':x:'; - } - - flags(): T { - return this._flags; - } - - translate>(key: K, args?: any[] | Record) { - return this.reply(this.locale.translate(key, args)); - } - - reply(content: string | EmbedBuilder, allowReply: boolean = true) { - const payload: AdvancedMessageContent = { - allowedMentions: { - repliedUser: false, - everyone: false, - roles: false, - users: false, - }, - }; - - if (allowReply) payload.messageReference = { messageID: this.#message.id }; - - if (typeof content === 'string') { - payload.content = content; - return this.channel.createMessage(payload); - } else { - if (this.guild) { - if (this.self?.permissions.has('embedLinks')) - return this.channel.createMessage({ - embed: content.build(), - ...payload, - }); - // TODO: unembedify util - else - return this.channel.createMessage({ - content: content.description!, - ...payload, - }); - } else { - return this.channel.createMessage({ - embed: content.build(), - ...payload, - }); - } - } - } - - success(content: string) { - return this.reply(`${this.successEmote} ${content}`); - } - - error(content: string) { - return this.reply(`${this.errorEmote} ${content}`); - } - - async awaitReply(content: string | EmbedBuilder, time: number, filter: Filter) { - const message = await this.reply(content); - const replied = await this.#message.collector.awaitMessage(filter, { - channel: this.channel.id, - author: this.author.id, - time, - }); - - if (replied === null) { - await message.delete(); - return this.error("**Didn't receive anything, assuming to cancel.**"); - } - - return replied; - } -} diff --git a/src/structures/EmbedBuilder.ts b/src/structures/EmbedBuilder.ts deleted file mode 100644 index 4609b107..00000000 --- a/src/structures/EmbedBuilder.ts +++ /dev/null @@ -1,199 +0,0 @@ -/** - * Copyright (c) 2019-2021 Nino - * - * 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. - */ - -/* eslint-disable camelcase */ - -import type { - APIEmbedAuthor, - APIEmbedField, - APIEmbedFooter, - APIEmbedImage, - APIEmbedThumbnail, -} from 'discord-api-types'; - -import { omitUndefinedOrNull } from '@augu/utils'; -import type { EmbedOptions } from 'eris'; -import { Color } from '../util/Constants'; - -export default class EmbedBuilder { - public description?: string; - public timestamp?: string | Date; - public thumbnail?: APIEmbedThumbnail; - public author?: APIEmbedAuthor; - public footer?: APIEmbedFooter; - public fields?: APIEmbedField[]; - public image?: APIEmbedImage; - public color?: number; - public title?: string; - public url?: string; - - constructor(data: EmbedOptions = {}) { - this.patch(data); - } - - patch(data: EmbedOptions) { - if (data.description !== undefined) this.description = data.description; - - // @ts-ignore - if (data.thumbnail !== undefined) this.thumbnail = data.thumbnail; - - if (data.timestamp !== undefined) this.timestamp = data.timestamp; - - if (data.author !== undefined) this.author = data.author; - - if (data.fields !== undefined) this.fields = data.fields; - - // @ts-ignore - if (data.image !== undefined) this.image = data.image; - - if (data.color !== undefined) this.color = data.color; - - if (data.title !== undefined) this.title = data.title; - - if (data.url !== undefined) this.url = data.url; - } - - setDescription(description: string | string[]) { - this.description = Array.isArray(description) ? description.join('\n') : description; - return this; - } - - setTimestamp(stamp: Date | number = new Date()) { - let timestamp!: number; - - if (stamp instanceof Date) timestamp = stamp.getTime(); - else if (typeof stamp === 'number') timestamp = stamp; - - this.timestamp = String(timestamp); - return this; - } - - setThumbnail(thumb: string) { - this.thumbnail = { url: thumb }; - return this; - } - - setAuthor(name: string, url?: string, iconUrl?: string) { - this.author = { name, url, icon_url: iconUrl }; - return this; - } - - addField(name: string, value: string, inline: boolean = false) { - if (this.fields === undefined) this.fields = []; - if (this.fields.length > 25) throw new RangeError('Maximum amount of fields reached.'); - - this.fields.push({ name, value, inline }); - return this; - } - - addBlankField(inline: boolean = false) { - return this.addField('\u200b', '\u200b', inline); - } - - addFields(fields: APIEmbedField[]) { - for (let i = 0; i < fields.length; i++) this.addField(fields[i].name, fields[i].value, fields[i].inline); - - return this; - } - - setColor(color: string | number | [r: number, g: number, b: number] | 'random' | 'default') { - if (typeof color === 'number') { - this.color = color; - return this; - } - - if (typeof color === 'string') { - if (color === 'default') { - this.color = 0; - return this; - } - - if (color === 'random') { - this.color = Math.floor(Math.random() * (0xffffff + 1)); - return this; - } - - const int = parseInt(color.replace('#', ''), 16); - - this.color = (int << 16) + (int << 8) + int; - return this; - } - - if (Array.isArray(color)) { - if (color.length > 2) throw new RangeError('RGB value cannot exceed to 3 or more elements'); - - const [r, g, b] = color; - this.color = (r << 16) + (g << 8) + b; - - return this; - } - - throw new TypeError( - `'color' argument was not a hexadecimal, number, RGB value, 'random', or 'default' (${typeof color})` - ); - } - - setTitle(title: string) { - this.title = title; - return this; - } - - setURL(url: string) { - this.url = url; - return this; - } - - setImage(url: string) { - this.image = { url }; - return this; - } - - setFooter(text: string, iconUrl?: string) { - this.footer = { text, icon_url: iconUrl }; - return this; - } - - static create() { - return new EmbedBuilder().setColor(Color); - } - - build() { - return omitUndefinedOrNull({ - description: this.description, - thumbnail: this.thumbnail, - timestamp: this.timestamp, - footer: this.footer, - author: this.author - ? { - name: this.author.name!, - url: this.author.url, - icon_url: this.author.icon_url, - } - : undefined, - fields: this.fields, - image: this.image, - color: this.color, - title: this.title, - url: this.url, - }); - } -} diff --git a/src/structures/Locale.ts b/src/structures/Locale.ts deleted file mode 100644 index 9a81bd9f..00000000 --- a/src/structures/Locale.ts +++ /dev/null @@ -1,113 +0,0 @@ -/** - * Copyright (c) 2019-2021 Nino - * - * 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. - */ - -import { isObject } from '@augu/utils'; - -const NOT_FOUND_SYMBOL = Symbol.for('$nino::localization::not_found'); - -interface Localization { - meta: LocalizationMeta; - strings: LocalizationStrings; -} - -const KEY_REGEX = /[$]\{([\w\.]+)\}/g; - -export default class Locale { - public contributors: string[]; - public translator: string; - public aliases: string[]; - public flag: string; - public full: string; - public code: string; - - private strings: LocalizationStrings; - - constructor({ meta, strings }: Localization) { - this.contributors = meta.contributors; - this.translator = meta.translator; - this.strings = strings; - this.aliases = meta.aliases; - this.flag = meta.flag; - this.full = meta.full; - this.code = meta.code; - } - - translate, R = KeyToPropType>( - key: K, - args?: { [x: string]: any } | any[] - ): R extends string[] ? string : string { - const nodes = key.split('.'); - let value: any = this.strings; - - for (const node of nodes) { - try { - value = value[node]; - } catch (ex) { - if ((ex as Error).message.includes('of undefined')) value = NOT_FOUND_SYMBOL; - - break; - } - } - - if (value === undefined || value === NOT_FOUND_SYMBOL) throw new TypeError(`Node '${key}' doesn't exist...`); - - if (isObject(value)) throw new TypeError(`Node '${key}' is a object!`); - - if (Array.isArray(value)) { - return value.map((val) => this.stringify(val, args)).join('\n') as unknown as any; - } else { - return this.stringify(value, args); - } - } - - private stringify(value: any, rawArgs?: { [x: string]: any } | (string | number)[]) { - // If no arguments are provided, best to assume to return the string - if (!rawArgs) return value; - - // Convert it to a string - if (typeof value !== 'string') value = String(value); - - let _i = 0; - if (Array.isArray(rawArgs)) { - const matches = /%s|%d/g.exec(value); - if (matches === null) return value; - - for (let i = 0; i < matches.length; i++) { - const match = matches[i]; - if (match === '%s') { - _i++; - return value.replace(/%s/g, () => String(rawArgs.shift())); - } else if (match === '%d') { - if (isNaN(Number(rawArgs[_i]))) throw new TypeError(`Value "${rawArgs[_i]}" was not a number (index: ${_i})`); - - _i++; - return value.replace(/%d/g, () => String(rawArgs.shift())); - } - } - } else { - return (value as string).replace(KEY_REGEX, (_, key) => { - const value = String(rawArgs[key]); - return value === '' ? '?' : value || '?'; - }); - } - } -} diff --git a/src/structures/MessageCollector.ts b/src/structures/MessageCollector.ts deleted file mode 100644 index 4bc90deb..00000000 --- a/src/structures/MessageCollector.ts +++ /dev/null @@ -1,76 +0,0 @@ -/** - * Copyright (c) 2019-2021 Nino - * - * 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. - */ - -import { Message, TextChannel } from 'eris'; -import { Collection } from '@augu/collections'; -import Discord from '../components/Discord'; - -export type Filter = (msg: Message) => boolean; - -interface Collector { - accept(value: Message | PromiseLike>): void; - - filter: Filter; - channel: string; - author: string; - time: number; -} - -/** - * Represents a bare bones message collector, which collects a message - * based on a predicate and returns a value or `null`. - */ -export default class MessageCollector { - #collection: Collection = new Collection(); - - constructor(discord: Discord) { - discord.client.on('messageCreate', this.onReact.bind(this)); - } - - private onReact(msg: Message) { - if (msg.author.bot) return; - - const result = this.#collection.get(`${msg.author.id}:${msg.channel.id}`); - if (!result) return; - - if (result.filter(msg)) { - result.accept(msg); - this.#collection.delete(`${msg.author.id}:${msg.channel.id}`); - } - } - - awaitMessage(filter: Filter, { channel, author, time }: Pick) { - return new Promise | null>((accept) => { - if (this.#collection.has(`${author}:${channel}`)) this.#collection.delete(`${author}:${channel}`); - - this.#collection.set(`${author}:${channel}`, { - accept, - filter, - channel, - author, - time, - }); - - setTimeout(accept.bind(null, null), time * 1000); - }); - } -} diff --git a/src/structures/Subcommand.ts b/src/structures/Subcommand.ts deleted file mode 100644 index 5704495d..00000000 --- a/src/structures/Subcommand.ts +++ /dev/null @@ -1,50 +0,0 @@ -/** - * Copyright (c) 2019-2021 Nino - * - * 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. - */ - -import type CommandMessage from './CommandMessage'; -import type { Constants } from 'eris'; -import type Command from './Command'; - -export interface SubcommandInfo { - run(this: Command, msg: CommandMessage): Promise; - - permissions?: keyof Constants['Permissions']; - methodName: string; - aliases?: string[]; - usage: string; -} - -export default class Subcommand { - public permissions?: keyof Constants['Permissions']; - public aliases: string[]; - public usage: string; - public name: string; - public run: (this: Command, msg: CommandMessage) => Promise; - - constructor(info: SubcommandInfo) { - this.permissions = info.permissions; - this.aliases = info.aliases ?? []; - this.usage = info.usage; - this.name = info.methodName; - this.run = info.run; - } -} diff --git a/src/structures/decorators/SlashCommand.ts b/src/structures/decorators/SlashCommand.ts deleted file mode 100644 index fe462787..00000000 --- a/src/structures/decorators/SlashCommand.ts +++ /dev/null @@ -1,32 +0,0 @@ -/** - * Copyright (c) 2019-2021 Nino - * - * 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. - */ - -import { SlashCommandOptions, SlashCreator } from 'slash-create'; -import { createProxyDecorator } from '../../util/proxy/ProxyDecoratorUtil'; -import app from '../../container'; - -export function SlashCommand(options: SlashCommandOptions): ClassDecorator { - return (target) => - createProxyDecorator(target, { - construct: (ctor: any) => new ctor(app.$ref(SlashCreator), options), - }); -} diff --git a/src/structures/decorators/Subcommand.ts b/src/structures/decorators/Subcommand.ts deleted file mode 100644 index f0bbf171..00000000 --- a/src/structures/decorators/Subcommand.ts +++ /dev/null @@ -1,51 +0,0 @@ -/** - * Copyright (c) 2019-2021 Nino - * - * 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. - */ - -import type { SubcommandInfo } from '../Subcommand'; -import { MetadataKeys } from '../../util/Constants'; - -export const getSubcommandsIn = (target: any) => - Reflect.getMetadata(MetadataKeys.Subcommand, target) ?? []; - -interface SubcommandDecoratorOptions { - aliases?: string[]; - permissions?: SubcommandInfo['permissions']; -} - -export default function Subcommand( - usage?: string, - aliasesOrOptions?: SubcommandDecoratorOptions | string[] -): MethodDecorator { - return (target, methodName, descriptor: TypedPropertyDescriptor) => { - const subcommands = getSubcommandsIn(target); - - subcommands.push({ - permissions: Array.isArray(aliasesOrOptions) ? undefined : aliasesOrOptions?.permissions, - aliases: Array.isArray(aliasesOrOptions) ? aliasesOrOptions : aliasesOrOptions?.aliases, - methodName: String(methodName), - usage: usage ?? '', - run: descriptor.value!, - }); - - Reflect.defineMetadata(MetadataKeys.Subcommand, subcommands, target); - }; -} diff --git a/src/structures/index.ts b/src/structures/index.ts deleted file mode 100644 index 361a7fb9..00000000 --- a/src/structures/index.ts +++ /dev/null @@ -1,31 +0,0 @@ -/** - * Copyright (c) 2019-2021 Nino - * - * 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. - */ - -export { SlashCommand as SlashCommandInfo } from './decorators/SlashCommand'; -export { Command as CommandInfo } from './decorators/Command'; -export { default as Subcommand } from './decorators/Subcommand'; -export { default as Subscribe } from './decorators/Subscribe'; - -export { default as CommandMessage } from './CommandMessage'; -export { default as EmbedBuilder } from './EmbedBuilder'; -export { default as Command } from './Command'; -export { Automod } from './Automod'; diff --git a/src/util/Constants.ts b/src/util/Constants.ts deleted file mode 100644 index 5b23a6d1..00000000 --- a/src/util/Constants.ts +++ /dev/null @@ -1,80 +0,0 @@ -/** - * Copyright (c) 2019-2021 Nino - * - * 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. - */ - -import { readFileSync } from 'fs'; -import { execSync } from 'child_process'; -import { join } from 'path'; - -const { version: pkgVersion } = require('@/package.json'); - -/** - * Returns the current version of Nino - */ -export const version: string = pkgVersion; - -/** - * Returns the commit hash of the bot. - */ -export const commitHash: string | null = (() => { - try { - const hash = execSync('git rev-parse HEAD', { encoding: 'utf8' }).trim(); - return hash.slice(0, 8); - } catch { - return null; - } -})(); - -export const SHORT_LINKS = JSON.parse( - readFileSync(join(process.cwd(), '..', 'assets', 'shortlinks.json'), 'utf8').split(/\n\r?/).join('\n') -); - -export const Color = 0xdaa2c6; -export const USERNAME_DISCRIM_REGEX = /^(.+)#(\d{4})$/; -export const DISCORD_INVITE_REGEX = - /(http(s)?:\/\/(www.)?)?(discord.gg|discord.io|discord.me|discord.link|invite.gg)\/\w+/; -export const USER_MENTION_REGEX = /^<@!?([0-9]+)>$/; -export const CHANNEL_REGEX = /<#([0-9]+)>$/; -export const QUOTE_REGEX = /['"]/; -export const ROLE_REGEX = /^<@&([0-9]+)>$/; -export const ID_REGEX = /^\d+$/; - -/** - * List of categories available to commands - */ -export enum Categories { - Moderation = 'moderation', - ThreadMod = 'thread moderation', - VoiceMod = 'voice moderation', - Settings = 'settings', - Owner = 'owner', - Core = 'core', -} - -/** - * List of metadata keys for decorators - */ -export const enum MetadataKeys { - Subcommand = '$nino::subcommands', - Subscribe = '$nino::subscriptions', - HasSlashVariant = '$nino::has-slash-variant', - CommandMeta = '$nino::command::metadata', -} diff --git a/src/util/Permissions.ts b/src/util/Permissions.ts deleted file mode 100644 index 245f947b..00000000 --- a/src/util/Permissions.ts +++ /dev/null @@ -1,96 +0,0 @@ -/** - * Copyright (c) 2019-2021 Nino - * - * 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. - */ - -import { Constants, Role, Member, Permission } from 'eris'; - -/** - * Contains utility functions to help with permission checking and hierarchy. - */ -export default class Permissions { - /** - * Returns the highest role the member has, `undefined` if none was found. - * @param member The member to check - */ - static getTopRole(member: Member) { - // eris why - if (member === undefined || member === null) return; - - // For some reason, `roles` will become undefined? So we have to check for that. - // It could be a bug in Discord or `member` is undefined. - if (member.roles === undefined) return; - - if (member.roles.length === 0) return; - - return member.roles - .map((roleID) => member.guild.roles.get(roleID)) - .filter((role) => role !== undefined) - .sort((a, b) => b!.position - a!.position)[0]; - } - - /** - * Checks if role A is above role B in hierarchy (vice-versa) - * @param a The role that should be higher - * @param b The role that should be lower - */ - static isRoleAbove(a?: Role, b?: Role) { - if (!a) return false; - if (!b) return true; - - return a.position > b.position; - } - - /** - * Checks if member A is above member B in hierarchy (vice-versa) - * @param a The member that should be higher - * @param b The member that should be lower - */ - static isMemberAbove(a: Member, b: Member) { - const topRoleA = this.getTopRole(a); - const topRoleB = this.getTopRole(b); - - return this.isRoleAbove(topRoleA, topRoleB); - } - - /** - * Shows a string representation of all of the permissions - * @param bits The permission bitfield - */ - static stringify(permission: bigint) { - const permissions = new Permission(Number(permission), 0).json; - const names: string[] = []; - - for (const key of Object.keys(Constants.Permissions)) { - if (permissions.hasOwnProperty(key)) names.push(key); - } - - return names.join(', '); - } - - /** - * Returns if the user's bitfield reaches the threshold of the [required] bitfield. - * @param user The user permission bitfield - * @param required The required permission bitfield - */ - static hasOverlap(user: number, required: number) { - return (user & 8) !== 0 || (user & required) === required; - } -} diff --git a/src/util/index.ts b/src/util/index.ts deleted file mode 100644 index e6e89cf4..00000000 --- a/src/util/index.ts +++ /dev/null @@ -1,89 +0,0 @@ -/** - * Copyright (c) 2019-2021 Nino - * - * 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. - */ - -import { QUOTE_REGEX } from './Constants'; - -/** - * Iterator function to provide a tuple of `[index, item]` in a Array. - * @param arr The array to run this iterator function - * @example - * ```ts - * const arr = ['str', 'uwu', 'owo']; - * for (const [index, item] of withIndex(arr)) { - * console.log(`${index}: ${item}`); - * // prints out: - * // 0: str - * // 1: uwu - * // 2: owo - * } - * ``` - */ -export function* withIndex(arr: T): Generator<[index: number, item: T[any]]> { - for (let i = 0; i < arr.length; i++) { - yield [i, arr[i]]; - } -} - -export function formatSize(bytes: number) { - const kilo = bytes / 1024; - const mega = kilo / 1024; - const giga = mega / 1024; - - if (kilo < 1024) return `${kilo.toFixed(1)}KB`; - else if (kilo > 1024 && mega < 1024) return `${mega.toFixed(1)}MB`; - else return `${giga.toFixed(1)}GB`; -} - -// credit: Ice (https://github.com/IceeMC) -export function getQuotedStrings(content: string) { - const parsed: string[] = []; - let curr = ''; - let opened = false; - for (let i = 0; i < content.length; i++) { - const char = content[i]; - if (char === ' ' && !opened) { - opened = false; - if (curr.length > 0) parsed.push(curr); - - curr = ''; - } - - if (QUOTE_REGEX.test(char)) { - if (opened) { - opened = false; - if (curr.length > 0) parsed.push(curr); - - curr = ''; - continue; - } - - opened = true; - continue; - } - - if (!opened && char === ' ') continue; - curr += char; - } - - if (curr.length > 0) parsed.push(curr); - return parsed; -} diff --git a/src/util/patches/ErisPatch.ts b/src/util/patches/ErisPatch.ts deleted file mode 100644 index 5a7320ed..00000000 --- a/src/util/patches/ErisPatch.ts +++ /dev/null @@ -1,45 +0,0 @@ -/** - * Copyright (c) 2019-2021 Nino - * - * 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. - */ - -import MessageCollector from '../../structures/MessageCollector'; -import { Message, User } from 'eris'; -import { Logger } from 'tslog'; - -const logger = app.$ref(Logger); -logger.info('monkeypatching eris...'); - -Object.defineProperty(User.prototype, 'tag', { - get(this: User) { - return `${this.username}#${this.discriminator}`; - }, - - set: () => { - throw new TypeError('cannot set user tags :('); - }, -}); - -Object.defineProperty(Message.prototype, 'collector', { - value: new MessageCollector(app.get('discord')), - writable: false, -}); - -logger.info('Monkey patched the following items:', ['User#tag', 'Message#collector'].join('\n')); diff --git a/src/util/patches/RequirePatch.ts b/src/util/patches/RequirePatch.ts deleted file mode 100644 index d6e2fdb4..00000000 --- a/src/util/patches/RequirePatch.ts +++ /dev/null @@ -1,35 +0,0 @@ -// I will not apply Nino's license here, -// since it's not mine! - -import { join } from 'path'; -import Module from 'module'; - -/** - * Patches the `require` function to support module aliases. This function - * is from [@aero/require](https://ravy.dev/aero/forks/require) by ravy but modified: - * - * - Add `@/*` for the root directory (in this case, it'll be `build/`) - * - Modify `~/*` for the src/ directory - * - * All credits belong to ravy! - */ -const requirePatch = () => { - Module.prototype.require = new Proxy(Module.prototype.require, { - apply(target, thisArg, args) { - const name = args[0]; - if (name.startsWith('~/')) { - const path = name.split('/').slice(1); - args[0] = join(process.cwd(), ...path); - } - - if (name.startsWith('@/')) { - const path = name.split('/').slice(1); - args[0] = join(process.cwd(), '..', ...path); - } - - return Reflect.apply(target, thisArg, args); - }, - }); -}; - -requirePatch(); diff --git a/tsconfig.json b/tsconfig.json deleted file mode 100644 index 56797ba7..00000000 --- a/tsconfig.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "extends": "@augu/tsconfig", - "compilerOptions": { - "moduleResolution": "node", - "typeRoots": ["./src/@types", "./node_modules/@types"], - "rootDir": "./src", - "types": ["node", "reflect-metadata"], - "outDir": "./build", - "skipLibCheck": true - }, - "exclude": ["node_modules"], - "include": ["src/**/*.ts"] -} diff --git a/yarn.lock b/yarn.lock deleted file mode 100644 index f840e608..00000000 --- a/yarn.lock +++ /dev/null @@ -1,2843 +0,0 @@ -# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. -# yarn lockfile v1 - - -"@augu/collections@1.0.3": - version "1.0.3" - resolved "https://registry.yarnpkg.com/@augu/collections/-/collections-1.0.3.tgz#435b9880777715fa33f0a9c4d8236d8dcb37d51b" - integrity sha512-/1rC3744iImEBMn84VK1Hk73PMcRMBT7UcNHCsxPFJa3Pwr2klmZd3bk+X3gT6bbeS21l51IARH/+jFp2i9W9Q== - -"@augu/collections@1.0.8": - version "1.0.8" - resolved "https://registry.yarnpkg.com/@augu/collections/-/collections-1.0.8.tgz#773f4bad2ed4000f007c05bbb98c431e2e01693d" - integrity sha512-N/cYv0ZdL5uyU2sx+HugleHIYN0WEDQUD/buFGgvC39lMUl0/V909h514EleJaFTLe2MG7Jrl6sVjhpQkbzldA== - -"@augu/collections@1.1.0": - version "1.1.0" - resolved "https://registry.yarnpkg.com/@augu/collections/-/collections-1.1.0.tgz#afa7a75fb397e94f7fc3c06c68a41ccf95fa3d9d" - integrity sha512-WEh0NYdKtmcJEZt3jf2PUGtb9Q5/taNaQmlVTIkFi9up7HNRX3uyGS61zwEHOEwPgUXxO1iDsjLwTKz2q1RqOw== - -"@augu/dotenv@1.3.0": - version "1.3.0" - resolved "https://registry.yarnpkg.com/@augu/dotenv/-/dotenv-1.3.0.tgz#b9c62df088b4d5b14af54e4c33774983ce1b9fba" - integrity sha512-TSp5nIyyrmsZLRu+aeciDk4ko8lfEFOu7AYnnubS2EOs8GdK378Q38I1nkti6b9goVN5Iz/YC2P2jgPXOOqpbg== - dependencies: - "@augu/collections" "1.0.3" - -"@augu/eslint-config@2.2.0": - version "2.2.0" - resolved "https://registry.yarnpkg.com/@augu/eslint-config/-/eslint-config-2.2.0.tgz#fb69f47dd11c7640408eb5e55b358710b20ef37e" - integrity sha512-VyrUTNdog2RdSynUddKNasfvONpJLFDJHYEQP7GKOzrlzLOEJrXE2x5cy6I6GFMiZbYoh/QEpcPv+fbTv9M2mA== - optionalDependencies: - "@typescript-eslint/eslint-plugin" "4.29.0" - "@typescript-eslint/parser" "4.29.0" - -"@augu/lilith@5.3.3": - version "5.3.3" - resolved "https://registry.yarnpkg.com/@augu/lilith/-/lilith-5.3.3.tgz#199763052e8a4c820b696cc223d4fea772a4e342" - integrity sha512-bOv0d7eHWoKwYpYxAhcr5Ux/pH4kJ74+au+fXgWU1NREgdk9ABexv759iUDU0iHuj6G5MNk3la5XNZFXnpK7Mw== - dependencies: - "@augu/collections" "1.1.0" - "@augu/utils" "1.5.5" - reflect-metadata "0.1.13" - -"@augu/orchid@3.1.1": - version "3.1.1" - resolved "https://registry.yarnpkg.com/@augu/orchid/-/orchid-3.1.1.tgz#0de1ee098cba3d64ce6903f8114d20dd35adbc12" - integrity sha512-1eYeun7FPd+3MegfFG59gRGitMWKlBr0JojNhAjx5R+at+uQKWH+JR91D9Bocw9jn06vZMTnFd6I9eG/XqhxIw== - dependencies: - "@augu/collections" "1.0.8" - "@augu/utils" "1.5.3" - form-data "4.0.0" - undici "3.3.6" - -"@augu/prettier-config@1.0.2": - version "1.0.2" - resolved "https://registry.yarnpkg.com/@augu/prettier-config/-/prettier-config-1.0.2.tgz#60d6bccff6d4d1902a9fb281fdaaa91e3e09d01a" - integrity sha512-hKFLHGisXNHAuv3F6tUKE2XJln9TA2Pn/w3KTcCwiIxMWkQFHbpfV8/3tcuWIke0V/pogU+muvTJPXlzdqIwgw== - -"@augu/tsconfig@1.1.1": - version "1.1.1" - resolved "https://registry.yarnpkg.com/@augu/tsconfig/-/tsconfig-1.1.1.tgz#1c3a80f0734749a63f85ebf5b22889de9ab2e976" - integrity sha512-qTqAK8+kTefw3PTixTFUHYATvl5inkFKnz3ByaYXO6P0prq5csA2T4weyVSWzR7dKL7rto9kHXnnN/8bTuPTKg== - -"@augu/utils@1.5.3": - version "1.5.3" - resolved "https://registry.yarnpkg.com/@augu/utils/-/utils-1.5.3.tgz#eb82352e2690b0467ef690885e6496f0e7a12da9" - integrity sha512-Pyu+JoK7f7AUjZvffCjf6ZLEpwa09ig7WNOn/ozzZDGeWNad9PMj8EsYzVsKLRyP/SnvbLDvpRWGEObgcACtYg== - -"@augu/utils@1.5.5": - version "1.5.5" - resolved "https://registry.yarnpkg.com/@augu/utils/-/utils-1.5.5.tgz#aea7f748c8254f4533dc394d190da470dcf0826a" - integrity sha512-MX3cUASPPttAU2LGzMcyw8Fr+HQFeJc+Re0Atem+KQwxd/dA3NM+Sk1A/6u1TQhUDm35ZmDRNORzInySabpkQg== - -"@augu/utils@1.5.6": - version "1.5.6" - resolved "https://registry.yarnpkg.com/@augu/utils/-/utils-1.5.6.tgz#655a5bc6d6452a33ca3a088d6efb49d43311454e" - integrity sha512-V252riszvwGlYrXEyXJKdCAi7M1kbCi2gTGF3Pd4d32riu/bo6gSUvdCHJsjjN9iEsLbWXad5ATUfzR5eSwEBg== - -"@cspotcode/source-map-consumer@0.8.0": - version "0.8.0" - resolved "https://registry.yarnpkg.com/@cspotcode/source-map-consumer/-/source-map-consumer-0.8.0.tgz#33bf4b7b39c178821606f669bbc447a6a629786b" - integrity sha512-41qniHzTU8yAGbCp04ohlmSrZf8bkf/iJsl3V0dRGsQN/5GFfx+LbCSsCpp2gqrqjTVg/K6O8ycoV35JIwAzAg== - -"@cspotcode/source-map-support@0.7.0": - version "0.7.0" - resolved "https://registry.yarnpkg.com/@cspotcode/source-map-support/-/source-map-support-0.7.0.tgz#4789840aa859e46d2f3173727ab707c66bf344f5" - integrity sha512-X4xqRHqN8ACt2aHVe51OxeA2HjbcL4MqFqXkrmQszJ1NOUuUu5u6Vqx/0lZSVNku7velL5FC/s5uEAj1lsBMhA== - dependencies: - "@cspotcode/source-map-consumer" "0.8.0" - -"@discordjs/collection@^0.3.2": - version "0.3.2" - resolved "https://registry.yarnpkg.com/@discordjs/collection/-/collection-0.3.2.tgz#3c271dd8a93dad89b186d330e24dbceaab58424a" - integrity sha512-dMjLl60b2DMqObbH1MQZKePgWhsNe49XkKBZ0W5Acl5uVV43SN414i2QfZwRI7dXAqIn8pEWD2+XXQFn9KWxqg== - -"@eslint/eslintrc@^1.0.5": - version "1.0.5" - resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-1.0.5.tgz#33f1b838dbf1f923bfa517e008362b78ddbbf318" - integrity sha512-BLxsnmK3KyPunz5wmCCpqy0YelEoxxGmH73Is+Z74oOTMtExcjkr3dDR6quwrjh1YspA8DH9gnX1o069KiS9AQ== - dependencies: - ajv "^6.12.4" - debug "^4.3.2" - espree "^9.2.0" - globals "^13.9.0" - ignore "^4.0.6" - import-fresh "^3.2.1" - js-yaml "^4.1.0" - minimatch "^3.0.4" - strip-json-comments "^3.1.1" - -"@fastify/ajv-compiler@^1.0.0": - version "1.1.0" - resolved "https://registry.yarnpkg.com/@fastify/ajv-compiler/-/ajv-compiler-1.1.0.tgz#5ce80b1fc8bebffc8c5ba428d5e392d0f9ed10a1" - integrity sha512-gvCOUNpXsWrIQ3A4aXCLIdblL0tDq42BG/2Xw7oxbil9h11uow10ztS2GuFazNBfjbrsZ5nl+nPl5jDSjj5TSg== - dependencies: - ajv "^6.12.6" - -"@humanwhocodes/config-array@^0.9.2": - version "0.9.2" - resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.9.2.tgz#68be55c737023009dfc5fe245d51181bb6476914" - integrity sha512-UXOuFCGcwciWckOpmfKDq/GyhlTf9pN/BzG//x8p8zTOFEcGuA68ANXheFS0AGvy3qgZqLBUkMs7hqzqCKOVwA== - dependencies: - "@humanwhocodes/object-schema" "^1.2.1" - debug "^4.1.1" - minimatch "^3.0.4" - -"@humanwhocodes/object-schema@^1.2.1": - version "1.2.1" - resolved "https://registry.yarnpkg.com/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz#b520529ec21d8e5945a1851dfd1c32e94e39ff45" - integrity sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA== - -"@nodelib/fs.scandir@2.1.5": - version "2.1.5" - resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz#7619c2eb21b25483f6d167548b4cfd5a7488c3d5" - integrity sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g== - dependencies: - "@nodelib/fs.stat" "2.0.5" - run-parallel "^1.1.9" - -"@nodelib/fs.stat@2.0.5", "@nodelib/fs.stat@^2.0.2": - version "2.0.5" - resolved "https://registry.yarnpkg.com/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz#5bd262af94e9d25bd1e71b05deed44876a222e8b" - integrity sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A== - -"@nodelib/fs.walk@^1.2.3": - version "1.2.8" - resolved "https://registry.yarnpkg.com/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz#e95737e8bb6746ddedf69c556953494f196fe69a" - integrity sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg== - dependencies: - "@nodelib/fs.scandir" "2.1.5" - fastq "^1.6.0" - -"@prisma/client@3.7.0": - version "3.7.0" - resolved "https://registry.yarnpkg.com/@prisma/client/-/client-3.7.0.tgz#9cafc105f12635c95e9b7e7b18e8fbf52cf3f18a" - integrity sha512-fUJMvBOX5C7JPc0e3CJD6Gbelbu4dMJB4ScYpiht8HMUnRShw20ULOipTopjNtl6ekHQJ4muI7pXlQxWS9nMbw== - dependencies: - "@prisma/engines-version" "3.7.0-31.8746e055198f517658c08a0c426c7eec87f5a85f" - -"@prisma/engines-version@3.7.0-31.8746e055198f517658c08a0c426c7eec87f5a85f": - version "3.7.0-31.8746e055198f517658c08a0c426c7eec87f5a85f" - resolved "https://registry.yarnpkg.com/@prisma/engines-version/-/engines-version-3.7.0-31.8746e055198f517658c08a0c426c7eec87f5a85f.tgz#055f36ac8b06c301332c14963cd0d6c795942c90" - integrity sha512-+qx2b+HK7BKF4VCa0LZ/t1QCXsu6SmvhUQyJkOD2aPpmOzket4fEnSKQZSB0i5tl7rwCDsvAiSeK8o7rf+yvwg== - -"@prisma/engines@3.7.0-31.8746e055198f517658c08a0c426c7eec87f5a85f": - version "3.7.0-31.8746e055198f517658c08a0c426c7eec87f5a85f" - resolved "https://registry.yarnpkg.com/@prisma/engines/-/engines-3.7.0-31.8746e055198f517658c08a0c426c7eec87f5a85f.tgz#12f28d5b78519fbd84c89a5bdff457ff5095e7a2" - integrity sha512-W549ub5NlgexNhR8EFstA/UwAWq3Zq0w9aNkraqsozVCt2CsX+lK4TK7IW5OZVSnxHwRjrgEAt3r9yPy8nZQRg== - -"@sentry/core@6.16.1": - version "6.16.1" - resolved "https://registry.yarnpkg.com/@sentry/core/-/core-6.16.1.tgz#d9f7a75f641acaddf21b6aafa7a32e142f68f17c" - integrity sha512-UFI0264CPUc5cR1zJH+S2UPOANpm6dLJOnsvnIGTjsrwzR0h8Hdl6rC2R/GPq+WNbnipo9hkiIwDlqbqvIU5vw== - dependencies: - "@sentry/hub" "6.16.1" - "@sentry/minimal" "6.16.1" - "@sentry/types" "6.16.1" - "@sentry/utils" "6.16.1" - tslib "^1.9.3" - -"@sentry/hub@6.16.1": - version "6.16.1" - resolved "https://registry.yarnpkg.com/@sentry/hub/-/hub-6.16.1.tgz#526e19db51f4412da8634734044c605b936a7b80" - integrity sha512-4PGtg6AfpqMkreTpL7ymDeQ/U1uXv03bKUuFdtsSTn/FRf9TLS4JB0KuTZCxfp1IRgAA+iFg6B784dDkT8R9eg== - dependencies: - "@sentry/types" "6.16.1" - "@sentry/utils" "6.16.1" - tslib "^1.9.3" - -"@sentry/minimal@6.16.1": - version "6.16.1" - resolved "https://registry.yarnpkg.com/@sentry/minimal/-/minimal-6.16.1.tgz#6a9506a92623d2ff1fc17d60989688323326772e" - integrity sha512-dq+mI1EQIvUM+zJtGCVgH3/B3Sbx4hKlGf2Usovm9KoqWYA+QpfVBholYDe/H2RXgO7LFEefDLvOdHDkqeJoyA== - dependencies: - "@sentry/hub" "6.16.1" - "@sentry/types" "6.16.1" - tslib "^1.9.3" - -"@sentry/node@6.16.1": - version "6.16.1" - resolved "https://registry.yarnpkg.com/@sentry/node/-/node-6.16.1.tgz#d92916da3e95d23e1ada274e97d6bf369e74ac51" - integrity sha512-SeDDoug2kUxeF1D7JGPa3h5EXxKtmA01mITBPYx5xbJ0sMksnv5I5bC1SJ8arRRzq6+W1C4IEeDBQtrVCk6ixA== - dependencies: - "@sentry/core" "6.16.1" - "@sentry/hub" "6.16.1" - "@sentry/tracing" "6.16.1" - "@sentry/types" "6.16.1" - "@sentry/utils" "6.16.1" - cookie "^0.4.1" - https-proxy-agent "^5.0.0" - lru_map "^0.3.3" - tslib "^1.9.3" - -"@sentry/tracing@6.16.1": - version "6.16.1" - resolved "https://registry.yarnpkg.com/@sentry/tracing/-/tracing-6.16.1.tgz#32fba3e07748e9a955055afd559a65996acb7d71" - integrity sha512-MPSbqXX59P+OEeST+U2V/8Hu/8QjpTUxTNeNyTHWIbbchdcMMjDbXTS3etCgajZR6Ro+DHElOz5cdSxH6IBGlA== - dependencies: - "@sentry/hub" "6.16.1" - "@sentry/minimal" "6.16.1" - "@sentry/types" "6.16.1" - "@sentry/utils" "6.16.1" - tslib "^1.9.3" - -"@sentry/types@6.16.1": - version "6.16.1" - resolved "https://registry.yarnpkg.com/@sentry/types/-/types-6.16.1.tgz#4917607115b30315757c2cf84f80bac5100b8ac0" - integrity sha512-Wh354g30UsJ5kYJbercektGX4ZMc9MHU++1NjeN2bTMnbofEcpUDWIiKeulZEY65IC1iU+1zRQQgtYO+/hgCUQ== - -"@sentry/utils@6.16.1": - version "6.16.1" - resolved "https://registry.yarnpkg.com/@sentry/utils/-/utils-6.16.1.tgz#1b9e14c2831b6e8b816f7021b9876133bf2be008" - integrity sha512-7ngq/i4R8JZitJo9Sl8PDnjSbDehOxgr1vsoMmerIsyRZ651C/8B+jVkMhaAPgSdyJ0AlE3O7DKKTP1FXFw9qw== - dependencies: - "@sentry/types" "6.16.1" - tslib "^1.9.3" - -"@sindresorhus/is@^0.14.0": - version "0.14.0" - resolved "https://registry.yarnpkg.com/@sindresorhus/is/-/is-0.14.0.tgz#9fb3a3cf3132328151f353de4632e01e52102bea" - integrity sha512-9NET910DNaIPngYnLLPeg+Ogzqsi9uM4mSboU5y6p8S5DzMTVEsJZrawi+BoDNUVBa2DhJqQYUFvMDfgU062LQ== - -"@sqltools/formatter@1.2.2": - version "1.2.2" - resolved "https://registry.yarnpkg.com/@sqltools/formatter/-/formatter-1.2.2.tgz#9390a8127c0dcba61ebd7fdcc748655e191bdd68" - integrity sha512-/5O7Fq6Vnv8L6ucmPjaWbVG1XkP4FO+w5glqfkIsq3Xw4oyNAdJddbnYodNDAfjVUvo/rrSCTom4kAND7T1o5Q== - -"@szmarczak/http-timer@^1.1.2": - version "1.1.2" - resolved "https://registry.yarnpkg.com/@szmarczak/http-timer/-/http-timer-1.1.2.tgz#b1665e2c461a2cd92f4c1bbf50d5454de0d4b421" - integrity sha512-XIB2XbzHTN6ieIjfIMV9hlVcfPU26s2vafYWQcZHWXHOxiaRZYEDKEwdl129Zyg50+foYV2jCgtrqSA6qNuNSA== - dependencies: - defer-to-connect "^1.0.1" - -"@tsconfig/node10@^1.0.7": - version "1.0.8" - resolved "https://registry.yarnpkg.com/@tsconfig/node10/-/node10-1.0.8.tgz#c1e4e80d6f964fbecb3359c43bd48b40f7cadad9" - integrity sha512-6XFfSQmMgq0CFLY1MslA/CPUfhIL919M1rMsa5lP2P097N2Wd1sSX0tx1u4olM16fLNhtHZpRhedZJphNJqmZg== - -"@tsconfig/node12@^1.0.7": - version "1.0.9" - resolved "https://registry.yarnpkg.com/@tsconfig/node12/-/node12-1.0.9.tgz#62c1f6dee2ebd9aead80dc3afa56810e58e1a04c" - integrity sha512-/yBMcem+fbvhSREH+s14YJi18sp7J9jpuhYByADT2rypfajMZZN4WQ6zBGgBKp53NKmqI36wFYDb3yaMPurITw== - -"@tsconfig/node14@^1.0.0": - version "1.0.1" - resolved "https://registry.yarnpkg.com/@tsconfig/node14/-/node14-1.0.1.tgz#95f2d167ffb9b8d2068b0b235302fafd4df711f2" - integrity sha512-509r2+yARFfHHE7T6Puu2jjkoycftovhXRqW328PDXTVGKihlb1P8Z9mMZH04ebyajfRY7dedfGynlrFHJUQCg== - -"@tsconfig/node16@^1.0.2": - version "1.0.2" - resolved "https://registry.yarnpkg.com/@tsconfig/node16/-/node16-1.0.2.tgz#423c77877d0569db20e1fc80885ac4118314010e" - integrity sha512-eZxlbI8GZscaGS7kkc/trHTT5xgrjH3/1n2JDwusC9iahPKWMRvRjJSAN5mCXviuTGQ/lHnhvv8Q1YTpnfz9gA== - -"@types/ioredis@4.28.5": - version "4.28.5" - resolved "https://registry.yarnpkg.com/@types/ioredis/-/ioredis-4.28.5.tgz#933aa76dd0b66147be48f94967e2571ff848408e" - integrity sha512-bp5mdpzscWZMEE/jLvvzze5TZFYGhynB1am69l/a0XPqZRXWpbswY6lb5buEht57jOnw5pPG5zL9pFUWw1nggw== - dependencies: - "@types/node" "*" - -"@types/js-yaml@4.0.5": - version "4.0.5" - resolved "https://registry.yarnpkg.com/@types/js-yaml/-/js-yaml-4.0.5.tgz#738dd390a6ecc5442f35e7f03fa1431353f7e138" - integrity sha512-FhpRzf927MNQdRZP0J5DLIdTXhjLYzeUTmLAu69mnVksLH9CJY3IuSeEgbKUki7GQZm0WqDkGzyxju2EZGD2wA== - -"@types/json-schema@^7.0.7", "@types/json-schema@^7.0.9": - version "7.0.9" - resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.9.tgz#97edc9037ea0c38585320b28964dde3b39e4660d" - integrity sha512-qcUXuemtEu+E5wZSJHNxUXeCZhAfXKQ41D+duX+VYPde7xyEVZci+/oXKJL13tnRs9lR2pr4fod59GT6/X1/yQ== - -"@types/luxon@2.0.8": - version "2.0.8" - resolved "https://registry.yarnpkg.com/@types/luxon/-/luxon-2.0.8.tgz#a0fdd7ab0b67e08bf1d301232a7fef79b74ded69" - integrity sha512-lGmxL6hMEVqXr8w9bL52RUWXVu90o7vH8WQSutQssr2e+w0TNttXx2Zfw2V2lHHHWfW6OGqB8bXDvtKocv19qQ== - -"@types/ms@0.7.31": - version "0.7.31" - resolved "https://registry.yarnpkg.com/@types/ms/-/ms-0.7.31.tgz#31b7ca6407128a3d2bbc27fe2d21b345397f6197" - integrity sha512-iiUgKzV9AuaEkZqkOLDIvlQiL6ltuZd9tGcW3gwpnX8JbuiuhFlEGmmFXEXkN50Cvq7Os88IY2v0dkDqXYWVgA== - -"@types/node@*": - version "16.7.13" - resolved "https://registry.yarnpkg.com/@types/node/-/node-16.7.13.tgz#86fae356b03b5a12f2506c6cf6cd9287b205973f" - integrity sha512-pLUPDn+YG3FYEt/pHI74HmnJOWzeR+tOIQzUx93pi9M7D8OE7PSLr97HboXwk5F+JS+TLtWuzCOW97AHjmOXXA== - -"@types/node@16.11.17": - version "16.11.17" - resolved "https://registry.yarnpkg.com/@types/node/-/node-16.11.17.tgz#ae146499772e33fc6382e1880bc567e41a528586" - integrity sha512-C1vTZME8cFo8uxY2ui41xcynEotVkczIVI5AjLmy5pkpBv/FtG+jhtOlfcPysI8VRVwoOMv6NJm44LGnoMSWkw== - -"@types/ws@8.2.2": - version "8.2.2" - resolved "https://registry.yarnpkg.com/@types/ws/-/ws-8.2.2.tgz#7c5be4decb19500ae6b3d563043cd407bf366c21" - integrity sha512-NOn5eIcgWLOo6qW8AcuLZ7G8PycXu0xTxxkS6Q18VWFxgPUSOwV0pBj2a/4viNZVu25i7RIB7GttdkAIUUXOOg== - dependencies: - "@types/node" "*" - -"@typescript-eslint/eslint-plugin@4.29.0": - version "4.29.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-4.29.0.tgz#b866c9cd193bfaba5e89bade0015629ebeb27996" - integrity sha512-eiREtqWRZ8aVJcNru7cT/AMVnYd9a2UHsfZT8MR1dW3UUEg6jDv9EQ9Cq4CUPZesyQ58YUpoAADGv71jY8RwgA== - dependencies: - "@typescript-eslint/experimental-utils" "4.29.0" - "@typescript-eslint/scope-manager" "4.29.0" - debug "^4.3.1" - functional-red-black-tree "^1.0.1" - regexpp "^3.1.0" - semver "^7.3.5" - tsutils "^3.21.0" - -"@typescript-eslint/eslint-plugin@5.8.1": - version "5.8.1" - resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.8.1.tgz#97dfaa39f38e99f86801fdf34f9f1bed66704258" - integrity sha512-wTZ5oEKrKj/8/366qTM366zqhIKAp6NCMweoRONtfuC07OAU9nVI2GZZdqQ1qD30WAAtcPdkH+npDwtRFdp4Rw== - dependencies: - "@typescript-eslint/experimental-utils" "5.8.1" - "@typescript-eslint/scope-manager" "5.8.1" - debug "^4.3.2" - functional-red-black-tree "^1.0.1" - ignore "^5.1.8" - regexpp "^3.2.0" - semver "^7.3.5" - tsutils "^3.21.0" - -"@typescript-eslint/experimental-utils@4.29.0": - version "4.29.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/experimental-utils/-/experimental-utils-4.29.0.tgz#19b1417602d0e1ef325b3312ee95f61220542df5" - integrity sha512-FpNVKykfeaIxlArLUP/yQfv/5/3rhl1ov6RWgud4OgbqWLkEq7lqgQU9iiavZRzpzCRQV4XddyFz3wFXdkiX9w== - dependencies: - "@types/json-schema" "^7.0.7" - "@typescript-eslint/scope-manager" "4.29.0" - "@typescript-eslint/types" "4.29.0" - "@typescript-eslint/typescript-estree" "4.29.0" - eslint-scope "^5.1.1" - eslint-utils "^3.0.0" - -"@typescript-eslint/experimental-utils@5.8.1": - version "5.8.1" - resolved "https://registry.yarnpkg.com/@typescript-eslint/experimental-utils/-/experimental-utils-5.8.1.tgz#01861eb2f0749f07d02db342b794145a66ed346f" - integrity sha512-fbodVnjIDU4JpeXWRDsG5IfIjYBxEvs8EBO8W1+YVdtrc2B9ppfof5sZhVEDOtgTfFHnYQJDI8+qdqLYO4ceww== - dependencies: - "@types/json-schema" "^7.0.9" - "@typescript-eslint/scope-manager" "5.8.1" - "@typescript-eslint/types" "5.8.1" - "@typescript-eslint/typescript-estree" "5.8.1" - eslint-scope "^5.1.1" - eslint-utils "^3.0.0" - -"@typescript-eslint/parser@4.29.0": - version "4.29.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-4.29.0.tgz#e5367ca3c63636bb5d8e0748fcbab7a4f4a04289" - integrity sha512-+92YRNHFdXgq+GhWQPT2bmjX09X7EH36JfgN2/4wmhtwV/HPxozpCNst8jrWcngLtEVd/4zAwA6BKojAlf+YqA== - dependencies: - "@typescript-eslint/scope-manager" "4.29.0" - "@typescript-eslint/types" "4.29.0" - "@typescript-eslint/typescript-estree" "4.29.0" - debug "^4.3.1" - -"@typescript-eslint/parser@5.8.1": - version "5.8.1" - resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-5.8.1.tgz#380f5f1e596b540059998aa3fc80d78f0f9b0d0a" - integrity sha512-K1giKHAjHuyB421SoXMXFHHVI4NdNY603uKw92++D3qyxSeYvC10CBJ/GE5Thpo4WTUvu1mmJI2/FFkz38F2Gw== - dependencies: - "@typescript-eslint/scope-manager" "5.8.1" - "@typescript-eslint/types" "5.8.1" - "@typescript-eslint/typescript-estree" "5.8.1" - debug "^4.3.2" - -"@typescript-eslint/scope-manager@4.29.0": - version "4.29.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-4.29.0.tgz#cf5474f87321bedf416ef65839b693bddd838599" - integrity sha512-HPq7XAaDMM3DpmuijxLV9Io8/6pQnliiXMQUcAdjpJJSR+fdmbD/zHCd7hMkjJn04UQtCQBtshgxClzg6NIS2w== - dependencies: - "@typescript-eslint/types" "4.29.0" - "@typescript-eslint/visitor-keys" "4.29.0" - -"@typescript-eslint/scope-manager@5.8.1": - version "5.8.1" - resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-5.8.1.tgz#7fc0604f7ade8833e4d42cebaa1e2debf8b932e4" - integrity sha512-DGxJkNyYruFH3NIZc3PwrzwOQAg7vvgsHsHCILOLvUpupgkwDZdNq/cXU3BjF4LNrCsVg0qxEyWasys5AiJ85Q== - dependencies: - "@typescript-eslint/types" "5.8.1" - "@typescript-eslint/visitor-keys" "5.8.1" - -"@typescript-eslint/types@4.29.0": - version "4.29.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-4.29.0.tgz#c8f1a1e4441ea4aca9b3109241adbc145f7f8a4e" - integrity sha512-2YJM6XfWfi8pgU2HRhTp7WgRw78TCRO3dOmSpAvIQ8MOv4B46JD2chnhpNT7Jq8j0APlIbzO1Bach734xxUl4A== - -"@typescript-eslint/types@5.8.1": - version "5.8.1" - resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-5.8.1.tgz#04c6b49ebc8c99238238a6b8b43f2fc613983b5a" - integrity sha512-L/FlWCCgnjKOLefdok90/pqInkomLnAcF9UAzNr+DSqMC3IffzumHTQTrINXhP1gVp9zlHiYYjvozVZDPleLcA== - -"@typescript-eslint/typescript-estree@4.29.0": - version "4.29.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-4.29.0.tgz#af7ab547757b86c91bfdbc54ff86845410856256" - integrity sha512-8ZpNHDIOyqzzgZrQW9+xQ4k5hM62Xy2R4RPO3DQxMc5Rq5QkCdSpk/drka+DL9w6sXNzV5nrdlBmf8+x495QXQ== - dependencies: - "@typescript-eslint/types" "4.29.0" - "@typescript-eslint/visitor-keys" "4.29.0" - debug "^4.3.1" - globby "^11.0.3" - is-glob "^4.0.1" - semver "^7.3.5" - tsutils "^3.21.0" - -"@typescript-eslint/typescript-estree@5.8.1": - version "5.8.1" - resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-5.8.1.tgz#a592855be688e7b729a1e9411d7d74ec992ed6ef" - integrity sha512-26lQ8l8tTbG7ri7xEcCFT9ijU5Fk+sx/KRRyyzCv7MQ+rZZlqiDPtMKWLC8P7o+dtCnby4c+OlxuX1tp8WfafQ== - dependencies: - "@typescript-eslint/types" "5.8.1" - "@typescript-eslint/visitor-keys" "5.8.1" - debug "^4.3.2" - globby "^11.0.4" - is-glob "^4.0.3" - semver "^7.3.5" - tsutils "^3.21.0" - -"@typescript-eslint/visitor-keys@4.29.0": - version "4.29.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-4.29.0.tgz#1ff60f240def4d85ea68d4fd2e4e9759b7850c04" - integrity sha512-LoaofO1C/jAJYs0uEpYMXfHboGXzOJeV118X4OsZu9f7rG7Pr9B3+4HTU8+err81rADa4xfQmAxnRnPAI2jp+Q== - dependencies: - "@typescript-eslint/types" "4.29.0" - eslint-visitor-keys "^2.0.0" - -"@typescript-eslint/visitor-keys@5.8.1": - version "5.8.1" - resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-5.8.1.tgz#58a2c566265d5511224bc316149890451c1bbab0" - integrity sha512-SWgiWIwocK6NralrJarPZlWdr0hZnj5GXHIgfdm8hNkyKvpeQuFyLP6YjSIe9kf3YBIfU6OHSZLYkQ+smZwtNg== - dependencies: - "@typescript-eslint/types" "5.8.1" - eslint-visitor-keys "^3.0.0" - -abbrev@1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.1.1.tgz#f8f2c887ad10bf67f634f005b6987fed3179aac8" - integrity sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q== - -abstract-logging@^2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/abstract-logging/-/abstract-logging-2.0.1.tgz#6b0c371df212db7129b57d2e7fcf282b8bf1c839" - integrity sha512-2BjRTZxTPvheOvGbBslFSYOUkr+SjPtOnrLP33f+VIWLzezQpZcqVg7ja3L4dBXmzzgwT+a029jRx5PCi3JuiA== - -acorn-jsx@^5.3.1: - version "5.3.2" - resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.3.2.tgz#7ed5bb55908b3b2f1bc55c6af1653bada7f07937" - integrity sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ== - -acorn-walk@^8.1.1: - version "8.2.0" - resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-8.2.0.tgz#741210f2e2426454508853a2f44d0ab83b7f69c1" - integrity sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA== - -acorn@^8.4.1: - version "8.5.0" - resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.5.0.tgz#4512ccb99b3698c752591e9bb4472e38ad43cee2" - integrity sha512-yXbYeFy+jUuYd3/CDcg2NkIYE991XYX/bje7LmjJigUciaeO1JR4XxXgCIV1/Zc/dRuFEyw1L0pbA+qynJkW5Q== - -acorn@^8.6.0: - version "8.6.0" - resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.6.0.tgz#e3692ba0eb1a0c83eaa4f37f5fa7368dd7142895" - integrity sha512-U1riIR+lBSNi3IbxtaHOIKdH8sLFv3NYfNv8sg7ZsNhcfl4HF2++BfqqrNAxoCLQW1iiylOj76ecnaUxz+z9yw== - -acorn@^8.7.0: - version "8.7.0" - resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.7.0.tgz#90951fde0f8f09df93549481e5fc141445b791cf" - integrity sha512-V/LGr1APy+PXIwKebEWrkZPwoeoF+w1jiOBUmuxuiUIaOHtob8Qc9BTrYo7VuI5fR8tqsy+buA2WFooR5olqvQ== - -agent-base@6: - version "6.0.2" - resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-6.0.2.tgz#49fff58577cfee3f37176feab4c22e00f86d7f77" - integrity sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ== - dependencies: - debug "4" - -ajv@^6.10.0, ajv@^6.11.0, ajv@^6.12.4, ajv@^6.12.6: - version "6.12.6" - resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4" - integrity sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g== - dependencies: - fast-deep-equal "^3.1.1" - fast-json-stable-stringify "^2.0.0" - json-schema-traverse "^0.4.1" - uri-js "^4.2.2" - -ajv@^8.1.0: - version "8.6.2" - resolved "https://registry.yarnpkg.com/ajv/-/ajv-8.6.2.tgz#2fb45e0e5fcbc0813326c1c3da535d1881bb0571" - integrity sha512-9807RlWAgT564wT+DjeyU5OFMPjmzxVobvDFmNAhY+5zD6A2ly3jDp6sgnfyDtlIQ+7H97oc/DGCzzfu9rjw9w== - dependencies: - fast-deep-equal "^3.1.1" - json-schema-traverse "^1.0.0" - require-from-string "^2.0.2" - uri-js "^4.2.2" - -ansi-align@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/ansi-align/-/ansi-align-3.0.0.tgz#b536b371cf687caaef236c18d3e21fe3797467cb" - integrity sha512-ZpClVKqXN3RGBmKibdfWzqCY4lnjEuoNzU5T0oEFpfd/z5qJHVarukridD4juLO2FXMiwUQxr9WqQtaYa8XRYw== - dependencies: - string-width "^3.0.0" - -ansi-colors@^4.1.1: - version "4.1.1" - resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-4.1.1.tgz#cbb9ae256bf750af1eab344f229aa27fe94ba348" - integrity sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA== - -ansi-regex@^2.0.0: - version "2.1.1" - resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-2.1.1.tgz#c3b33ab5ee360d86e0e628f0468ae7ef27d654df" - integrity sha1-w7M6te42DYbg5ijwRorn7yfWVN8= - -ansi-regex@^4.1.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-4.1.0.tgz#8b9f8f08cf1acb843756a839ca8c7e3168c51997" - integrity sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg== - -ansi-regex@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.0.tgz#388539f55179bf39339c81af30a654d69f87cb75" - integrity sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg== - -ansi-regex@^5.0.1: - version "5.0.1" - resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.1.tgz#082cb2c89c9fe8659a311a53bd6a4dc5301db304" - integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ== - -ansi-styles@^2.2.1: - version "2.2.1" - resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-2.2.1.tgz#b432dd3358b634cf75e1e4664368240533c1ddbe" - integrity sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4= - -ansi-styles@^4.0.0, ansi-styles@^4.1.0: - version "4.3.0" - resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.3.0.tgz#edd803628ae71c04c85ae7a0906edad34b648937" - integrity sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg== - dependencies: - color-convert "^2.0.1" - -any-promise@^1.0.0: - version "1.3.0" - resolved "https://registry.yarnpkg.com/any-promise/-/any-promise-1.3.0.tgz#abc6afeedcea52e809cdc0376aed3ce39635d17f" - integrity sha1-q8av7tzqUugJzcA3au0845Y10X8= - -anymatch@~3.1.2: - version "3.1.2" - resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.2.tgz#c0557c096af32f106198f4f4e2a383537e378716" - integrity sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg== - dependencies: - normalize-path "^3.0.0" - picomatch "^2.0.4" - -app-root-path@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/app-root-path/-/app-root-path-3.0.0.tgz#210b6f43873227e18a4b810a032283311555d5ad" - integrity sha512-qMcx+Gy2UZynHjOHOIXPNvpf+9cjvk3cWrBBK7zg4gH9+clobJRb9NGzcT7mQTcV/6Gm/1WelUtqxVXnNlrwcw== - -archy@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/archy/-/archy-1.0.0.tgz#f9c8c13757cc1dd7bc379ac77b2c62a5c2868c40" - integrity sha1-+cjBN1fMHde8N5rHeyxipcKGjEA= - -arg@^4.1.0: - version "4.1.3" - resolved "https://registry.yarnpkg.com/arg/-/arg-4.1.3.tgz#269fc7ad5b8e42cb63c896d5666017261c144089" - integrity sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA== - -argparse@^1.0.7: - version "1.0.10" - resolved "https://registry.yarnpkg.com/argparse/-/argparse-1.0.10.tgz#bcd6791ea5ae09725e17e5ad988134cd40b3d911" - integrity sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg== - dependencies: - sprintf-js "~1.0.2" - -argparse@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/argparse/-/argparse-2.0.1.tgz#246f50f3ca78a3240f6c997e8a9bd1eac49e4b38" - integrity sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q== - -array-union@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/array-union/-/array-union-2.1.0.tgz#b798420adbeb1de828d84acd8a2e23d3efe85e8d" - integrity sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw== - -asynckit@^0.4.0: - version "0.4.0" - resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" - integrity sha1-x57Zf380y48robyXkLzDZkdLS3k= - -atomic-sleep@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/atomic-sleep/-/atomic-sleep-1.0.0.tgz#eb85b77a601fc932cfe432c5acd364a9e2c9075b" - integrity sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ== - -avvio@^7.1.2: - version "7.2.2" - resolved "https://registry.yarnpkg.com/avvio/-/avvio-7.2.2.tgz#58e00e7968870026cd7b7d4f689d596db629e251" - integrity sha512-XW2CMCmZaCmCCsIaJaLKxAzPwF37fXi1KGxNOvedOpeisLdmxZnblGc3hpHWYnlP+KOUxZsazh43WXNHgXpbqw== - dependencies: - archy "^1.0.0" - debug "^4.0.0" - fastq "^1.6.1" - queue-microtask "^1.1.2" - -balanced-match@^1.0.0: - version "1.0.2" - resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" - integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== - -base64-js@^1.3.1: - version "1.5.1" - resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a" - integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA== - -binary-extensions@^2.0.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.2.0.tgz#75f502eeaf9ffde42fc98829645be4ea76bd9e2d" - integrity sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA== - -bintrees@1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/bintrees/-/bintrees-1.0.1.tgz#0e655c9b9c2435eaab68bf4027226d2b55a34524" - integrity sha1-DmVcm5wkNeqraL9AJyJtK1WjRSQ= - -boxen@^5.0.0: - version "5.1.2" - resolved "https://registry.yarnpkg.com/boxen/-/boxen-5.1.2.tgz#788cb686fc83c1f486dfa8a40c68fc2b831d2b50" - integrity sha512-9gYgQKXx+1nP8mP7CzFyaUARhg7D3n1dF/FnErWmu9l6JvGpNUN278h0aSb+QjoiKSWG+iZ3uHrcqk0qrY9RQQ== - dependencies: - ansi-align "^3.0.0" - camelcase "^6.2.0" - chalk "^4.1.0" - cli-boxes "^2.2.1" - string-width "^4.2.2" - type-fest "^0.20.2" - widest-line "^3.1.0" - wrap-ansi "^7.0.0" - -brace-expansion@^1.1.7: - version "1.1.11" - resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" - integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA== - dependencies: - balanced-match "^1.0.0" - concat-map "0.0.1" - -braces@^3.0.1, braces@~3.0.2: - version "3.0.2" - resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107" - integrity sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A== - dependencies: - fill-range "^7.0.1" - -buffer-from@^1.0.0: - version "1.1.2" - resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.2.tgz#2b146a6fd72e80b4f55d255f35ed59a3a9a41bd5" - integrity sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ== - -buffer-writer@2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/buffer-writer/-/buffer-writer-2.0.0.tgz#ce7eb81a38f7829db09c873f2fbb792c0c98ec04" - integrity sha512-a7ZpuTZU1TRtnwyCNW3I5dc0wWNC3VR9S++Ewyk2HHZdrO3CQJqSpd+95Us590V6AL7JqUAH2IwZ/398PmNFgw== - -buffer@^5.5.0: - version "5.7.1" - resolved "https://registry.yarnpkg.com/buffer/-/buffer-5.7.1.tgz#ba62e7c13133053582197160851a8f648e99eed0" - integrity sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ== - dependencies: - base64-js "^1.3.1" - ieee754 "^1.1.13" - -cacheable-request@^6.0.0: - version "6.1.0" - resolved "https://registry.yarnpkg.com/cacheable-request/-/cacheable-request-6.1.0.tgz#20ffb8bd162ba4be11e9567d823db651052ca912" - integrity sha512-Oj3cAGPCqOZX7Rz64Uny2GYAZNliQSqfbePrgAQ1wKAihYmCUnraBtJtKcGR4xz7wF+LoJC+ssFZvv5BgF9Igg== - dependencies: - clone-response "^1.0.2" - get-stream "^5.1.0" - http-cache-semantics "^4.0.0" - keyv "^3.0.0" - lowercase-keys "^2.0.0" - normalize-url "^4.1.0" - responselike "^1.0.2" - -callsites@^3.0.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/callsites/-/callsites-3.1.0.tgz#b3630abd8943432f54b3f0519238e33cd7df2f73" - integrity sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ== - -camelcase@^6.2.0: - version "6.2.0" - resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-6.2.0.tgz#924af881c9d525ac9d87f40d964e5cea982a1809" - integrity sha512-c7wVvbw3f37nuobQNtgsgG9POC9qMbNuMQmTCqZv23b6MIz0fcYpBiOlv9gEN/hdLdnZTDQhg6e9Dq5M1vKvfg== - -chalk@^1.1.1: - version "1.1.3" - resolved "https://registry.yarnpkg.com/chalk/-/chalk-1.1.3.tgz#a8115c55e4a702fe4d150abd3872822a7e09fc98" - integrity sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg= - dependencies: - ansi-styles "^2.2.1" - escape-string-regexp "^1.0.2" - has-ansi "^2.0.0" - strip-ansi "^3.0.0" - supports-color "^2.0.0" - -chalk@^4.0.0, chalk@^4.1.0: - version "4.1.2" - resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01" - integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA== - dependencies: - ansi-styles "^4.1.0" - supports-color "^7.1.0" - -chokidar@^3.5.2: - version "3.5.2" - resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.5.2.tgz#dba3976fcadb016f66fd365021d91600d01c1e75" - integrity sha512-ekGhOnNVPgT77r4K/U3GDhu+FQ2S8TnK/s2KbIGXi0SZWuwkZ2QNyfWdZW+TVfn84DpEP7rLeCt2UI6bJ8GwbQ== - dependencies: - anymatch "~3.1.2" - braces "~3.0.2" - glob-parent "~5.1.2" - is-binary-path "~2.1.0" - is-glob "~4.0.1" - normalize-path "~3.0.0" - readdirp "~3.6.0" - optionalDependencies: - fsevents "~2.3.2" - -ci-info@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-2.0.0.tgz#67a9e964be31a51e15e5010d58e6f12834002f46" - integrity sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ== - -cli-boxes@^2.2.1: - version "2.2.1" - resolved "https://registry.yarnpkg.com/cli-boxes/-/cli-boxes-2.2.1.tgz#ddd5035d25094fce220e9cab40a45840a440318f" - integrity sha512-y4coMcylgSCdVinjiDBuR8PCC2bLjyGTwEmPb9NHR/QaNU6EUOXcTY/s6VjGMD6ENSEaeQYHCY0GNGS5jfMwPw== - -cli-highlight@^2.1.10: - version "2.1.11" - resolved "https://registry.yarnpkg.com/cli-highlight/-/cli-highlight-2.1.11.tgz#49736fa452f0aaf4fae580e30acb26828d2dc1bf" - integrity sha512-9KDcoEVwyUXrjcJNvHD0NFc/hiwe/WPVYIleQh2O1N2Zro5gWJZ/K+3DGn8w8P/F6FxOgzyC5bxDyHIgCSPhGg== - dependencies: - chalk "^4.0.0" - highlight.js "^10.7.1" - mz "^2.4.0" - parse5 "^5.1.1" - parse5-htmlparser2-tree-adapter "^6.0.0" - yargs "^16.0.0" - -cliui@^7.0.2: - version "7.0.4" - resolved "https://registry.yarnpkg.com/cliui/-/cliui-7.0.4.tgz#a0265ee655476fc807aea9df3df8df7783808b4f" - integrity sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ== - dependencies: - string-width "^4.2.0" - strip-ansi "^6.0.0" - wrap-ansi "^7.0.0" - -clone-response@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/clone-response/-/clone-response-1.0.2.tgz#d1dc973920314df67fbeb94223b4ee350239e96b" - integrity sha1-0dyXOSAxTfZ/vrlCI7TuNQI56Ws= - dependencies: - mimic-response "^1.0.0" - -cluster-key-slot@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/cluster-key-slot/-/cluster-key-slot-1.1.0.tgz#30474b2a981fb12172695833052bc0d01336d10d" - integrity sha512-2Nii8p3RwAPiFwsnZvukotvow2rIHM+yQ6ZcBXGHdniadkYGZYiGmkHJIbZPIV9nfv7m/U1IPMVVcAhoWFeklw== - -color-convert@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-2.0.1.tgz#72d3a68d598c9bdb3af2ad1e84f21d896abd4de3" - integrity sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ== - dependencies: - color-name "~1.1.4" - -color-name@~1.1.4: - version "1.1.4" - resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" - integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== - -combined-stream@^1.0.8: - version "1.0.8" - resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f" - integrity sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg== - dependencies: - delayed-stream "~1.0.0" - -concat-map@0.0.1: - version "0.0.1" - resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" - integrity sha1-2Klr13/Wjfd5OnMDajug1UBdR3s= - -configstore@^5.0.1: - version "5.0.1" - resolved "https://registry.yarnpkg.com/configstore/-/configstore-5.0.1.tgz#d365021b5df4b98cdd187d6a3b0e3f6a7cc5ed96" - integrity sha512-aMKprgk5YhBNyH25hj8wGt2+D52Sw1DRRIzqBwLp2Ya9mFmY8KPvvtvmna8SxVR9JMZ4kzMD68N22vlaRpkeFA== - dependencies: - dot-prop "^5.2.0" - graceful-fs "^4.1.2" - make-dir "^3.0.0" - unique-string "^2.0.0" - write-file-atomic "^3.0.0" - xdg-basedir "^4.0.0" - -cookie@^0.4.0, cookie@^0.4.1: - version "0.4.1" - resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.4.1.tgz#afd713fe26ebd21ba95ceb61f9a8116e50a537d1" - integrity sha512-ZwrFkGJxUR3EIoXtO+yVE69Eb7KlixbaeAWfBQB9vVsNn/o+Yw69gBWSSDK825hQNdN+wF8zELf3dFNl/kxkUA== - -create-require@^1.1.0: - version "1.1.1" - resolved "https://registry.yarnpkg.com/create-require/-/create-require-1.1.1.tgz#c1d7e8f1e5f6cfc9ff65f9cd352d37348756c333" - integrity sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ== - -cross-spawn@^7.0.2: - version "7.0.3" - resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6" - integrity sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w== - dependencies: - path-key "^3.1.0" - shebang-command "^2.0.0" - which "^2.0.1" - -crypto-random-string@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/crypto-random-string/-/crypto-random-string-2.0.0.tgz#ef2a7a966ec11083388369baa02ebead229b30d5" - integrity sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA== - -debug@4, debug@^4.0.0, debug@^4.1.1, debug@^4.3.1, debug@^4.3.2: - version "4.3.2" - resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.2.tgz#f0a49c18ac8779e31d4a0c6029dfb76873c7428b" - integrity sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw== - dependencies: - ms "2.1.2" - -debug@^3.2.7: - version "3.2.7" - resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.7.tgz#72580b7e9145fb39b6676f9c5e5fb100b934179a" - integrity sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ== - dependencies: - ms "^2.1.1" - -decompress-response@^3.3.0: - version "3.3.0" - resolved "https://registry.yarnpkg.com/decompress-response/-/decompress-response-3.3.0.tgz#80a4dd323748384bfa248083622aedec982adff3" - integrity sha1-gKTdMjdIOEv6JICDYirt7Jgq3/M= - dependencies: - mimic-response "^1.0.0" - -deep-extend@^0.6.0: - version "0.6.0" - resolved "https://registry.yarnpkg.com/deep-extend/-/deep-extend-0.6.0.tgz#c4fa7c95404a17a9c3e8ca7e1537312b736330ac" - integrity sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA== - -deep-is@^0.1.3: - version "0.1.4" - resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.4.tgz#a6f2dce612fadd2ef1f519b73551f17e85199831" - integrity sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ== - -deepmerge@^4.2.2: - version "4.2.2" - resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-4.2.2.tgz#44d2ea3679b8f4d4ffba33f03d865fc1e7bf4955" - integrity sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg== - -defer-to-connect@^1.0.1: - version "1.1.3" - resolved "https://registry.yarnpkg.com/defer-to-connect/-/defer-to-connect-1.1.3.tgz#331ae050c08dcf789f8c83a7b81f0ed94f4ac591" - integrity sha512-0ISdNousHvZT2EiFlZeZAHBUvSxmKswVCEf8hW7KWgG4a8MVEu/3Vb6uWYozkjylyCxe0JBIiRB1jV45S70WVQ== - -delayed-stream@~1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" - integrity sha1-3zrhmayt+31ECqrgsp4icrJOxhk= - -denque@^1.1.0: - version "1.5.1" - resolved "https://registry.yarnpkg.com/denque/-/denque-1.5.1.tgz#07f670e29c9a78f8faecb2566a1e2c11929c5cbf" - integrity sha512-XwE+iZ4D6ZUB7mfYRMb5wByE8L74HCn30FBN7sWnXksWc1LO1bPDl67pBR9o/kC4z/xSNAwkMYcGgqDV3BE3Hw== - -diff@^4.0.1: - version "4.0.2" - resolved "https://registry.yarnpkg.com/diff/-/diff-4.0.2.tgz#60f3aecb89d5fae520c11aa19efc2bb982aade7d" - integrity sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A== - -dir-glob@^3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/dir-glob/-/dir-glob-3.0.1.tgz#56dbf73d992a4a93ba1584f4534063fd2e41717f" - integrity sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA== - dependencies: - path-type "^4.0.0" - -discord-api-types@0.26.1: - version "0.26.1" - resolved "https://registry.yarnpkg.com/discord-api-types/-/discord-api-types-0.26.1.tgz#726f766ddc37d60da95740991d22cb6ef2ed787b" - integrity sha512-T5PdMQ+Y1MEECYMV5wmyi9VEYPagEDEi4S0amgsszpWY0VB9JJ/hEvM6BgLhbdnKky4gfmZEXtEEtojN8ZKJQQ== - -doctrine@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-3.0.0.tgz#addebead72a6574db783639dc87a121773973961" - integrity sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w== - dependencies: - esutils "^2.0.2" - -dot-prop@^5.2.0: - version "5.3.0" - resolved "https://registry.yarnpkg.com/dot-prop/-/dot-prop-5.3.0.tgz#90ccce708cd9cd82cc4dc8c3ddd9abdd55b20e88" - integrity sha512-QM8q3zDe58hqUqjraQOmzZ1LIH9SWQJTlEKCH4kJ2oQvLZk7RbQXvtDM2XEq3fwkV9CCvvH4LA0AV+ogFsBM2Q== - dependencies: - is-obj "^2.0.0" - -dotenv@^8.2.0: - version "8.6.0" - resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-8.6.0.tgz#061af664d19f7f4d8fc6e4ff9b584ce237adcb8b" - integrity sha512-IrPdXQsk2BbzvCBGBOTmmSH5SodmqZNt4ERAZDmW4CT+tL8VtvinqywuANaFu4bOMWki16nqf0e4oC0QIaDr/g== - -duplexer3@^0.1.4: - version "0.1.4" - resolved "https://registry.yarnpkg.com/duplexer3/-/duplexer3-0.1.4.tgz#ee01dd1cac0ed3cbc7fdbea37dc0a8f1ce002ce2" - integrity sha1-7gHdHKwO08vH/b6jfcCo8c4ALOI= - -emoji-regex@^7.0.1: - version "7.0.3" - resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-7.0.3.tgz#933a04052860c85e83c122479c4748a8e4c72156" - integrity sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA== - -emoji-regex@^8.0.0: - version "8.0.0" - resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37" - integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A== - -end-of-stream@^1.1.0: - version "1.4.4" - resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.4.tgz#5ae64a5f45057baf3626ec14da0ca5e4b2431eb0" - integrity sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q== - dependencies: - once "^1.4.0" - -enquirer@^2.3.5: - version "2.3.6" - resolved "https://registry.yarnpkg.com/enquirer/-/enquirer-2.3.6.tgz#2a7fe5dd634a1e4125a975ec994ff5456dc3734d" - integrity sha512-yjNnPr315/FjS4zIsUxYguYUPP2e1NK4d7E7ZOLiyYCcbFBiTMyID+2wvm2w6+pZ/odMA7cRkjhsPbltwBOrLg== - dependencies: - ansi-colors "^4.1.1" - -"eris@github:DonovanDMC/eris#everything": - version "0.16.0-dev" - resolved "https://codeload.github.com/DonovanDMC/eris/tar.gz/49fcdf590b2534fcec6afda071b3dd0532e8129c" - dependencies: - ws "^7.4.6" - optionalDependencies: - opusscript "^0.0.8" - tweetnacl "^1.0.1" - -escalade@^3.1.1: - version "3.1.1" - resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.1.tgz#d8cfdc7000965c5a0174b4a82eaa5c0552742e40" - integrity sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw== - -escape-goat@^2.0.0: - version "2.1.1" - resolved "https://registry.yarnpkg.com/escape-goat/-/escape-goat-2.1.1.tgz#1b2dc77003676c457ec760b2dc68edb648188675" - integrity sha512-8/uIhbG12Csjy2JEW7D9pHbreaVaS/OpN3ycnyvElTdwM5n6GY6W6e2IPemfvGZeUMqZ9A/3GqIZMgKnBhAw/Q== - -escape-string-regexp@^1.0.2: - version "1.0.5" - resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" - integrity sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ= - -escape-string-regexp@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz#14ba83a5d373e3d311e5afca29cf5bfad965bf34" - integrity sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA== - -eslint-config-prettier@8.3.0: - version "8.3.0" - resolved "https://registry.yarnpkg.com/eslint-config-prettier/-/eslint-config-prettier-8.3.0.tgz#f7471b20b6fe8a9a9254cc684454202886a2dd7a" - integrity sha512-BgZuLUSeKzvlL/VUjx/Yb787VQ26RU3gGjA3iiFvdsp/2bMfVIWUVP7tjxtjS0e+HP409cPlPvNkQloz8C91ew== - -eslint-plugin-prettier@4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/eslint-plugin-prettier/-/eslint-plugin-prettier-4.0.0.tgz#8b99d1e4b8b24a762472b4567992023619cb98e0" - integrity sha512-98MqmCJ7vJodoQK359bqQWaxOE0CS8paAz/GgjaZLyex4TTk3g9HugoO89EqWCrFiOqn9EVvcoo7gZzONCWVwQ== - dependencies: - prettier-linter-helpers "^1.0.0" - -eslint-scope@^5.1.1: - version "5.1.1" - resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-5.1.1.tgz#e786e59a66cb92b3f6c1fb0d508aab174848f48c" - integrity sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw== - dependencies: - esrecurse "^4.3.0" - estraverse "^4.1.1" - -eslint-scope@^7.1.0: - version "7.1.0" - resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-7.1.0.tgz#c1f6ea30ac583031f203d65c73e723b01298f153" - integrity sha512-aWwkhnS0qAXqNOgKOK0dJ2nvzEbhEvpy8OlJ9kZ0FeZnA6zpjv1/Vei+puGFFX7zkPCkHHXb7IDX3A+7yPrRWg== - dependencies: - esrecurse "^4.3.0" - estraverse "^5.2.0" - -eslint-utils@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/eslint-utils/-/eslint-utils-3.0.0.tgz#8aebaface7345bb33559db0a1f13a1d2d48c3672" - integrity sha512-uuQC43IGctw68pJA1RgbQS8/NP7rch6Cwd4j3ZBtgo4/8Flj4eGE7ZYSZRN3iq5pVUv6GPdW5Z1RFleo84uLDA== - dependencies: - eslint-visitor-keys "^2.0.0" - -eslint-visitor-keys@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz#f65328259305927392c938ed44eb0a5c9b2bd303" - integrity sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw== - -eslint-visitor-keys@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-3.0.0.tgz#e32e99c6cdc2eb063f204eda5db67bfe58bb4186" - integrity sha512-mJOZa35trBTb3IyRmo8xmKBZlxf+N7OnUl4+ZhJHs/r+0770Wh/LEACE2pqMGMe27G/4y8P2bYGk4J70IC5k1Q== - -eslint-visitor-keys@^3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-3.1.0.tgz#eee4acea891814cda67a7d8812d9647dd0179af2" - integrity sha512-yWJFpu4DtjsWKkt5GeNBBuZMlNcYVs6vRCLoCVEJrTjaSB6LC98gFipNK/erM2Heg/E8mIK+hXG/pJMLK+eRZA== - -eslint@8.6.0: - version "8.6.0" - resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.6.0.tgz#4318c6a31c5584838c1a2e940c478190f58d558e" - integrity sha512-UvxdOJ7mXFlw7iuHZA4jmzPaUqIw54mZrv+XPYKNbKdLR0et4rf60lIZUU9kiNtnzzMzGWxMV+tQ7uG7JG8DPw== - dependencies: - "@eslint/eslintrc" "^1.0.5" - "@humanwhocodes/config-array" "^0.9.2" - ajv "^6.10.0" - chalk "^4.0.0" - cross-spawn "^7.0.2" - debug "^4.3.2" - doctrine "^3.0.0" - enquirer "^2.3.5" - escape-string-regexp "^4.0.0" - eslint-scope "^7.1.0" - eslint-utils "^3.0.0" - eslint-visitor-keys "^3.1.0" - espree "^9.3.0" - esquery "^1.4.0" - esutils "^2.0.2" - fast-deep-equal "^3.1.3" - file-entry-cache "^6.0.1" - functional-red-black-tree "^1.0.1" - glob-parent "^6.0.1" - globals "^13.6.0" - ignore "^4.0.6" - import-fresh "^3.0.0" - imurmurhash "^0.1.4" - is-glob "^4.0.0" - js-yaml "^4.1.0" - json-stable-stringify-without-jsonify "^1.0.1" - levn "^0.4.1" - lodash.merge "^4.6.2" - minimatch "^3.0.4" - natural-compare "^1.4.0" - optionator "^0.9.1" - progress "^2.0.0" - regexpp "^3.2.0" - semver "^7.2.1" - strip-ansi "^6.0.1" - strip-json-comments "^3.1.0" - text-table "^0.2.0" - v8-compile-cache "^2.0.3" - -espree@^9.2.0: - version "9.2.0" - resolved "https://registry.yarnpkg.com/espree/-/espree-9.2.0.tgz#c50814e01611c2d0f8bd4daa83c369eabba80dbc" - integrity sha512-oP3utRkynpZWF/F2x/HZJ+AGtnIclaR7z1pYPxy7NYM2fSO6LgK/Rkny8anRSPK/VwEA1eqm2squui0T7ZMOBg== - dependencies: - acorn "^8.6.0" - acorn-jsx "^5.3.1" - eslint-visitor-keys "^3.1.0" - -espree@^9.3.0: - version "9.3.0" - resolved "https://registry.yarnpkg.com/espree/-/espree-9.3.0.tgz#c1240d79183b72aaee6ccfa5a90bc9111df085a8" - integrity sha512-d/5nCsb0JcqsSEeQzFZ8DH1RmxPcglRWh24EFTlUEmCKoehXGdpsx0RkHDubqUI8LSAIKMQp4r9SzQ3n+sm4HQ== - dependencies: - acorn "^8.7.0" - acorn-jsx "^5.3.1" - eslint-visitor-keys "^3.1.0" - -esprima@^4.0.0: - version "4.0.1" - resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.1.tgz#13b04cdb3e6c5d19df91ab6987a8695619b0aa71" - integrity sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A== - -esquery@^1.4.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/esquery/-/esquery-1.4.0.tgz#2148ffc38b82e8c7057dfed48425b3e61f0f24a5" - integrity sha512-cCDispWt5vHHtwMY2YrAQ4ibFkAL8RbH5YGBnZBc90MolvvfkkQcJro/aZiAQUlQ3qgrYS6D6v8Gc5G5CQsc9w== - dependencies: - estraverse "^5.1.0" - -esrecurse@^4.3.0: - version "4.3.0" - resolved "https://registry.yarnpkg.com/esrecurse/-/esrecurse-4.3.0.tgz#7ad7964d679abb28bee72cec63758b1c5d2c9921" - integrity sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag== - dependencies: - estraverse "^5.2.0" - -estraverse@^4.1.1: - version "4.3.0" - resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-4.3.0.tgz#398ad3f3c5a24948be7725e83d11a7de28cdbd1d" - integrity sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw== - -estraverse@^5.1.0, estraverse@^5.2.0: - version "5.2.0" - resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-5.2.0.tgz#307df42547e6cc7324d3cf03c155d5cdb8c53880" - integrity sha512-BxbNGGNm0RyRYvUdHpIwv9IWzeM9XClbOxwoATuFdOE7ZE6wHL+HQ5T8hoPM+zHvmKzzsEqhgy0GrQ5X13afiQ== - -esutils@^2.0.2: - version "2.0.3" - resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.3.tgz#74d2eb4de0b8da1293711910d50775b9b710ef64" - integrity sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g== - -eventemitter3@^4.0.7: - version "4.0.7" - resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-4.0.7.tgz#2de9b68f6528d5644ef5c59526a1b4a07306169f" - integrity sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw== - -fast-decode-uri-component@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/fast-decode-uri-component/-/fast-decode-uri-component-1.0.1.tgz#46f8b6c22b30ff7a81357d4f59abfae938202543" - integrity sha512-WKgKWg5eUxvRZGwW8FvfbaH7AXSh2cL+3j5fMGzUMCxWBJ3dV3a7Wz8y2f/uQ0e3B6WmodD3oS54jTQ9HVTIIg== - -fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3: - version "3.1.3" - resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" - integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q== - -fast-diff@^1.1.2: - version "1.2.0" - resolved "https://registry.yarnpkg.com/fast-diff/-/fast-diff-1.2.0.tgz#73ee11982d86caaf7959828d519cfe927fac5f03" - integrity sha512-xJuoT5+L99XlZ8twedaRf6Ax2TgQVxvgZOYoPKqZufmJib0tL2tegPBOZb1pVNgIhlqDlA0eO0c3wBvQcmzx4w== - -fast-glob@^3.1.1: - version "3.2.7" - resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.2.7.tgz#fd6cb7a2d7e9aa7a7846111e85a196d6b2f766a1" - integrity sha512-rYGMRwip6lUMvYD3BTScMwT1HtAs2d71SMv66Vrxs0IekGZEjhM0pcMfjQPnknBt2zeCwQMEupiN02ZP4DiT1Q== - dependencies: - "@nodelib/fs.stat" "^2.0.2" - "@nodelib/fs.walk" "^1.2.3" - glob-parent "^5.1.2" - merge2 "^1.3.0" - micromatch "^4.0.4" - -fast-json-stable-stringify@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633" - integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw== - -fast-json-stringify@^2.5.2: - version "2.7.9" - resolved "https://registry.yarnpkg.com/fast-json-stringify/-/fast-json-stringify-2.7.9.tgz#f9086584bb54f54181b07669173695ca090fcfce" - integrity sha512-FC9RJtux5cyojLEbpLyt6cMo6lkJPsfvx0E5O/I5fFkcnYVOSFjg53VUeVWudYXNJOS9Mmjx7totdrLCUWHPTA== - dependencies: - ajv "^6.11.0" - deepmerge "^4.2.2" - rfdc "^1.2.0" - string-similarity "^4.0.1" - -fast-levenshtein@^2.0.6: - version "2.0.6" - resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917" - integrity sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc= - -fast-redact@^3.0.0: - version "3.0.2" - resolved "https://registry.yarnpkg.com/fast-redact/-/fast-redact-3.0.2.tgz#c940ba7162dde3aeeefc522926ae8c5231412904" - integrity sha512-YN+CYfCVRVMUZOUPeinHNKgytM1wPI/C/UCLEi56EsY2dwwvI00kIJHJoI7pMVqGoMew8SMZ2SSfHKHULHXDsg== - -fast-safe-stringify@^2.0.8: - version "2.1.1" - resolved "https://registry.yarnpkg.com/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz#c406a83b6e70d9e35ce3b30a81141df30aeba884" - integrity sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA== - -fastify-cors@6.0.2: - version "6.0.2" - resolved "https://registry.yarnpkg.com/fastify-cors/-/fastify-cors-6.0.2.tgz#4fd5102549659e9b34d252fd7ee607b63d021390" - integrity sha512-sE0AOyzmj5hLLRRVgenjA6G2iOGX35/1S3QGYB9rr9TXelMZB3lFrXy4CzwYVOMiujJeMiLgO4J7eRm8sQSv8Q== - dependencies: - fastify-plugin "^3.0.0" - vary "^1.1.2" - -fastify-error@^0.3.0: - version "0.3.1" - resolved "https://registry.yarnpkg.com/fastify-error/-/fastify-error-0.3.1.tgz#8eb993e15e3cf57f0357fc452af9290f1c1278d2" - integrity sha512-oCfpcsDndgnDVgiI7bwFKAun2dO+4h84vBlkWsWnz/OUK9Reff5UFoFl241xTiLeHWX/vU9zkDVXqYUxjOwHcQ== - -fastify-no-icon@4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/fastify-no-icon/-/fastify-no-icon-4.0.0.tgz#d37973a345ed3171d17a326fefb1af2058225876" - integrity sha512-4LzH9zgICq1+vKhSmfAqmYZUYpWU/yvonH7NSN8cb0xYt9VE0MR92kXPuS3+4XMavBXtfMFAoUEZqpQemJRuzQ== - dependencies: - fastify-plugin "^2.3.0" - -fastify-plugin@^2.3.0: - version "2.3.4" - resolved "https://registry.yarnpkg.com/fastify-plugin/-/fastify-plugin-2.3.4.tgz#b17abdc36a97877d88101fb86ad8a07f2c07de87" - integrity sha512-I+Oaj6p9oiRozbam30sh39BiuiqBda7yK2nmSPVwDCfIBlKnT8YB3MY+pRQc2Fcd07bf6KPGklHJaQ2Qu81TYQ== - dependencies: - semver "^7.3.2" - -fastify-plugin@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/fastify-plugin/-/fastify-plugin-3.0.0.tgz#cf1b8c8098e3b5a7c8c30e6aeb06903370c054ca" - integrity sha512-ZdCvKEEd92DNLps5n0v231Bha8bkz1DjnPP/aEz37rz/q42Z5JVLmgnqR4DYuNn3NXAO3IDCPyRvgvxtJ4Ym4w== - -fastify-warning@^0.2.0: - version "0.2.0" - resolved "https://registry.yarnpkg.com/fastify-warning/-/fastify-warning-0.2.0.tgz#e717776026a4493dc9a2befa44db6d17f618008f" - integrity sha512-s1EQguBw/9qtc1p/WTY4eq9WMRIACkj+HTcOIK1in4MV5aFaQC9ZCIt0dJ7pr5bIf4lPpHvAtP2ywpTNgs7hqw== - -fastify@3.25.3: - version "3.25.3" - resolved "https://registry.yarnpkg.com/fastify/-/fastify-3.25.3.tgz#7a2578edbdb59729cbf48ec44b5ae2b0f8db1f4a" - integrity sha512-20SbobjjMfjGNCU4PlcZis3d5XLDtQxIbcAf6ogi/8zPxRxOOkKwfjmj7yW9Q1VnxDpBwcllwPtbZ/LyvQzXbQ== - dependencies: - "@fastify/ajv-compiler" "^1.0.0" - abstract-logging "^2.0.0" - avvio "^7.1.2" - fast-json-stringify "^2.5.2" - fastify-error "^0.3.0" - find-my-way "^4.5.0" - flatstr "^1.0.12" - light-my-request "^4.2.0" - pino "^6.13.0" - process-warning "^1.0.0" - proxy-addr "^2.0.7" - rfdc "^1.1.4" - secure-json-parse "^2.0.0" - semver "^7.3.2" - tiny-lru "^7.0.0" - -fastq@^1.6.0, fastq@^1.6.1: - version "1.12.0" - resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.12.0.tgz#ed7b6ab5d62393fb2cc591c853652a5c318bf794" - integrity sha512-VNX0QkHK3RsXVKr9KrlUv/FoTa0NdbYoHHl7uXHv2rzyHSlxjdNAKug2twd9luJxpcyNeAgf5iPPMutJO67Dfg== - dependencies: - reusify "^1.0.4" - -figlet@^1.1.1: - version "1.5.2" - resolved "https://registry.yarnpkg.com/figlet/-/figlet-1.5.2.tgz#dda34ff233c9a48e36fcff6741aeb5bafe49b634" - integrity sha512-WOn21V8AhyE1QqVfPIVxe3tupJacq1xGkPTB4iagT6o+P2cAgEOOwIxMftr4+ZCTI6d551ij9j61DFr0nsP2uQ== - -file-entry-cache@^6.0.1: - version "6.0.1" - resolved "https://registry.yarnpkg.com/file-entry-cache/-/file-entry-cache-6.0.1.tgz#211b2dd9659cb0394b073e7323ac3c933d522027" - integrity sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg== - dependencies: - flat-cache "^3.0.4" - -fill-range@^7.0.1: - version "7.0.1" - resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.0.1.tgz#1919a6a7c75fe38b2c7c77e5198535da9acdda40" - integrity sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ== - dependencies: - to-regex-range "^5.0.1" - -find-my-way@^4.5.0: - version "4.5.0" - resolved "https://registry.yarnpkg.com/find-my-way/-/find-my-way-4.5.0.tgz#885ceb7a60b9be529a11dbfed02c8cf0ab7eedb7" - integrity sha512-kVEY1lW/zXETKEEwRboCWPySygBFVNJpdY7lRnFllDdYxQD6cwcaT3Ddh8P7v824jRN3cld+8+zRbnFBxeVUxA== - dependencies: - fast-decode-uri-component "^1.0.1" - fast-deep-equal "^3.1.3" - safe-regex2 "^2.0.0" - semver-store "^0.3.0" - -flat-cache@^3.0.4: - version "3.0.4" - resolved "https://registry.yarnpkg.com/flat-cache/-/flat-cache-3.0.4.tgz#61b0338302b2fe9f957dcc32fc2a87f1c3048b11" - integrity sha512-dm9s5Pw7Jc0GvMYbshN6zchCA9RgQlzzEZX3vylR9IqFfS8XciblUXOKfW6SiuJ0e13eDYZoZV5wdrev7P3Nwg== - dependencies: - flatted "^3.1.0" - rimraf "^3.0.2" - -flatstr@^1.0.12: - version "1.0.12" - resolved "https://registry.yarnpkg.com/flatstr/-/flatstr-1.0.12.tgz#c2ba6a08173edbb6c9640e3055b95e287ceb5931" - integrity sha512-4zPxDyhCyiN2wIAtSLI6gc82/EjqZc1onI4Mz/l0pWrAlsSfYH/2ZIcU+e3oA2wDwbzIWNKwa23F8rh6+DRWkw== - -flatted@^3.1.0: - version "3.2.2" - resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.2.2.tgz#64bfed5cb68fe3ca78b3eb214ad97b63bedce561" - integrity sha512-JaTY/wtrcSyvXJl4IMFHPKyFur1sE9AUqc0QnhOaJ0CxHtAoIV8pYDzeEfAaNEtGkOfq4gr3LBFmdXW5mOQFnA== - -form-data@4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/form-data/-/form-data-4.0.0.tgz#93919daeaf361ee529584b9b31664dc12c9fa452" - integrity sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww== - dependencies: - asynckit "^0.4.0" - combined-stream "^1.0.8" - mime-types "^2.1.12" - -forwarded@0.2.0: - version "0.2.0" - resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.2.0.tgz#2269936428aad4c15c7ebe9779a84bf0b2a81811" - integrity sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow== - -fs.realpath@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" - integrity sha1-FQStJSMVjKpA20onh8sBQRmU6k8= - -fsevents@~2.3.2: - version "2.3.2" - resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.2.tgz#8a526f78b8fdf4623b709e0b975c52c24c02fd1a" - integrity sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA== - -functional-red-black-tree@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz#1b0ab3bd553b2a0d6399d29c0e3ea0b252078327" - integrity sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc= - -get-caller-file@^2.0.5: - version "2.0.5" - resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e" - integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg== - -get-stream@^4.1.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-4.1.0.tgz#c1b255575f3dc21d59bfc79cd3d2b46b1c3a54b5" - integrity sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w== - dependencies: - pump "^3.0.0" - -get-stream@^5.1.0: - version "5.2.0" - resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-5.2.0.tgz#4966a1795ee5ace65e706c4b7beb71257d6e22d3" - integrity sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA== - dependencies: - pump "^3.0.0" - -glob-parent@^5.1.2, glob-parent@~5.1.2: - version "5.1.2" - resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4" - integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow== - dependencies: - is-glob "^4.0.1" - -glob-parent@^6.0.1: - version "6.0.2" - resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-6.0.2.tgz#6d237d99083950c79290f24c7642a3de9a28f9e3" - integrity sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A== - dependencies: - is-glob "^4.0.3" - -glob@^7.1.3, glob@^7.1.6: - version "7.1.7" - resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.7.tgz#3b193e9233f01d42d0b3f78294bbeeb418f94a90" - integrity sha512-OvD9ENzPLbegENnYP5UUfJIirTg4+XwMWGaQfQTY0JenxNvvIKP3U3/tAQSPIu/lHxXYSZmpXlUHeqAIdKzBLQ== - dependencies: - fs.realpath "^1.0.0" - inflight "^1.0.4" - inherits "2" - minimatch "^3.0.4" - once "^1.3.0" - path-is-absolute "^1.0.0" - -global-dirs@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/global-dirs/-/global-dirs-3.0.0.tgz#70a76fe84ea315ab37b1f5576cbde7d48ef72686" - integrity sha512-v8ho2DS5RiCjftj1nD9NmnfaOzTdud7RRnVd9kFNOjqZbISlx5DQ+OrTkywgd0dIt7oFCvKetZSHoHcP3sDdiA== - dependencies: - ini "2.0.0" - -globals@^13.6.0, globals@^13.9.0: - version "13.11.0" - resolved "https://registry.yarnpkg.com/globals/-/globals-13.11.0.tgz#40ef678da117fe7bd2e28f1fab24951bd0255be7" - integrity sha512-08/xrJ7wQjK9kkkRoI3OFUBbLx4f+6x3SGwcPvQ0QH6goFDrOU2oyAWrmh3dJezu65buo+HBMzAMQy6rovVC3g== - dependencies: - type-fest "^0.20.2" - -globby@^11.0.3, globby@^11.0.4: - version "11.0.4" - resolved "https://registry.yarnpkg.com/globby/-/globby-11.0.4.tgz#2cbaff77c2f2a62e71e9b2813a67b97a3a3001a5" - integrity sha512-9O4MVG9ioZJ08ffbcyVYyLOJLk5JQ688pJ4eMGLpdWLHq/Wr1D9BlriLQyL0E+jbkuePVZXYFj47QM/v093wHg== - dependencies: - array-union "^2.1.0" - dir-glob "^3.0.1" - fast-glob "^3.1.1" - ignore "^5.1.4" - merge2 "^1.3.0" - slash "^3.0.0" - -got@^9.6.0: - version "9.6.0" - resolved "https://registry.yarnpkg.com/got/-/got-9.6.0.tgz#edf45e7d67f99545705de1f7bbeeeb121765ed85" - integrity sha512-R7eWptXuGYxwijs0eV+v3o6+XH1IqVK8dJOEecQfTmkncw9AV4dcw/Dhxi8MdlqPthxxpZyizMzyg8RTmEsG+Q== - dependencies: - "@sindresorhus/is" "^0.14.0" - "@szmarczak/http-timer" "^1.1.2" - cacheable-request "^6.0.0" - decompress-response "^3.3.0" - duplexer3 "^0.1.4" - get-stream "^4.1.0" - lowercase-keys "^1.0.1" - mimic-response "^1.0.1" - p-cancelable "^1.0.0" - to-readable-stream "^1.0.0" - url-parse-lax "^3.0.0" - -graceful-fs@^4.1.2: - version "4.2.8" - resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.8.tgz#e412b8d33f5e006593cbd3cee6df9f2cebbe802a" - integrity sha512-qkIilPUYcNhJpd33n0GBXTB1MMPp14TxEsEs0pTrsSVucApsYzW5V+Q8Qxhik6KU3evy+qkAAowTByymK0avdg== - -has-ansi@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/has-ansi/-/has-ansi-2.0.0.tgz#34f5049ce1ecdf2b0649af3ef24e45ed35416d91" - integrity sha1-NPUEnOHs3ysGSa8+8k5F7TVBbZE= - dependencies: - ansi-regex "^2.0.0" - -has-flag@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd" - integrity sha1-tdRU3CGZriJWmfNGfloH87lVuv0= - -has-flag@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b" - integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ== - -has-yarn@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/has-yarn/-/has-yarn-2.1.0.tgz#137e11354a7b5bf11aa5cb649cf0c6f3ff2b2e77" - integrity sha512-UqBRqi4ju7T+TqGNdqAO0PaSVGsDGJUBQvk9eUWNGRY1CFGDzYhLWoM7JQEemnlvVcv/YEmc2wNW8BC24EnUsw== - -highlight.js@^10.7.1: - version "10.7.3" - resolved "https://registry.yarnpkg.com/highlight.js/-/highlight.js-10.7.3.tgz#697272e3991356e40c3cac566a74eef681756531" - integrity sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A== - -http-cache-semantics@^4.0.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/http-cache-semantics/-/http-cache-semantics-4.1.0.tgz#49e91c5cbf36c9b94bcfcd71c23d5249ec74e390" - integrity sha512-carPklcUh7ROWRK7Cv27RPtdhYhUsela/ue5/jKzjegVvXDqM2ILE9Q2BGn9JZJh1g87cp56su/FgQSzcWS8cQ== - -https-proxy-agent@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-5.0.0.tgz#e2a90542abb68a762e0a0850f6c9edadfd8506b2" - integrity sha512-EkYm5BcKUGiduxzSt3Eppko+PiNWNEpa4ySk9vTC6wDsQJW9rHSa+UhGNJoRYp7bz6Ht1eaRIa6QaJqO5rCFbA== - dependencies: - agent-base "6" - debug "4" - -husky@7.0.4: - version "7.0.4" - resolved "https://registry.yarnpkg.com/husky/-/husky-7.0.4.tgz#242048245dc49c8fb1bf0cc7cfb98dd722531535" - integrity sha512-vbaCKN2QLtP/vD4yvs6iz6hBEo6wkSzs8HpRah1Z6aGmF2KW5PdYuAd7uX5a+OyBZHBhd+TFLqgjUgytQr4RvQ== - -ieee754@^1.1.13: - version "1.2.1" - resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352" - integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA== - -ignore-by-default@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/ignore-by-default/-/ignore-by-default-1.0.1.tgz#48ca6d72f6c6a3af00a9ad4ae6876be3889e2b09" - integrity sha1-SMptcvbGo68Aqa1K5odr44ieKwk= - -ignore@^4.0.6: - version "4.0.6" - resolved "https://registry.yarnpkg.com/ignore/-/ignore-4.0.6.tgz#750e3db5862087b4737ebac8207ffd1ef27b25fc" - integrity sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg== - -ignore@^5.1.4, ignore@^5.1.8: - version "5.1.8" - resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.1.8.tgz#f150a8b50a34289b33e22f5889abd4d8016f0e57" - integrity sha512-BMpfD7PpiETpBl/A6S498BaIJ6Y/ABT93ETbby2fP00v4EbvPBXWEoaR1UBPKs3iR53pJY7EtZk5KACI57i1Uw== - -import-fresh@^3.0.0, import-fresh@^3.2.1: - version "3.3.0" - resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.3.0.tgz#37162c25fcb9ebaa2e6e53d5b4d88ce17d9e0c2b" - integrity sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw== - dependencies: - parent-module "^1.0.0" - resolve-from "^4.0.0" - -import-lazy@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/import-lazy/-/import-lazy-2.1.0.tgz#05698e3d45c88e8d7e9d92cb0584e77f096f3e43" - integrity sha1-BWmOPUXIjo1+nZLLBYTnfwlvPkM= - -imurmurhash@^0.1.4: - version "0.1.4" - resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea" - integrity sha1-khi5srkoojixPcT7a21XbyMUU+o= - -inflight@^1.0.4: - version "1.0.6" - resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" - integrity sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk= - dependencies: - once "^1.3.0" - wrappy "1" - -inherits@2, inherits@^2.0.1, inherits@^2.0.3: - version "2.0.4" - resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" - integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== - -ini@2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/ini/-/ini-2.0.0.tgz#e5fd556ecdd5726be978fa1001862eacb0a94bc5" - integrity sha512-7PnF4oN3CvZF23ADhA5wRaYEQpJ8qygSkbtTXWBeXWXmEVRXK+1ITciHWwHhsjv1TmW0MgacIv6hEi5pX5NQdA== - -ini@~1.3.0: - version "1.3.8" - resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.8.tgz#a29da425b48806f34767a4efce397269af28432c" - integrity sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew== - -ioredis@4.28.2: - version "4.28.2" - resolved "https://registry.yarnpkg.com/ioredis/-/ioredis-4.28.2.tgz#493ccd5d869fd0ec86c96498192718171f6c9203" - integrity sha512-kQ+Iv7+c6HsDdPP2XUHaMv8DhnSeAeKEwMbaoqsXYbO+03dItXt7+5jGQDRyjdRUV2rFJbzg7P4Qt1iX2tqkOg== - dependencies: - cluster-key-slot "^1.1.0" - debug "^4.3.1" - denque "^1.1.0" - lodash.defaults "^4.2.0" - lodash.flatten "^4.4.0" - lodash.isarguments "^3.1.0" - p-map "^2.1.0" - redis-commands "1.7.0" - redis-errors "^1.2.0" - redis-parser "^3.0.0" - standard-as-callback "^2.1.0" - -ipaddr.js@1.9.1: - version "1.9.1" - resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.9.1.tgz#bff38543eeb8984825079ff3a2a8e6cbd46781b3" - integrity sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g== - -is-binary-path@~2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-2.1.0.tgz#ea1f7f3b80f064236e83470f86c09c254fb45b09" - integrity sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw== - dependencies: - binary-extensions "^2.0.0" - -is-ci@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/is-ci/-/is-ci-2.0.0.tgz#6bc6334181810e04b5c22b3d589fdca55026404c" - integrity sha512-YfJT7rkpQB0updsdHLGWrvhBJfcfzNNawYDNIyQXJz0IViGf75O8EBPKSdvw2rF+LGCsX4FZ8tcr3b19LcZq4w== - dependencies: - ci-info "^2.0.0" - -is-extglob@^2.1.1: - version "2.1.1" - resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2" - integrity sha1-qIwCU1eR8C7TfHahueqXc8gz+MI= - -is-fullwidth-code-point@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz#a3b30a5c4f199183167aaab93beefae3ddfb654f" - integrity sha1-o7MKXE8ZkYMWeqq5O+764937ZU8= - -is-fullwidth-code-point@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz#f116f8064fe90b3f7844a38997c0b75051269f1d" - integrity sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg== - -is-glob@^4.0.0, is-glob@^4.0.1, is-glob@~4.0.1: - version "4.0.1" - resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.1.tgz#7567dbe9f2f5e2467bc77ab83c4a29482407a5dc" - integrity sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg== - dependencies: - is-extglob "^2.1.1" - -is-glob@^4.0.3: - version "4.0.3" - resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.3.tgz#64f61e42cbbb2eec2071a9dac0b28ba1e65d5084" - integrity sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg== - dependencies: - is-extglob "^2.1.1" - -is-installed-globally@^0.4.0: - version "0.4.0" - resolved "https://registry.yarnpkg.com/is-installed-globally/-/is-installed-globally-0.4.0.tgz#9a0fd407949c30f86eb6959ef1b7994ed0b7b520" - integrity sha512-iwGqO3J21aaSkC7jWnHP/difazwS7SFeIqxv6wEtLU8Y5KlzFTjyqcSIT0d8s4+dDhKytsk9PJZ2BkS5eZwQRQ== - dependencies: - global-dirs "^3.0.0" - is-path-inside "^3.0.2" - -is-npm@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/is-npm/-/is-npm-5.0.0.tgz#43e8d65cc56e1b67f8d47262cf667099193f45a8" - integrity sha512-WW/rQLOazUq+ST/bCAVBp/2oMERWLsR7OrKyt052dNDk4DHcDE0/7QSXITlmi+VBcV13DfIbysG3tZJm5RfdBA== - -is-number@^7.0.0: - version "7.0.0" - resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b" - integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng== - -is-obj@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/is-obj/-/is-obj-2.0.0.tgz#473fb05d973705e3fd9620545018ca8e22ef4982" - integrity sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w== - -is-path-inside@^3.0.2: - version "3.0.3" - resolved "https://registry.yarnpkg.com/is-path-inside/-/is-path-inside-3.0.3.tgz#d231362e53a07ff2b0e0ea7fed049161ffd16283" - integrity sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ== - -is-typedarray@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a" - integrity sha1-5HnICFjfDBsR3dppQPlgEfzaSpo= - -is-yarn-global@^0.3.0: - version "0.3.0" - resolved "https://registry.yarnpkg.com/is-yarn-global/-/is-yarn-global-0.3.0.tgz#d502d3382590ea3004893746754c89139973e232" - integrity sha512-VjSeb/lHmkoyd8ryPVIKvOCn4D1koMqY+vqyjjUfc3xyKtP4dYOxM44sZrnqQSzSds3xyOrUTLTC9LVCVgLngw== - -isexe@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" - integrity sha1-6PvzdNxVb/iUehDcsFctYz8s+hA= - -js-yaml@4.1.0, js-yaml@^4.1.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.1.0.tgz#c1fb65f8f5017901cdd2c951864ba18458a10602" - integrity sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA== - dependencies: - argparse "^2.0.1" - -js-yaml@^3.14.0: - version "3.14.1" - resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.14.1.tgz#dae812fdb3825fa306609a8717383c50c36a0537" - integrity sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g== - dependencies: - argparse "^1.0.7" - esprima "^4.0.0" - -json-buffer@3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/json-buffer/-/json-buffer-3.0.0.tgz#5b1f397afc75d677bde8bcfc0e47e1f9a3d9a898" - integrity sha1-Wx85evx11ne96Lz8Dkfh+aPZqJg= - -json-schema-traverse@^0.4.1: - version "0.4.1" - resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660" - integrity sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg== - -json-schema-traverse@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz#ae7bcb3656ab77a73ba5c49bf654f38e6b6860e2" - integrity sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug== - -json-stable-stringify-without-jsonify@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz#9db7b59496ad3f3cfef30a75142d2d930ad72651" - integrity sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE= - -keyv@^3.0.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/keyv/-/keyv-3.1.0.tgz#ecc228486f69991e49e9476485a5be1e8fc5c4d9" - integrity sha512-9ykJ/46SN/9KPM/sichzQ7OvXyGDYKGTaDlKMGCAlg2UK8KRy4jb0d8sFc+0Tt0YYnThq8X2RZgCg74RPxgcVA== - dependencies: - json-buffer "3.0.0" - -latest-version@^5.1.0: - version "5.1.0" - resolved "https://registry.yarnpkg.com/latest-version/-/latest-version-5.1.0.tgz#119dfe908fe38d15dfa43ecd13fa12ec8832face" - integrity sha512-weT+r0kTkRQdCdYCNtkMwWXQTMEswKrFBkm4ckQOMVhhqhIMI1UT2hMj+1iigIhgSZm5gTmrRXBNoGUgaTY1xA== - dependencies: - package-json "^6.3.0" - -levn@^0.4.1: - version "0.4.1" - resolved "https://registry.yarnpkg.com/levn/-/levn-0.4.1.tgz#ae4562c007473b932a6200d403268dd2fffc6ade" - integrity sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ== - dependencies: - prelude-ls "^1.2.1" - type-check "~0.4.0" - -light-my-request@^4.2.0: - version "4.4.4" - resolved "https://registry.yarnpkg.com/light-my-request/-/light-my-request-4.4.4.tgz#051e0d440a7bdaea31bcbe6b480a67a8df77c203" - integrity sha512-nxYLB+Lke3wGQ55HQIo/CjSS18xGyHRF0y/u7YxEwp1YsqQTxObteBXYHZY3ELSvYmqy0pRLTWbI5//zRYTXlg== - dependencies: - ajv "^8.1.0" - cookie "^0.4.0" - fastify-warning "^0.2.0" - readable-stream "^3.6.0" - set-cookie-parser "^2.4.1" - -lodash.defaults@^4.2.0: - version "4.2.0" - resolved "https://registry.yarnpkg.com/lodash.defaults/-/lodash.defaults-4.2.0.tgz#d09178716ffea4dde9e5fb7b37f6f0802274580c" - integrity sha1-0JF4cW/+pN3p5ft7N/bwgCJ0WAw= - -lodash.flatten@^4.4.0: - version "4.4.0" - resolved "https://registry.yarnpkg.com/lodash.flatten/-/lodash.flatten-4.4.0.tgz#f31c22225a9632d2bbf8e4addbef240aa765a61f" - integrity sha1-8xwiIlqWMtK7+OSt2+8kCqdlph8= - -lodash.isarguments@^3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz#2f573d85c6a24289ff00663b491c1d338ff3458a" - integrity sha1-L1c9hcaiQon/AGY7SRwdM4/zRYo= - -lodash.isequal@^4.5.0: - version "4.5.0" - resolved "https://registry.yarnpkg.com/lodash.isequal/-/lodash.isequal-4.5.0.tgz#415c4478f2bcc30120c22ce10ed3226f7d3e18e0" - integrity sha1-QVxEePK8wwEgwizhDtMib30+GOA= - -lodash.merge@^4.6.2: - version "4.6.2" - resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.2.tgz#558aa53b43b661e1925a0afdfa36a9a1085fe57a" - integrity sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ== - -lowercase-keys@^1.0.0, lowercase-keys@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/lowercase-keys/-/lowercase-keys-1.0.1.tgz#6f9e30b47084d971a7c820ff15a6c5167b74c26f" - integrity sha512-G2Lj61tXDnVFFOi8VZds+SoQjtQC3dgokKdDG2mTm1tx4m50NUHBOZSBwQQHyy0V12A0JTG4icfZQH+xPyh8VA== - -lowercase-keys@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/lowercase-keys/-/lowercase-keys-2.0.0.tgz#2603e78b7b4b0006cbca2fbcc8a3202558ac9479" - integrity sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA== - -lru-cache@^6.0.0: - version "6.0.0" - resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-6.0.0.tgz#6d6fe6570ebd96aaf90fcad1dafa3b2566db3a94" - integrity sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA== - dependencies: - yallist "^4.0.0" - -lru_map@^0.3.3: - version "0.3.3" - resolved "https://registry.yarnpkg.com/lru_map/-/lru_map-0.3.3.tgz#b5c8351b9464cbd750335a79650a0ec0e56118dd" - integrity sha1-tcg1G5Rky9dQM1p5ZQoOwOVhGN0= - -luxon@2.3.0: - version "2.3.0" - resolved "https://registry.yarnpkg.com/luxon/-/luxon-2.3.0.tgz#bf16a7e642513c2a20a6230a6a41b0ab446d0045" - integrity sha512-gv6jZCV+gGIrVKhO90yrsn8qXPKD8HYZJtrUDSfEbow8Tkw84T9OnCyJhWvnJIaIF/tBuiAjZuQHUt1LddX2mg== - -make-dir@^3.0.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-3.1.0.tgz#415e967046b3a7f1d185277d84aa58203726a13f" - integrity sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw== - dependencies: - semver "^6.0.0" - -make-error@^1.1.1: - version "1.3.6" - resolved "https://registry.yarnpkg.com/make-error/-/make-error-1.3.6.tgz#2eb2e37ea9b67c4891f684a1394799af484cf7a2" - integrity sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw== - -merge2@^1.3.0: - version "1.4.1" - resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae" - integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg== - -micromatch@^4.0.4: - version "4.0.4" - resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.4.tgz#896d519dfe9db25fce94ceb7a500919bf881ebf9" - integrity sha512-pRmzw/XUcwXGpD9aI9q/0XOwLNygjETJ8y0ao0wdqprrzDa4YnxLcz7fQRZr8voh8V10kGhABbNcHVk5wHgWwg== - dependencies: - braces "^3.0.1" - picomatch "^2.2.3" - -mime-db@1.49.0: - version "1.49.0" - resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.49.0.tgz#f3dfde60c99e9cf3bc9701d687778f537001cbed" - integrity sha512-CIc8j9URtOVApSFCQIF+VBkX1RwXp/oMMOrqdyXSBXq5RWNEsRfyj1kiRnQgmNXmHxPoFIxOroKA3zcU9P+nAA== - -mime-types@^2.1.12: - version "2.1.32" - resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.32.tgz#1d00e89e7de7fe02008db61001d9e02852670fd5" - integrity sha512-hJGaVS4G4c9TSMYh2n6SQAGrC4RnfU+daP8G7cSCmaqNjiOoUY0VHCMS42pxnQmVF1GWwFhbHWn3RIxCqTmZ9A== - dependencies: - mime-db "1.49.0" - -mimic-response@^1.0.0, mimic-response@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/mimic-response/-/mimic-response-1.0.1.tgz#4923538878eef42063cb8a3e3b0798781487ab1b" - integrity sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ== - -minimatch@^3.0.4: - version "3.0.4" - resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083" - integrity sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA== - dependencies: - brace-expansion "^1.1.7" - -minimist@^1.2.0: - version "1.2.5" - resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602" - integrity sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw== - -mkdirp@^1.0.4: - version "1.0.4" - resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e" - integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw== - -ms@2.1.2: - version "2.1.2" - resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" - integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== - -ms@2.1.3, ms@^2.1.1: - version "2.1.3" - resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" - integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== - -mz@^2.4.0: - version "2.7.0" - resolved "https://registry.yarnpkg.com/mz/-/mz-2.7.0.tgz#95008057a56cafadc2bc63dde7f9ff6955948e32" - integrity sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q== - dependencies: - any-promise "^1.0.0" - object-assign "^4.0.1" - thenify-all "^1.0.0" - -natural-compare@^1.4.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7" - integrity sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc= - -nodemon@2.0.15: - version "2.0.15" - resolved "https://registry.yarnpkg.com/nodemon/-/nodemon-2.0.15.tgz#504516ce3b43d9dc9a955ccd9ec57550a31a8d4e" - integrity sha512-gdHMNx47Gw7b3kWxJV64NI+Q5nfl0y5DgDbiVtShiwa7Z0IZ07Ll4RLFo6AjrhzMtoEZn5PDE3/c2AbVsiCkpA== - dependencies: - chokidar "^3.5.2" - debug "^3.2.7" - ignore-by-default "^1.0.1" - minimatch "^3.0.4" - pstree.remy "^1.1.8" - semver "^5.7.1" - supports-color "^5.5.0" - touch "^3.1.0" - undefsafe "^2.0.5" - update-notifier "^5.1.0" - -nopt@~1.0.10: - version "1.0.10" - resolved "https://registry.yarnpkg.com/nopt/-/nopt-1.0.10.tgz#6ddd21bd2a31417b92727dd585f8a6f37608ebee" - integrity sha1-bd0hvSoxQXuScn3Vhfim83YI6+4= - dependencies: - abbrev "1" - -normalize-path@^3.0.0, normalize-path@~3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65" - integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA== - -normalize-url@^4.1.0: - version "4.5.1" - resolved "https://registry.yarnpkg.com/normalize-url/-/normalize-url-4.5.1.tgz#0dd90cf1288ee1d1313b87081c9a5932ee48518a" - integrity sha512-9UZCFRHQdNrfTpGg8+1INIg93B6zE0aXMVFkw1WFwvO4SlZywU6aLg5Of0Ap/PgcbSw4LNxvMWXMeugwMCX0AA== - -object-assign@^4.0.1: - version "4.1.1" - resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" - integrity sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM= - -once@^1.3.0, once@^1.3.1, once@^1.4.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" - integrity sha1-WDsap3WWHUsROsF9nFC6753Xa9E= - dependencies: - wrappy "1" - -optionator@^0.9.1: - version "0.9.1" - resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.9.1.tgz#4f236a6373dae0566a6d43e1326674f50c291499" - integrity sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw== - dependencies: - deep-is "^0.1.3" - fast-levenshtein "^2.0.6" - levn "^0.4.1" - prelude-ls "^1.2.1" - type-check "^0.4.0" - word-wrap "^1.2.3" - -opusscript@^0.0.8: - version "0.0.8" - resolved "https://registry.yarnpkg.com/opusscript/-/opusscript-0.0.8.tgz#00b49e81281b4d99092d013b1812af8654bd0a87" - integrity sha512-VSTi1aWFuCkRCVq+tx/BQ5q9fMnQ9pVZ3JU4UHKqTkf0ED3fKEPdr+gKAAl3IA2hj9rrP6iyq3hlcJq3HELtNQ== - -p-cancelable@^1.0.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/p-cancelable/-/p-cancelable-1.1.0.tgz#d078d15a3af409220c886f1d9a0ca2e441ab26cc" - integrity sha512-s73XxOZ4zpt1edZYZzvhqFa6uvQc1vwUa0K0BdtIZgQMAJj9IbebH+JkgKZc9h+B05PKHLOTl4ajG1BmNrVZlw== - -p-map@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/p-map/-/p-map-2.1.0.tgz#310928feef9c9ecc65b68b17693018a665cea175" - integrity sha512-y3b8Kpd8OAN444hxfBbFfj1FY/RjtTd8tzYwhUqNYXx0fXx2iX4maP4Qr6qhIKbQXI02wTLAda4fYUbDagTUFw== - -package-json@^6.3.0: - version "6.5.0" - resolved "https://registry.yarnpkg.com/package-json/-/package-json-6.5.0.tgz#6feedaca35e75725876d0b0e64974697fed145b0" - integrity sha512-k3bdm2n25tkyxcjSKzB5x8kfVxlMdgsbPr0GkZcwHsLpba6cBjqCt1KlcChKEvxHIcTB1FVMuwoijZ26xex5MQ== - dependencies: - got "^9.6.0" - registry-auth-token "^4.0.0" - registry-url "^5.0.0" - semver "^6.2.0" - -packet-reader@1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/packet-reader/-/packet-reader-1.0.0.tgz#9238e5480dedabacfe1fe3f2771063f164157d74" - integrity sha512-HAKu/fG3HpHFO0AA8WE8q2g+gBJaZ9MG7fcKk+IJPLTGAD6Psw4443l+9DGRbOIh3/aXr7Phy0TjilYivJo5XQ== - -parent-module@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/parent-module/-/parent-module-1.0.1.tgz#691d2709e78c79fae3a156622452d00762caaaa2" - integrity sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g== - dependencies: - callsites "^3.0.0" - -parent-require@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/parent-require/-/parent-require-1.0.0.tgz#746a167638083a860b0eef6732cb27ed46c32977" - integrity sha1-dGoWdjgIOoYLDu9nMssn7UbDKXc= - -parse5-htmlparser2-tree-adapter@^6.0.0: - version "6.0.1" - resolved "https://registry.yarnpkg.com/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-6.0.1.tgz#2cdf9ad823321140370d4dbf5d3e92c7c8ddc6e6" - integrity sha512-qPuWvbLgvDGilKc5BoicRovlT4MtYT6JfJyBOMDsKoiT+GiuP5qyrPCnR9HcPECIJJmZh5jRndyNThnhhb/vlA== - dependencies: - parse5 "^6.0.1" - -parse5@^5.1.1: - version "5.1.1" - resolved "https://registry.yarnpkg.com/parse5/-/parse5-5.1.1.tgz#f68e4e5ba1852ac2cadc00f4555fff6c2abb6178" - integrity sha512-ugq4DFI0Ptb+WWjAdOK16+u/nHfiIrcE+sh8kZMaM0WllQKLI9rOUq6c2b7cwPkXdzfQESqvoqK6ug7U/Yyzug== - -parse5@^6.0.1: - version "6.0.1" - resolved "https://registry.yarnpkg.com/parse5/-/parse5-6.0.1.tgz#e1a1c085c569b3dc08321184f19a39cc27f7c30b" - integrity sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw== - -path-is-absolute@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" - integrity sha1-F0uSaHNVNP+8es5r9TpanhtcX18= - -path-key@^3.1.0: - version "3.1.1" - resolved "https://registry.yarnpkg.com/path-key/-/path-key-3.1.1.tgz#581f6ade658cbba65a0d3380de7753295054f375" - integrity sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q== - -path-type@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b" - integrity sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw== - -pg-connection-string@^2.5.0: - version "2.5.0" - resolved "https://registry.yarnpkg.com/pg-connection-string/-/pg-connection-string-2.5.0.tgz#538cadd0f7e603fc09a12590f3b8a452c2c0cf34" - integrity sha512-r5o/V/ORTA6TmUnyWZR9nCj1klXCO2CEKNRlVuJptZe85QuhFayC7WeMic7ndayT5IRIR0S0xFxFi2ousartlQ== - -pg-int8@1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/pg-int8/-/pg-int8-1.0.1.tgz#943bd463bf5b71b4170115f80f8efc9a0c0eb78c" - integrity sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw== - -pg-pool@^3.4.1: - version "3.4.1" - resolved "https://registry.yarnpkg.com/pg-pool/-/pg-pool-3.4.1.tgz#0e71ce2c67b442a5e862a9c182172c37eda71e9c" - integrity sha512-TVHxR/gf3MeJRvchgNHxsYsTCHQ+4wm3VIHSS19z8NC0+gioEhq1okDY1sm/TYbfoP6JLFx01s0ShvZ3puP/iQ== - -pg-protocol@^1.5.0: - version "1.5.0" - resolved "https://registry.yarnpkg.com/pg-protocol/-/pg-protocol-1.5.0.tgz#b5dd452257314565e2d54ab3c132adc46565a6a0" - integrity sha512-muRttij7H8TqRNu/DxrAJQITO4Ac7RmX3Klyr/9mJEOBeIpgnF8f9jAfRz5d3XwQZl5qBjF9gLsUtMPJE0vezQ== - -pg-types@^2.1.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/pg-types/-/pg-types-2.2.0.tgz#2d0250d636454f7cfa3b6ae0382fdfa8063254a3" - integrity sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA== - dependencies: - pg-int8 "1.0.1" - postgres-array "~2.0.0" - postgres-bytea "~1.0.0" - postgres-date "~1.0.4" - postgres-interval "^1.1.0" - -pg@8.7.1: - version "8.7.1" - resolved "https://registry.yarnpkg.com/pg/-/pg-8.7.1.tgz#9ea9d1ec225980c36f94e181d009ab9f4ce4c471" - integrity sha512-7bdYcv7V6U3KAtWjpQJJBww0UEsWuh4yQ/EjNf2HeO/NnvKjpvhEIe/A/TleP6wtmSKnUnghs5A9jUoK6iDdkA== - dependencies: - buffer-writer "2.0.0" - packet-reader "1.0.0" - pg-connection-string "^2.5.0" - pg-pool "^3.4.1" - pg-protocol "^1.5.0" - pg-types "^2.1.0" - pgpass "1.x" - -pgpass@1.x: - version "1.0.4" - resolved "https://registry.yarnpkg.com/pgpass/-/pgpass-1.0.4.tgz#85eb93a83800b20f8057a2b029bf05abaf94ea9c" - integrity sha512-YmuA56alyBq7M59vxVBfPJrGSozru8QAdoNlWuW3cz8l+UX3cWge0vTvjKhsSHSJpo3Bom8/Mm6hf0TR5GY0+w== - dependencies: - split2 "^3.1.1" - -picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.2.3: - version "2.3.0" - resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.0.tgz#f1f061de8f6a4bf022892e2d128234fb98302972" - integrity sha512-lY1Q/PiJGC2zOv/z391WOTD+Z02bCgsFfvxoXXf6h7kv9o+WmsmzYqrAwY63sNgOxE4xEdq0WyUnXfKeBrSvYw== - -pino-std-serializers@^3.1.0: - version "3.2.0" - resolved "https://registry.yarnpkg.com/pino-std-serializers/-/pino-std-serializers-3.2.0.tgz#b56487c402d882eb96cd67c257868016b61ad671" - integrity sha512-EqX4pwDPrt3MuOAAUBMU0Tk5kR/YcCM5fNPEzgCO2zJ5HfX0vbiH9HbJglnyeQsN96Kznae6MWD47pZB5avTrg== - -pino@^6.13.0: - version "6.13.2" - resolved "https://registry.yarnpkg.com/pino/-/pino-6.13.2.tgz#948a0fcadca668f3b5fb8a427f2854b08661eccf" - integrity sha512-vmD/cabJ4xKqo9GVuAoAEeQhra8XJ7YydPV/JyIP+0zDtFTu5JSKdtt8eksGVWKtTSrNGcRrzJ4/IzvUWep3FA== - dependencies: - fast-redact "^3.0.0" - fast-safe-stringify "^2.0.8" - fastify-warning "^0.2.0" - flatstr "^1.0.12" - pino-std-serializers "^3.1.0" - quick-format-unescaped "^4.0.3" - sonic-boom "^1.0.2" - -postgres-array@~2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/postgres-array/-/postgres-array-2.0.0.tgz#48f8fce054fbc69671999329b8834b772652d82e" - integrity sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA== - -postgres-bytea@~1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/postgres-bytea/-/postgres-bytea-1.0.0.tgz#027b533c0aa890e26d172d47cf9ccecc521acd35" - integrity sha1-AntTPAqokOJtFy1Hz5zOzFIazTU= - -postgres-date@~1.0.4: - version "1.0.7" - resolved "https://registry.yarnpkg.com/postgres-date/-/postgres-date-1.0.7.tgz#51bc086006005e5061c591cee727f2531bf641a8" - integrity sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q== - -postgres-interval@^1.1.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/postgres-interval/-/postgres-interval-1.2.0.tgz#b460c82cb1587507788819a06aa0fffdb3544695" - integrity sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ== - dependencies: - xtend "^4.0.0" - -prelude-ls@^1.2.1: - version "1.2.1" - resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396" - integrity sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g== - -prepend-http@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/prepend-http/-/prepend-http-2.0.0.tgz#e92434bfa5ea8c19f41cdfd401d741a3c819d897" - integrity sha1-6SQ0v6XqjBn0HN/UAddBo8gZ2Jc= - -prettier-linter-helpers@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/prettier-linter-helpers/-/prettier-linter-helpers-1.0.0.tgz#d23d41fe1375646de2d0104d3454a3008802cf7b" - integrity sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w== - dependencies: - fast-diff "^1.1.2" - -prettier@2.5.1: - version "2.5.1" - resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.5.1.tgz#fff75fa9d519c54cf0fce328c1017d94546bc56a" - integrity sha512-vBZcPRUR5MZJwoyi3ZoyQlc1rXeEck8KgeC9AwwOn+exuxLxq5toTRDTSaVrXHxelDMHy9zlicw8u66yxoSUFg== - -prisma@3.7.0: - version "3.7.0" - resolved "https://registry.yarnpkg.com/prisma/-/prisma-3.7.0.tgz#9c73eeb2f16f767fdf523d0f4cc4c749734d62e2" - integrity sha512-pzgc95msPLcCHqOli7Hnabu/GRfSGSUWl5s2P6N13T/rgMB+NNeKbxCmzQiZT2yLOeLEPivV6YrW1oeQIwJxcg== - dependencies: - "@prisma/engines" "3.7.0-31.8746e055198f517658c08a0c426c7eec87f5a85f" - -process-warning@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/process-warning/-/process-warning-1.0.0.tgz#980a0b25dc38cd6034181be4b7726d89066b4616" - integrity sha512-du4wfLyj4yCZq1VupnVSZmRsPJsNuxoDQFdCFHLaYiEbFBD7QE0a+I4D7hOxrVnh78QE/YipFAj9lXHiXocV+Q== - -progress@^2.0.0: - version "2.0.3" - resolved "https://registry.yarnpkg.com/progress/-/progress-2.0.3.tgz#7e8cf8d8f5b8f239c1bc68beb4eb78567d572ef8" - integrity sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA== - -prom-client@14.0.1: - version "14.0.1" - resolved "https://registry.yarnpkg.com/prom-client/-/prom-client-14.0.1.tgz#bdd9583e02ec95429677c0e013712d42ef1f86a8" - integrity sha512-HxTArb6fkOntQHoRGvv4qd/BkorjliiuO2uSWC2KC17MUTKYttWdDoXX/vxOhQdkoECEM9BBH0pj2l8G8kev6w== - dependencies: - tdigest "^0.1.1" - -proxy-addr@^2.0.7: - version "2.0.7" - resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.7.tgz#f19fe69ceab311eeb94b42e70e8c2070f9ba1025" - integrity sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg== - dependencies: - forwarded "0.2.0" - ipaddr.js "1.9.1" - -pstree.remy@^1.1.8: - version "1.1.8" - resolved "https://registry.yarnpkg.com/pstree.remy/-/pstree.remy-1.1.8.tgz#c242224f4a67c21f686839bbdb4ac282b8373d3a" - integrity sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w== - -pump@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/pump/-/pump-3.0.0.tgz#b4a2116815bde2f4e1ea602354e8c75565107a64" - integrity sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww== - dependencies: - end-of-stream "^1.1.0" - once "^1.3.1" - -punycode@^2.1.0: - version "2.1.1" - resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec" - integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A== - -pupa@^2.1.1: - version "2.1.1" - resolved "https://registry.yarnpkg.com/pupa/-/pupa-2.1.1.tgz#f5e8fd4afc2c5d97828faa523549ed8744a20d62" - integrity sha512-l1jNAspIBSFqbT+y+5FosojNpVpF94nlI+wDUpqP9enwOTfHx9f0gh5nB96vl+6yTpsJsypeNrwfzPrKuHB41A== - dependencies: - escape-goat "^2.0.0" - -queue-microtask@^1.1.2, queue-microtask@^1.2.2: - version "1.2.3" - resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243" - integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A== - -quick-format-unescaped@^4.0.3: - version "4.0.3" - resolved "https://registry.yarnpkg.com/quick-format-unescaped/-/quick-format-unescaped-4.0.3.tgz#6d6b66b8207aa2b35eef12be1421bb24c428f652" - integrity sha512-MaL/oqh02mhEo5m5J2rwsVL23Iw2PEaGVHgT2vFt8AAsr0lfvQA5dpXo9TPu0rz7tSBdUPgkbam0j/fj5ZM8yg== - -rc@^1.2.8: - version "1.2.8" - resolved "https://registry.yarnpkg.com/rc/-/rc-1.2.8.tgz#cd924bf5200a075b83c188cd6b9e211b7fc0d3ed" - integrity sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw== - dependencies: - deep-extend "^0.6.0" - ini "~1.3.0" - minimist "^1.2.0" - strip-json-comments "~2.0.1" - -readable-stream@^3.0.0, readable-stream@^3.6.0: - version "3.6.0" - resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.0.tgz#337bbda3adc0706bd3e024426a286d4b4b2c9198" - integrity sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA== - dependencies: - inherits "^2.0.3" - string_decoder "^1.1.1" - util-deprecate "^1.0.1" - -readdirp@~3.6.0: - version "3.6.0" - resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.6.0.tgz#74a370bd857116e245b29cc97340cd431a02a6c7" - integrity sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA== - dependencies: - picomatch "^2.2.1" - -redis-commands@1.7.0: - version "1.7.0" - resolved "https://registry.yarnpkg.com/redis-commands/-/redis-commands-1.7.0.tgz#15a6fea2d58281e27b1cd1acfb4b293e278c3a89" - integrity sha512-nJWqw3bTFy21hX/CPKHth6sfhZbdiHP6bTawSgQBlKOVRG7EZkfHbbHwQJnrE4vsQf0CMNE+3gJ4Fmm16vdVlQ== - -redis-errors@^1.0.0, redis-errors@^1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/redis-errors/-/redis-errors-1.2.0.tgz#eb62d2adb15e4eaf4610c04afe1529384250abad" - integrity sha1-62LSrbFeTq9GEMBK/hUpOEJQq60= - -redis-parser@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/redis-parser/-/redis-parser-3.0.0.tgz#b66d828cdcafe6b4b8a428a7def4c6bcac31c8b4" - integrity sha1-tm2CjNyv5rS4pCin3vTGvKwxyLQ= - dependencies: - redis-errors "^1.0.0" - -reflect-metadata@0.1.13, reflect-metadata@^0.1.13: - version "0.1.13" - resolved "https://registry.yarnpkg.com/reflect-metadata/-/reflect-metadata-0.1.13.tgz#67ae3ca57c972a2aa1642b10fe363fe32d49dc08" - integrity sha512-Ts1Y/anZELhSsjMcU605fU9RE4Oi3p5ORujwbIKXfWa+0Zxs510Qrmrce5/Jowq3cHSZSJqBjypxmHarc+vEWg== - -regexpp@^3.1.0, regexpp@^3.2.0: - version "3.2.0" - resolved "https://registry.yarnpkg.com/regexpp/-/regexpp-3.2.0.tgz#0425a2768d8f23bad70ca4b90461fa2f1213e1b2" - integrity sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg== - -registry-auth-token@^4.0.0: - version "4.2.1" - resolved "https://registry.yarnpkg.com/registry-auth-token/-/registry-auth-token-4.2.1.tgz#6d7b4006441918972ccd5fedcd41dc322c79b250" - integrity sha512-6gkSb4U6aWJB4SF2ZvLb76yCBjcvufXBqvvEx1HbmKPkutswjW1xNVRY0+daljIYRbogN7O0etYSlbiaEQyMyw== - dependencies: - rc "^1.2.8" - -registry-url@^5.0.0: - version "5.1.0" - resolved "https://registry.yarnpkg.com/registry-url/-/registry-url-5.1.0.tgz#e98334b50d5434b81136b44ec638d9c2009c5009" - integrity sha512-8acYXXTI0AkQv6RAOjE3vOaIXZkT9wo4LOFbBKYQEEnnMNBpKqdUrI6S4NT0KPIo/WVvJ5tE/X5LF/TQUf0ekw== - dependencies: - rc "^1.2.8" - -require-directory@^2.1.1: - version "2.1.1" - resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42" - integrity sha1-jGStX9MNqxyXbiNE/+f3kqam30I= - -require-from-string@^2.0.2: - version "2.0.2" - resolved "https://registry.yarnpkg.com/require-from-string/-/require-from-string-2.0.2.tgz#89a7fdd938261267318eafe14f9c32e598c36909" - integrity sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw== - -resolve-from@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-4.0.0.tgz#4abcd852ad32dd7baabfe9b40e00a36db5f392e6" - integrity sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g== - -responselike@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/responselike/-/responselike-1.0.2.tgz#918720ef3b631c5642be068f15ade5a46f4ba1e7" - integrity sha1-kYcg7ztjHFZCvgaPFa3lpG9Loec= - dependencies: - lowercase-keys "^1.0.0" - -ret@~0.2.0: - version "0.2.2" - resolved "https://registry.yarnpkg.com/ret/-/ret-0.2.2.tgz#b6861782a1f4762dce43402a71eb7a283f44573c" - integrity sha512-M0b3YWQs7R3Z917WRQy1HHA7Ba7D8hvZg6UE5mLykJxQVE2ju0IXbGlaHPPlkY+WN7wFP+wUMXmBFA0aV6vYGQ== - -reusify@^1.0.4: - version "1.0.4" - resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.0.4.tgz#90da382b1e126efc02146e90845a88db12925d76" - integrity sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw== - -rfdc@^1.1.4, rfdc@^1.2.0: - version "1.3.0" - resolved "https://registry.yarnpkg.com/rfdc/-/rfdc-1.3.0.tgz#d0b7c441ab2720d05dc4cf26e01c89631d9da08b" - integrity sha512-V2hovdzFbOi77/WajaSMXk2OLm+xNIeQdMMuB7icj7bk6zi2F8GGAxigcnDFpJHbNyNcgyJDiP+8nOrY5cZGrA== - -rimraf@3.0.2, rimraf@^3.0.2: - version "3.0.2" - resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-3.0.2.tgz#f1a5402ba6220ad52cc1282bac1ae3aa49fd061a" - integrity sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA== - dependencies: - glob "^7.1.3" - -run-parallel@^1.1.9: - version "1.2.0" - resolved "https://registry.yarnpkg.com/run-parallel/-/run-parallel-1.2.0.tgz#66d1368da7bdf921eb9d95bd1a9229e7f21a43ee" - integrity sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA== - dependencies: - queue-microtask "^1.2.2" - -safe-buffer@^5.0.1, safe-buffer@~5.2.0: - version "5.2.1" - resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" - integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== - -safe-regex2@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/safe-regex2/-/safe-regex2-2.0.0.tgz#b287524c397c7a2994470367e0185e1916b1f5b9" - integrity sha512-PaUSFsUaNNuKwkBijoAPHAK6/eM6VirvyPWlZ7BAQy4D+hCvh4B6lIG+nPdhbFfIbP+gTGBcrdsOaUs0F+ZBOQ== - dependencies: - ret "~0.2.0" - -sax@>=0.6.0: - version "1.2.4" - resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9" - integrity sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw== - -secure-json-parse@^2.0.0: - version "2.4.0" - resolved "https://registry.yarnpkg.com/secure-json-parse/-/secure-json-parse-2.4.0.tgz#5aaeaaef85c7a417f76271a4f5b0cc3315ddca85" - integrity sha512-Q5Z/97nbON5t/L/sH6mY2EacfjVGwrCcSi5D3btRO2GZ8pf1K1UN7Z9H5J57hjVU2Qzxr1xO+FmBhOvEkzCMmg== - -semver-diff@^3.1.1: - version "3.1.1" - resolved "https://registry.yarnpkg.com/semver-diff/-/semver-diff-3.1.1.tgz#05f77ce59f325e00e2706afd67bb506ddb1ca32b" - integrity sha512-GX0Ix/CJcHyB8c4ykpHGIAvLyOwOobtM/8d+TQkAd81/bEjgPHrfba41Vpesr7jX/t8Uh+R3EX9eAS5be+jQYg== - dependencies: - semver "^6.3.0" - -semver-store@^0.3.0: - version "0.3.0" - resolved "https://registry.yarnpkg.com/semver-store/-/semver-store-0.3.0.tgz#ce602ff07df37080ec9f4fb40b29576547befbe9" - integrity sha512-TcZvGMMy9vodEFSse30lWinkj+JgOBvPn8wRItpQRSayhc+4ssDs335uklkfvQQJgL/WvmHLVj4Ycv2s7QCQMg== - -semver@^5.7.1: - version "5.7.1" - resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7" - integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ== - -semver@^6.0.0, semver@^6.2.0, semver@^6.3.0: - version "6.3.0" - resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d" - integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw== - -semver@^7.2.1, semver@^7.3.2, semver@^7.3.4, semver@^7.3.5: - version "7.3.5" - resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.5.tgz#0b621c879348d8998e4b0e4be94b3f12e6018ef7" - integrity sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ== - dependencies: - lru-cache "^6.0.0" - -set-cookie-parser@^2.4.1: - version "2.4.8" - resolved "https://registry.yarnpkg.com/set-cookie-parser/-/set-cookie-parser-2.4.8.tgz#d0da0ed388bc8f24e706a391f9c9e252a13c58b2" - integrity sha512-edRH8mBKEWNVIVMKejNnuJxleqYE/ZSdcT8/Nem9/mmosx12pctd80s2Oy00KNZzrogMZS5mauK2/ymL1bvlvg== - -sha.js@^2.4.11: - version "2.4.11" - resolved "https://registry.yarnpkg.com/sha.js/-/sha.js-2.4.11.tgz#37a5cf0b81ecbc6943de109ba2960d1b26584ae7" - integrity sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ== - dependencies: - inherits "^2.0.1" - safe-buffer "^5.0.1" - -shebang-command@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-2.0.0.tgz#ccd0af4f8835fbdc265b82461aaf0c36663f34ea" - integrity sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA== - dependencies: - shebang-regex "^3.0.0" - -shebang-regex@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172" - integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A== - -signal-exit@^3.0.2: - version "3.0.3" - resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.3.tgz#a1410c2edd8f077b08b4e253c8eacfcaf057461c" - integrity sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA== - -slash-create@5.0.1: - version "5.0.1" - resolved "https://registry.yarnpkg.com/slash-create/-/slash-create-5.0.1.tgz#074c182c09d0fbd4c59aceeedb77edb8a7769f0a" - integrity sha512-JxOAWuoXH38rvths7rhVGhSeI73eG82ckZu9Db3YxAEEDXsC1yy0iRyJt9VfhiO0DvIgtH/D6tbPPpV0GLC7Kw== - dependencies: - "@discordjs/collection" "^0.3.2" - eventemitter3 "^4.0.7" - lodash.isequal "^4.5.0" - tweetnacl "^1.0.3" - -slash@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/slash/-/slash-3.0.0.tgz#6539be870c165adbd5240220dbe361f1bc4d4634" - integrity sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q== - -sonic-boom@^1.0.2: - version "1.4.1" - resolved "https://registry.yarnpkg.com/sonic-boom/-/sonic-boom-1.4.1.tgz#d35d6a74076624f12e6f917ade7b9d75e918f53e" - integrity sha512-LRHh/A8tpW7ru89lrlkU4AszXt1dbwSjVWguGrmlxE7tawVmDBlI1PILMkXAxJTwqhgsEeTHzj36D5CmHgQmNg== - dependencies: - atomic-sleep "^1.0.0" - flatstr "^1.0.12" - -source-map-support@0.5.21, source-map-support@^0.5.21: - version "0.5.21" - resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.21.tgz#04fe7c7f9e1ed2d662233c28cb2b35b9f63f6e4f" - integrity sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w== - dependencies: - buffer-from "^1.0.0" - source-map "^0.6.0" - -source-map@^0.6.0: - version "0.6.1" - resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" - integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== - -split2@^3.1.1: - version "3.2.2" - resolved "https://registry.yarnpkg.com/split2/-/split2-3.2.2.tgz#bf2cf2a37d838312c249c89206fd7a17dd12365f" - integrity sha512-9NThjpgZnifTkJpzTZ7Eue85S49QwpNhZTq6GRJwObb6jnLFNGB7Qm73V5HewTROPyxD0C29xqmaI68bQtV+hg== - dependencies: - readable-stream "^3.0.0" - -sprintf-js@~1.0.2: - version "1.0.3" - resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c" - integrity sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw= - -standard-as-callback@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/standard-as-callback/-/standard-as-callback-2.1.0.tgz#8953fc05359868a77b5b9739a665c5977bb7df45" - integrity sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A== - -string-similarity@^4.0.1: - version "4.0.4" - resolved "https://registry.yarnpkg.com/string-similarity/-/string-similarity-4.0.4.tgz#42d01ab0b34660ea8a018da8f56a3309bb8b2a5b" - integrity sha512-/q/8Q4Bl4ZKAPjj8WerIBJWALKkaPRfrvhfF8k/B23i4nzrlRj2/go1m90In7nG/3XDSbOo0+pu6RvCTM9RGMQ== - -string-width@^3.0.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/string-width/-/string-width-3.1.0.tgz#22767be21b62af1081574306f69ac51b62203961" - integrity sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w== - dependencies: - emoji-regex "^7.0.1" - is-fullwidth-code-point "^2.0.0" - strip-ansi "^5.1.0" - -string-width@^4.0.0, string-width@^4.1.0, string-width@^4.2.0: - version "4.2.2" - resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.2.tgz#dafd4f9559a7585cfba529c6a0a4f73488ebd4c5" - integrity sha512-XBJbT3N4JhVumXE0eoLU9DCjcaF92KLNqTmFCnG1pf8duUxFGwtP6AD6nkjw9a3IdiRtL3E2w3JDiE/xi3vOeA== - dependencies: - emoji-regex "^8.0.0" - is-fullwidth-code-point "^3.0.0" - strip-ansi "^6.0.0" - -string-width@^4.2.2: - version "4.2.3" - resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" - integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== - dependencies: - emoji-regex "^8.0.0" - is-fullwidth-code-point "^3.0.0" - strip-ansi "^6.0.1" - -string_decoder@^1.1.1: - version "1.3.0" - resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.3.0.tgz#42f114594a46cf1a8e30b0a84f56c78c3edac21e" - integrity sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA== - dependencies: - safe-buffer "~5.2.0" - -strip-ansi@^3.0.0: - version "3.0.1" - resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-3.0.1.tgz#6a385fb8853d952d5ff05d0e8aaf94278dc63dcf" - integrity sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8= - dependencies: - ansi-regex "^2.0.0" - -strip-ansi@^5.1.0: - version "5.2.0" - resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-5.2.0.tgz#8c9a536feb6afc962bdfa5b104a5091c1ad9c0ae" - integrity sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA== - dependencies: - ansi-regex "^4.1.0" - -strip-ansi@^6.0.0: - version "6.0.0" - resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.0.tgz#0b1571dd7669ccd4f3e06e14ef1eed26225ae532" - integrity sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w== - dependencies: - ansi-regex "^5.0.0" - -strip-ansi@^6.0.1: - version "6.0.1" - resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" - integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== - dependencies: - ansi-regex "^5.0.1" - -strip-json-comments@^3.1.0, strip-json-comments@^3.1.1: - version "3.1.1" - resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz#31f1281b3832630434831c310c01cccda8cbe006" - integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig== - -strip-json-comments@~2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a" - integrity sha1-PFMZQukIwml8DsNEhYwobHygpgo= - -supports-color@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-2.0.0.tgz#535d045ce6b6363fa40117084629995e9df324c7" - integrity sha1-U10EXOa2Nj+kARcIRimZXp3zJMc= - -supports-color@^5.5.0: - version "5.5.0" - resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f" - integrity sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow== - dependencies: - has-flag "^3.0.0" - -supports-color@^7.1.0: - version "7.2.0" - resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.2.0.tgz#1b7dcdcb32b8138801b3e478ba6a51caa89648da" - integrity sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw== - dependencies: - has-flag "^4.0.0" - -tdigest@^0.1.1: - version "0.1.1" - resolved "https://registry.yarnpkg.com/tdigest/-/tdigest-0.1.1.tgz#2e3cb2c39ea449e55d1e6cd91117accca4588021" - integrity sha1-Ljyyw56kSeVdHmzZEReszKRYgCE= - dependencies: - bintrees "1.0.1" - -text-table@^0.2.0: - version "0.2.0" - resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4" - integrity sha1-f17oI66AUgfACvLfSoTsP8+lcLQ= - -thenify-all@^1.0.0: - version "1.6.0" - resolved "https://registry.yarnpkg.com/thenify-all/-/thenify-all-1.6.0.tgz#1a1918d402d8fc3f98fbf234db0bcc8cc10e9726" - integrity sha1-GhkY1ALY/D+Y+/I02wvMjMEOlyY= - dependencies: - thenify ">= 3.1.0 < 4" - -"thenify@>= 3.1.0 < 4": - version "3.3.1" - resolved "https://registry.yarnpkg.com/thenify/-/thenify-3.3.1.tgz#8932e686a4066038a016dd9e2ca46add9838a95f" - integrity sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw== - dependencies: - any-promise "^1.0.0" - -tiny-lru@^7.0.0: - version "7.0.6" - resolved "https://registry.yarnpkg.com/tiny-lru/-/tiny-lru-7.0.6.tgz#b0c3cdede1e5882aa2d1ae21cb2ceccf2a331f24" - integrity sha512-zNYO0Kvgn5rXzWpL0y3RS09sMK67eGaQj9805jlK9G6pSadfriTczzLHFXa/xcW4mIRfmlB9HyQ/+SgL0V1uow== - -to-readable-stream@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/to-readable-stream/-/to-readable-stream-1.0.0.tgz#ce0aa0c2f3df6adf852efb404a783e77c0475771" - integrity sha512-Iq25XBt6zD5npPhlLVXGFN3/gyR2/qODcKNNyTMd4vbm39HUaOiAM4PMq0eMVC/Tkxz+Zjdsc55g9yyz+Yq00Q== - -to-regex-range@^5.0.1: - version "5.0.1" - resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-5.0.1.tgz#1648c44aae7c8d988a326018ed72f5b4dd0392e4" - integrity sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ== - dependencies: - is-number "^7.0.0" - -touch@^3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/touch/-/touch-3.1.0.tgz#fe365f5f75ec9ed4e56825e0bb76d24ab74af83b" - integrity sha512-WBx8Uy5TLtOSRtIq+M03/sKDrXCLHxwDcquSP2c43Le03/9serjQBIztjRz6FkJez9D/hleyAXTBGLwwZUw9lA== - dependencies: - nopt "~1.0.10" - -ts-node@10.4.0: - version "10.4.0" - resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-10.4.0.tgz#680f88945885f4e6cf450e7f0d6223dd404895f7" - integrity sha512-g0FlPvvCXSIO1JDF6S232P5jPYqBkRL9qly81ZgAOSU7rwI0stphCgd2kLiCrU9DjQCrJMWEqcNSjQL02s6d8A== - dependencies: - "@cspotcode/source-map-support" "0.7.0" - "@tsconfig/node10" "^1.0.7" - "@tsconfig/node12" "^1.0.7" - "@tsconfig/node14" "^1.0.0" - "@tsconfig/node16" "^1.0.2" - acorn "^8.4.1" - acorn-walk "^8.1.1" - arg "^4.1.0" - create-require "^1.1.0" - diff "^4.0.1" - make-error "^1.1.1" - yn "3.1.1" - -tslib@^1.13.0, tslib@^1.8.1, tslib@^1.9.3: - version "1.14.1" - resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00" - integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg== - -tslog@3.3.1: - version "3.3.1" - resolved "https://registry.yarnpkg.com/tslog/-/tslog-3.3.1.tgz#cf5b236772c05c59e183dc1d088e4dbf5bcd8f85" - integrity sha512-An3uyXX95uU/X7v5H6G9OKW6ip/gVOpvsERGJ/nR4Or5TP5GwoI9nUjhNWEc8mJOWC7uhPMg2UzkrVDUtadELg== - dependencies: - source-map-support "^0.5.21" - -tsutils@^3.21.0: - version "3.21.0" - resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-3.21.0.tgz#b48717d394cea6c1e096983eed58e9d61715b623" - integrity sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA== - dependencies: - tslib "^1.8.1" - -tweetnacl@^1.0.1, tweetnacl@^1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-1.0.3.tgz#ac0af71680458d8a6378d0d0d050ab1407d35596" - integrity sha512-6rt+RN7aOi1nGMyC4Xa5DdYiukl2UWCbcJft7YhxReBGQD7OAM8Pbxw6YMo4r2diNEA8FEmu32YOn9rhaiE5yw== - -type-check@^0.4.0, type-check@~0.4.0: - version "0.4.0" - resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.4.0.tgz#07b8203bfa7056c0657050e3ccd2c37730bab8f1" - integrity sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew== - dependencies: - prelude-ls "^1.2.1" - -type-fest@^0.20.2: - version "0.20.2" - resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.20.2.tgz#1bf207f4b28f91583666cb5fbd327887301cd5f4" - integrity sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ== - -typedarray-to-buffer@^3.1.5: - version "3.1.5" - resolved "https://registry.yarnpkg.com/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz#a97ee7a9ff42691b9f783ff1bc5112fe3fca9080" - integrity sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q== - dependencies: - is-typedarray "^1.0.0" - -typeorm@0.2.31: - version "0.2.31" - resolved "https://registry.yarnpkg.com/typeorm/-/typeorm-0.2.31.tgz#82b8a1b233224f81c738f53b0380386ccf360917" - integrity sha512-dVvCEVHH48DG0QPXAKfo0l6ecQrl3A8ucGP4Yw4myz4YEDMProebTQo8as83uyES+nrwCbu3qdkL4ncC2+qcMA== - dependencies: - "@sqltools/formatter" "1.2.2" - app-root-path "^3.0.0" - buffer "^5.5.0" - chalk "^4.1.0" - cli-highlight "^2.1.10" - debug "^4.1.1" - dotenv "^8.2.0" - glob "^7.1.6" - js-yaml "^3.14.0" - mkdirp "^1.0.4" - reflect-metadata "^0.1.13" - sha.js "^2.4.11" - tslib "^1.13.0" - xml2js "^0.4.23" - yargonaut "^1.1.2" - yargs "^16.0.3" - -typescript@4.5.4: - version "4.5.4" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.5.4.tgz#a17d3a0263bf5c8723b9c52f43c5084edf13c2e8" - integrity sha512-VgYs2A2QIRuGphtzFV7aQJduJ2gyfTljngLzjpfW9FoYZF6xuw1W0vW9ghCKLfcWrCFxK81CSGRAvS1pn4fIUg== - -undefsafe@^2.0.5: - version "2.0.5" - resolved "https://registry.yarnpkg.com/undefsafe/-/undefsafe-2.0.5.tgz#38733b9327bdcd226db889fb723a6efd162e6e2c" - integrity sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA== - -undici@3.3.6: - version "3.3.6" - resolved "https://registry.yarnpkg.com/undici/-/undici-3.3.6.tgz#06d3b97b7eeff46bce6f8a71079c09f64dd59dc1" - integrity sha512-/j3YTZ5AobMB4ZrTY72mzM54uFUX32v0R/JRW9G2vOyF1uSKYAx+WT8dMsAcRS13TOFISv094TxIyWYk+WEPsA== - -unique-string@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/unique-string/-/unique-string-2.0.0.tgz#39c6451f81afb2749de2b233e3f7c5e8843bd89d" - integrity sha512-uNaeirEPvpZWSgzwsPGtU2zVSTrn/8L5q/IexZmH0eH6SA73CmAA5U4GwORTxQAZs95TAXLNqeLoPPNO5gZfWg== - dependencies: - crypto-random-string "^2.0.0" - -update-notifier@^5.1.0: - version "5.1.0" - resolved "https://registry.yarnpkg.com/update-notifier/-/update-notifier-5.1.0.tgz#4ab0d7c7f36a231dd7316cf7729313f0214d9ad9" - integrity sha512-ItnICHbeMh9GqUy31hFPrD1kcuZ3rpxDZbf4KUDavXwS0bW5m7SLbDQpGX3UYr072cbrF5hFUs3r5tUsPwjfHw== - dependencies: - boxen "^5.0.0" - chalk "^4.1.0" - configstore "^5.0.1" - has-yarn "^2.1.0" - import-lazy "^2.1.0" - is-ci "^2.0.0" - is-installed-globally "^0.4.0" - is-npm "^5.0.0" - is-yarn-global "^0.3.0" - latest-version "^5.1.0" - pupa "^2.1.1" - semver "^7.3.4" - semver-diff "^3.1.1" - xdg-basedir "^4.0.0" - -uri-js@^4.2.2: - version "4.4.1" - resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.4.1.tgz#9b1a52595225859e55f669d928f88c6c57f2a77e" - integrity sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg== - dependencies: - punycode "^2.1.0" - -url-parse-lax@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/url-parse-lax/-/url-parse-lax-3.0.0.tgz#16b5cafc07dbe3676c1b1999177823d6503acb0c" - integrity sha1-FrXK/Afb42dsGxmZF3gj1lA6yww= - dependencies: - prepend-http "^2.0.0" - -util-deprecate@^1.0.1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" - integrity sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8= - -v8-compile-cache@^2.0.3: - version "2.3.0" - resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz#2de19618c66dc247dcfb6f99338035d8245a2cee" - integrity sha512-l8lCEmLcLYZh4nbunNZvQCJc5pv7+RCwa8q/LdUx8u7lsWvPDKmpodJAJNwkAhJC//dFY48KuIEmjtd4RViDrA== - -vary@^1.1.2: - version "1.1.2" - resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc" - integrity sha1-IpnwLG3tMNSllhsLn3RSShj2NPw= - -which@^2.0.1: - version "2.0.2" - resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1" - integrity sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA== - dependencies: - isexe "^2.0.0" - -widest-line@^3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/widest-line/-/widest-line-3.1.0.tgz#8292333bbf66cb45ff0de1603b136b7ae1496eca" - integrity sha512-NsmoXalsWVDMGupxZ5R08ka9flZjjiLvHVAWYOKtiKM8ujtZWr9cRffak+uSE48+Ob8ObalXpwyeUiyDD6QFgg== - dependencies: - string-width "^4.0.0" - -word-wrap@^1.2.3: - version "1.2.3" - resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.3.tgz#610636f6b1f703891bd34771ccb17fb93b47079c" - integrity sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ== - -wrap-ansi@^7.0.0: - version "7.0.0" - resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" - integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== - dependencies: - ansi-styles "^4.0.0" - string-width "^4.1.0" - strip-ansi "^6.0.0" - -wrappy@1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" - integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8= - -write-file-atomic@^3.0.0: - version "3.0.3" - resolved "https://registry.yarnpkg.com/write-file-atomic/-/write-file-atomic-3.0.3.tgz#56bd5c5a5c70481cd19c571bd39ab965a5de56e8" - integrity sha512-AvHcyZ5JnSfq3ioSyjrBkH9yW4m7Ayk8/9My/DD9onKeu/94fwrMocemO2QAJFAlnnDN+ZDS+ZjAR5ua1/PV/Q== - dependencies: - imurmurhash "^0.1.4" - is-typedarray "^1.0.0" - signal-exit "^3.0.2" - typedarray-to-buffer "^3.1.5" - -ws@8.4.0: - version "8.4.0" - resolved "https://registry.yarnpkg.com/ws/-/ws-8.4.0.tgz#f05e982a0a88c604080e8581576e2a063802bed6" - integrity sha512-IHVsKe2pjajSUIl4KYMQOdlyliovpEPquKkqbwswulszzI7r0SfQrxnXdWAEqOlDCLrVSJzo+O1hAwdog2sKSQ== - -ws@^7.4.6: - version "7.5.4" - resolved "https://registry.yarnpkg.com/ws/-/ws-7.5.4.tgz#56bfa20b167427e138a7795de68d134fe92e21f9" - integrity sha512-zP9z6GXm6zC27YtspwH99T3qTG7bBFv2VIkeHstMLrLlDJuzA7tQ5ls3OJ1hOGGCzTQPniNJoHXIAOS0Jljohg== - -xdg-basedir@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/xdg-basedir/-/xdg-basedir-4.0.0.tgz#4bc8d9984403696225ef83a1573cbbcb4e79db13" - integrity sha512-PSNhEJDejZYV7h50BohL09Er9VaIefr2LMAf3OEmpCkjOi34eYyQYAXUTjEQtZJTKcF0E2UKTh+osDLsgNim9Q== - -xml2js@^0.4.23: - version "0.4.23" - resolved "https://registry.yarnpkg.com/xml2js/-/xml2js-0.4.23.tgz#a0c69516752421eb2ac758ee4d4ccf58843eac66" - integrity sha512-ySPiMjM0+pLDftHgXY4By0uswI3SPKLDw/i3UXbnO8M/p28zqexCUoPmQFrYD+/1BzhGJSs2i1ERWKJAtiLrug== - dependencies: - sax ">=0.6.0" - xmlbuilder "~11.0.0" - -xmlbuilder@~11.0.0: - version "11.0.1" - resolved "https://registry.yarnpkg.com/xmlbuilder/-/xmlbuilder-11.0.1.tgz#be9bae1c8a046e76b31127726347d0ad7002beb3" - integrity sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA== - -xtend@^4.0.0: - version "4.0.2" - resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54" - integrity sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ== - -y18n@^5.0.5: - version "5.0.8" - resolved "https://registry.yarnpkg.com/y18n/-/y18n-5.0.8.tgz#7f4934d0f7ca8c56f95314939ddcd2dd91ce1d55" - integrity sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA== - -yallist@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72" - integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A== - -yargonaut@^1.1.2: - version "1.1.4" - resolved "https://registry.yarnpkg.com/yargonaut/-/yargonaut-1.1.4.tgz#c64f56432c7465271221f53f5cc517890c3d6e0c" - integrity sha512-rHgFmbgXAAzl+1nngqOcwEljqHGG9uUZoPjsdZEs1w5JW9RXYzrSvH/u70C1JE5qFi0qjsdhnUX/dJRpWqitSA== - dependencies: - chalk "^1.1.1" - figlet "^1.1.1" - parent-require "^1.0.0" - -yargs-parser@^20.2.2: - version "20.2.9" - resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-20.2.9.tgz#2eb7dc3b0289718fc295f362753845c41a0c94ee" - integrity sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w== - -yargs@^16.0.0, yargs@^16.0.3: - version "16.2.0" - resolved "https://registry.yarnpkg.com/yargs/-/yargs-16.2.0.tgz#1c82bf0f6b6a66eafce7ef30e376f49a12477f66" - integrity sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw== - dependencies: - cliui "^7.0.2" - escalade "^3.1.1" - get-caller-file "^2.0.5" - require-directory "^2.1.1" - string-width "^4.2.0" - y18n "^5.0.5" - yargs-parser "^20.2.2" - -yn@3.1.1: - version "3.1.1" - resolved "https://registry.yarnpkg.com/yn/-/yn-3.1.1.tgz#1e87401a09d767c1d5eab26a6e4c185182d2eb50" - integrity sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==