├── .dockerignore ├── .env.example ├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── bug.yml │ ├── config.yml │ ├── docs.yml │ ├── ideas.yml │ └── other.yml ├── PULL_REQUEST_TEMPLATE │ └── pull.md └── workflows │ ├── build.yml │ ├── codeql.yml │ ├── deploy.yml │ ├── docker.yml │ ├── kubescape.yml │ └── release.yml ├── .gitignore ├── .gitpod.yml ├── .gitpod └── Dockerfile ├── .husky ├── .gitignore ├── commit-msg └── pre-commit ├── .prettierrc ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── Dockerfile ├── LICENSE ├── README.md ├── SECURITY.md ├── captain-definition ├── commitlint.config.js ├── eslint.config.mjs ├── jest.config.ts ├── kubernetes └── deployment.yaml ├── mongo-compose.yml ├── package-lock.json ├── package.json ├── src ├── alexjs │ ├── __tests__ │ │ └── stripSpecialCharacters.test.ts │ ├── checkBannedWords.ts │ ├── checkContent.ts │ └── stripSpecialCharacters.ts ├── commands │ ├── help.ts │ ├── stats.ts │ └── version.ts ├── config │ ├── AlexJsOptions.ts │ ├── BannedWordsOptions.ts │ ├── DictionaryOptions.ts │ ├── IntentOptions.ts │ └── UrlRegex.ts ├── database │ ├── connectDb.ts │ └── models │ │ ├── ServerConfig.ts │ │ ├── Statistics.ts │ │ └── Warnings.ts ├── events │ ├── _handleEvents.ts │ ├── onDelete.ts │ ├── onInteraction.ts │ ├── onMessage.ts │ └── onUpdate.ts ├── interfaces │ ├── Command.ts │ └── ExtendedClient.ts ├── links │ └── checkLinks.ts ├── main.ts ├── typo │ └── dict │ │ └── en_US │ │ ├── dictionaries.md │ │ ├── en_US.aff │ │ └── en_US.dic └── utils │ ├── __tests__ │ └── typoFixer.test.ts │ ├── errorHandler.ts │ ├── getAlexConfig.ts │ ├── getBannedWordConfig.ts │ ├── loadCommands.ts │ ├── logHandler.ts │ ├── registerCommands.ts │ ├── typoFixer.ts │ └── validateEnv.ts ├── tsconfig.build.json └── tsconfig.json /.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | npm-debug.log 3 | prod 4 | .git 5 | .gitignore 6 | Dockerfile 7 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | DISCORD_TOKEN= 2 | EDDIEBOT_MONGO_CONNECTION_STRING= 3 | DEBUG_HOOK= 4 | HOME_GUILD= 5 | NODE_ENV="development" 6 | ADMIN_CHANNEL= 7 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [eddiejaoude] 4 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug.yml: -------------------------------------------------------------------------------- 1 | name: Bug 2 | description: Report a bug found in the bot's source code 3 | labels: ["🛠 goal: fix"] 4 | body: 5 | - type: textarea 6 | id: description 7 | attributes: 8 | label: Description 9 | description: A brief description of the question or issue, also include what you tried and what didn't work 10 | validations: 11 | required: true 12 | - type: textarea 13 | id: screenshots 14 | attributes: 15 | label: Screenshots 16 | description: Please add screenshots if applicable 17 | validations: 18 | required: false 19 | - type: textarea 20 | id: extrainfo 21 | attributes: 22 | label: Additional information 23 | description: Is there anything else we should know about this bug? 24 | validations: 25 | required: false 26 | - type: markdown 27 | attributes: 28 | value: | 29 | You can also join the Discord community [here](http://discord.eddiehub.org) 30 | Feel free to check out other cool repositories of the EddieHub Community [here](https://github.com/EddieHubCommunity) 31 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/docs.yml: -------------------------------------------------------------------------------- 1 | name: Documentation typo 2 | description: Found a typo in the documentation? You can use this one! 3 | labels: ["📄 aspect: text"] 4 | body: 5 | - type: textarea 6 | id: description 7 | attributes: 8 | label: Description 9 | description: A brief description of the question or issue, also include what you tried and what didn't work 10 | validations: 11 | required: true 12 | - type: textarea 13 | id: screenshots 14 | attributes: 15 | label: Screenshots 16 | description: Please add screenshots if applicable 17 | validations: 18 | required: false 19 | - type: textarea 20 | id: extrainfo 21 | attributes: 22 | label: Additional information 23 | description: Is there anything else we should know about this issue? 24 | validations: 25 | required: false 26 | - type: markdown 27 | attributes: 28 | value: | 29 | You can also join the Discord community [here](http://discord.eddiehub.org) 30 | Feel free to check out other cool repositories of the EddieHub Community [here](https://github.com/EddieHubCommunity) 31 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/ideas.yml: -------------------------------------------------------------------------------- 1 | name: Ideas 2 | description: Have a new idea/feature for EddieBot? 3 | title: "[FEATURE] " 4 | labels: ["⭐ goal: addition"] 5 | body: 6 | - type: textarea 7 | id: description 8 | attributes: 9 | label: Description 10 | description: A brief description of the question or issue, also include what you tried and what didn't work 11 | - type: textarea 12 | id: screenshots 13 | attributes: 14 | label: Screenshots 15 | description: Please add screenshots if applicable 16 | validations: 17 | required: false 18 | - type: textarea 19 | id: extrainfo 20 | attributes: 21 | label: Additional information 22 | description: Is there anything else we should know about this idea? 23 | validations: 24 | required: false 25 | - type: markdown 26 | attributes: 27 | value: | 28 | You can also join the Discord community [here](http://discord.eddiehub.org) 29 | Feel free to check out other cool repositories of the EddieHub Community [here](https://github.com/EddieHubCommunity) 30 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/other.yml: -------------------------------------------------------------------------------- 1 | name: Other 2 | description: Use this for any other issues. Please do NOT create blank issues 3 | title: "[OTHER]" 4 | labels: ["🚦 status: awaiting triage"] 5 | body: 6 | - type: markdown 7 | attributes: 8 | value: "# Other issue" 9 | - type: textarea 10 | id: issuedescription 11 | attributes: 12 | label: What would you like to share? 13 | description: Provide a clear and concise explanation of your issue. 14 | validations: 15 | required: true 16 | - type: textarea 17 | id: extrainfo 18 | attributes: 19 | label: Additional information 20 | description: Is there anything else we should know about this issue? 21 | validations: 22 | required: false 23 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE/pull.md: -------------------------------------------------------------------------------- 1 | ## Pull request classification 2 | 3 | Please remove options that are not relevant. 4 | 5 | - [ ] Bug fix (changes that fix an issue) 6 | - [ ] New feature (changes which add functionality) 7 | - [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected) 8 | - [ ] Documentation update 9 | - [ ] Adding flowcharts for better understanding 10 | 11 | # Description 12 | 13 | Please include a summary of the change and which issue is fixed. Please also include relevant motivation and context. 14 | 15 | List any dependencies that are required for this change. 16 | 17 | Fixes # (issue-number) 18 | 19 | # Checklist: 20 | 21 | - [ ] My code follows the style guidelines of this project 22 | - [ ] I have performed a self-review of my own code 23 | - [ ] I have commented on my code, particularly in hard-to-understand areas 24 | - [ ] I have made corresponding changes to the documentation 25 | - [ ] My changes generate no new warnings 26 | - [ ] I have added tests that prove my fix is effective or that my feature works 27 | - [ ] New and existing unit tests pass locally with my changes 28 | - [ ] Created separate branch for separate bug/feature/documentation 29 | - [ ] Any dependent changes have been merged and published in downstream modules -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | lint: 7 | runs-on: ubuntu-latest 8 | 9 | steps: 10 | - uses: actions/checkout@v4 11 | - uses: actions/setup-node@v4 12 | with: 13 | node-version: 20 14 | - name: install dependencies 15 | run: npm ci 16 | - name: lint check 17 | run: npm run lint 18 | - name: run tests 19 | run: npm run test 20 | 21 | build: 22 | runs-on: ubuntu-latest 23 | 24 | steps: 25 | - uses: actions/checkout@v4 26 | - uses: actions/setup-node@v4 27 | with: 28 | node-version: 20 29 | - name: install dependencies 30 | run: npm ci 31 | - name: build app 32 | run: npm run build 33 | 34 | -------------------------------------------------------------------------------- /.github/workflows/codeql.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CodeQL" 13 | 14 | on: 15 | push: 16 | branches: [ "main" ] 17 | pull_request: 18 | branches: [ "main" ] 19 | workflow_dispatch: 20 | 21 | jobs: 22 | analyze: 23 | name: Analyze (${{ matrix.language }}) 24 | # Runner size impacts CodeQL analysis time. To learn more, please see: 25 | # - https://gh.io/recommended-hardware-resources-for-running-codeql 26 | # - https://gh.io/supported-runners-and-hardware-resources 27 | # - https://gh.io/using-larger-runners (GitHub.com only) 28 | # Consider using larger runners or machines with greater resources for possible analysis time improvements. 29 | runs-on: ${{ (matrix.language == 'swift' && 'macos-latest') || 'ubuntu-latest' }} 30 | timeout-minutes: ${{ (matrix.language == 'swift' && 120) || 360 }} 31 | permissions: 32 | # required for all workflows 33 | security-events: write 34 | 35 | # required to fetch internal or private CodeQL packs 36 | packages: read 37 | 38 | # only required for workflows in private repositories 39 | actions: read 40 | contents: read 41 | 42 | strategy: 43 | fail-fast: false 44 | matrix: 45 | include: 46 | - language: javascript-typescript 47 | build-mode: none 48 | # CodeQL supports the following values keywords for 'language': 'c-cpp', 'csharp', 'go', 'java-kotlin', 'javascript-typescript', 'python', 'ruby', 'swift' 49 | # Use `c-cpp` to analyze code written in C, C++ or both 50 | # Use 'java-kotlin' to analyze code written in Java, Kotlin or both 51 | # Use 'javascript-typescript' to analyze code written in JavaScript, TypeScript or both 52 | # To learn more about changing the languages that are analyzed or customizing the build mode for your analysis, 53 | # see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/customizing-your-advanced-setup-for-code-scanning. 54 | # If you are analyzing a compiled language, you can modify the 'build-mode' for that language to customize how 55 | # your codebase is analyzed, see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/codeql-code-scanning-for-compiled-languages 56 | steps: 57 | - name: Checkout repository 58 | uses: actions/checkout@v4 59 | 60 | # Initializes the CodeQL tools for scanning. 61 | - name: Initialize CodeQL 62 | uses: github/codeql-action/init@v3 63 | with: 64 | languages: ${{ matrix.language }} 65 | build-mode: ${{ matrix.build-mode }} 66 | # If you wish to specify custom queries, you can do so here or in a config file. 67 | # By default, queries listed here will override any specified in a config file. 68 | # Prefix the list here with "+" to use these queries and those in the config file. 69 | 70 | # For more details on CodeQL's query packs, refer to: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs 71 | # queries: security-extended,security-and-quality 72 | 73 | # If the analyze step fails for one of the languages you are analyzing with 74 | # "We were unable to automatically build your code", modify the matrix above 75 | # to set the build mode to "manual" for that language. Then modify this step 76 | # to build your code. 77 | # ℹ️ Command-line programs to run using the OS shell. 78 | # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun 79 | - if: matrix.build-mode == 'manual' 80 | shell: bash 81 | run: | 82 | echo 'If you are using a "manual" build mode for one or more of the' \ 83 | 'languages you are analyzing, replace this with the commands to build' \ 84 | 'your code, for example:' 85 | echo ' make bootstrap' 86 | echo ' make release' 87 | exit 1 88 | 89 | - name: Perform CodeQL Analysis 90 | uses: github/codeql-action/analyze@v3 91 | with: 92 | category: "/language:${{matrix.language}}" 93 | -------------------------------------------------------------------------------- /.github/workflows/deploy.yml: -------------------------------------------------------------------------------- 1 | name: Deploy to Caprover 2 | 3 | on: 4 | registry_package: 5 | types: [published] 6 | 7 | jobs: 8 | build-and-deploy: 9 | runs-on: ubuntu-latest 10 | 11 | strategy: 12 | matrix: 13 | node-version: [18.x] 14 | 15 | steps: 16 | - name: Deploy Image to CapRrover 17 | uses: caprover/deploy-from-github@v1.1.2 18 | with: 19 | server: "${{ secrets.CAPROVER_SERVER }}" 20 | app: eddie-bot 21 | token: "${{ secrets.CAPROVER_TOKEN }}" 22 | image: ghcr.io/eddiehubcommunity/eddiebot:latest 23 | -------------------------------------------------------------------------------- /.github/workflows/docker.yml: -------------------------------------------------------------------------------- 1 | name: Publish Docker image 2 | on: 3 | release: 4 | types: [published] 5 | 6 | jobs: 7 | build: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/checkout@v4 11 | - name: npm install and build 12 | run: | 13 | npm ci 14 | npm run build 15 | - uses: actions/upload-artifact@main 16 | with: 17 | name: artifacts 18 | path: prod/ 19 | push_to_registry: 20 | name: Push Docker image to GitHub Packages 21 | needs: build 22 | runs-on: ubuntu-latest 23 | steps: 24 | - name: check out the repo 25 | uses: actions/checkout@v4 26 | - name: get-npm-version 27 | id: package-version 28 | uses: martinbeentjes/npm-get-version-action@master 29 | - name: version dockerfile 30 | run: sed -i 's/v0.0.0/v${{ steps.package-version.outputs.current-version}}/g' Dockerfile 31 | - name: set up Docker builder 32 | uses: docker/setup-buildx-action@v3 33 | - name: log into GitHub Container Registry 34 | uses: docker/login-action@v3 35 | with: 36 | registry: ghcr.io 37 | username: ${{ github.repository_owner }} 38 | password: ${{ secrets.CR_PAT }} 39 | - name: push to Github Container Registry 40 | uses: docker/build-push-action@v6 41 | with: 42 | context: . 43 | push: true 44 | tags: | 45 | ghcr.io/eddiehubcommunity/eddiebot:v${{ steps.package-version.outputs.current-version}} 46 | ghcr.io/eddiehubcommunity/eddiebot:latest 47 | -------------------------------------------------------------------------------- /.github/workflows/kubescape.yml: -------------------------------------------------------------------------------- 1 | name: Kubescape 2 | on: 3 | push: 4 | paths: 5 | - 'kubernetes/**' 6 | pull_request: 7 | paths: 8 | - 'kubernetes/**' 9 | 10 | jobs: 11 | security: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v4 15 | - uses: actions/setup-node@v4 16 | with: 17 | node-version: '20' 18 | - name: Install Kubescape 19 | run: curl -s https://raw.githubusercontent.com/kubescape/kubescape/master/install.sh | /bin/bash 20 | - name: Kubescape 21 | run: kubescape scan kubernetes/*.yaml -v --fail-threshold 40 22 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Releases 2 | on: 3 | push: 4 | branches: 5 | - main 6 | 7 | jobs: 8 | changelog: 9 | runs-on: ubuntu-latest 10 | 11 | steps: 12 | - uses: actions/checkout@v4 13 | 14 | - name: conventional Changelog Action 15 | id: changelog 16 | uses: TriPSs/conventional-changelog-action@v5 17 | with: 18 | github-token: ${{ secrets.CHANGELOG_RELEASE }} 19 | 20 | - name: create release 21 | uses: softprops/action-gh-release@v2 22 | if: ${{ steps.changelog.outputs.skipped == 'false' }} 23 | with: 24 | name: ${{ steps.changelog.outputs.tag }} 25 | tag_name: ${{ steps.changelog.outputs.tag }} 26 | body: ${{ steps.changelog.outputs.clean_changelog }} 27 | token: ${{ secrets.CHANGELOG_RELEASE }} 28 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # compiled output 2 | /prod 3 | /node_modules 4 | 5 | # Logs 6 | logs 7 | *.log 8 | npm-debug.log* 9 | yarn-debug.log* 10 | yarn-error.log* 11 | lerna-debug.log* 12 | 13 | # OS 14 | .DS_Store 15 | 16 | # Tests 17 | /coverage 18 | /.nyc_output 19 | 20 | # IDEs and editors 21 | /.idea 22 | .project 23 | .classpath 24 | .c9/ 25 | *.launch 26 | .settings/ 27 | *.sublime-workspace 28 | 29 | # IDE - VSCode 30 | .vscode/* 31 | 32 | .env -------------------------------------------------------------------------------- /.gitpod.yml: -------------------------------------------------------------------------------- 1 | tasks: 2 | - name: Eddiebot 3 | init: | 4 | npm install 5 | npm run build 6 | - name: Mongo 7 | command: mkdir -p /workspace/data && mongod --dbpath /workspace/data 8 | 9 | image: 10 | file: .gitpod/Dockerfile 11 | 12 | github: 13 | prebuilds: 14 | main: true 15 | branches: true 16 | pullRequests: true 17 | pullRequestsFromForks: true 18 | addCheck: true 19 | addComment: false 20 | addBadge: true 21 | -------------------------------------------------------------------------------- /.gitpod/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM gitpod/workspace-mongodb 2 | -------------------------------------------------------------------------------- /.husky/.gitignore: -------------------------------------------------------------------------------- 1 | _ 2 | -------------------------------------------------------------------------------- /.husky/commit-msg: -------------------------------------------------------------------------------- 1 | npx --no-install commitlint --edit "$1" 2 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | npm run lint 2 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "all", 4 | "endOfLine": "auto" 5 | } 6 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## [1.8.8](https://github.com/EddieHubCommunity/EddieBot/compare/v1.8.7...v1.8.8) (2024-09-26) 2 | 3 | 4 | ### Bug Fixes 5 | 6 | * github.blog to allowed links & cleanup testing errors ([#823](https://github.com/EddieHubCommunity/EddieBot/issues/823)) ([055acb3](https://github.com/EddieHubCommunity/EddieBot/commit/055acb3913a5cc75a6774c17e7b6929d1a5cafa8)) 7 | 8 | 9 | 10 | ## [1.8.7](https://github.com/EddieHubCommunity/EddieBot/compare/v1.8.6...v1.8.7) (2024-09-24) 11 | 12 | 13 | ### Bug Fixes 14 | 15 | * dependencies ([#822](https://github.com/EddieHubCommunity/EddieBot/issues/822)) ([2e4af50](https://github.com/EddieHubCommunity/EddieBot/commit/2e4af506171a51b6119388a26dd025220965ff23)) 16 | 17 | 18 | 19 | ## [1.8.6](https://github.com/EddieHubCommunity/EddieBot/compare/v1.8.5...v1.8.6) (2024-08-21) 20 | 21 | 22 | ### Bug Fixes 23 | 24 | * update ESLint to v9 ([#819](https://github.com/EddieHubCommunity/EddieBot/issues/819)) ([560e396](https://github.com/EddieHubCommunity/EddieBot/commit/560e396d4759acdd1b2742aab8e5b1f5ce2937b7)) 25 | 26 | 27 | 28 | ## [1.8.5](https://github.com/EddieHubCommunity/EddieBot/compare/v1.8.4...v1.8.5) (2024-08-20) 29 | 30 | 31 | ### Bug Fixes 32 | 33 | * update dependencies ([#818](https://github.com/EddieHubCommunity/EddieBot/issues/818)) ([956bb45](https://github.com/EddieHubCommunity/EddieBot/commit/956bb45c9968d77418d7e8b0533c37ec644da49e)) 34 | 35 | 36 | 37 | ## [1.8.4](https://github.com/EddieHubCommunity/EddieBot/compare/v1.8.3...v1.8.4) (2024-08-16) 38 | 39 | 40 | ### Bug Fixes 41 | 42 | * update dictionaries for typo ([#817](https://github.com/EddieHubCommunity/EddieBot/issues/817)) ([7ab2477](https://github.com/EddieHubCommunity/EddieBot/commit/7ab2477c1945a0ac946d958a27f189a5021f7ede)) 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our 6 | community a harassment-free experience for everyone, regardless of age, body 7 | size, visible or invisible disability, ethnicity, sex characteristics, gender 8 | identity and expression, level of experience, education, socio-economic status, 9 | nationality, personal appearance, race, caste, color, religion, or sexual 10 | identity and orientation. 11 | 12 | We pledge to act and interact in ways that contribute to an open, welcoming, 13 | diverse, inclusive, and healthy community. 14 | 15 | ## Our Standards 16 | 17 | Examples of behavior that contributes to a positive environment for our 18 | community include: 19 | 20 | - Demonstrating empathy and kindness toward other people 21 | - Being respectful of differing opinions, viewpoints, and experiences 22 | - Giving and gracefully accepting constructive feedback 23 | - Accepting responsibility and apologizing to those affected by our mistakes, 24 | and learning from the experience 25 | - Focusing on what is best not just for us as individuals, but for the overall 26 | community 27 | 28 | Examples of unacceptable behavior include: 29 | 30 | - The use of sexualized language or imagery, and sexual attention or advances of 31 | any kind 32 | - Trolling, insulting or derogatory comments, and personal or political attacks 33 | - Public or private harassment 34 | - Publishing others' private information, such as a physical or email address, 35 | without their explicit permission 36 | - Other conduct which could reasonably be considered inappropriate in a 37 | professional setting 38 | 39 | ## Enforcement Responsibilities 40 | 41 | Project Maintainers are responsible for clarifying and enforcing our standards of 42 | acceptable behavior and will take appropriate and fair corrective action in 43 | response to any behavior that they deem inappropriate, threatening, offensive, 44 | or harmful. 45 | 46 | Project Maintainers have the right and responsibility to remove, edit, or reject 47 | comments, commits, code, wiki edits, issues, and other contributions that are 48 | not aligned to this Code of Conduct, and will communicate reasons for moderation 49 | decisions when appropriate. 50 | 51 | ## Scope 52 | 53 | This Code of Conduct applies within all community spaces, and also applies when 54 | an individual is officially representing the community in public spaces. 55 | Examples of representing our community include using an official e-mail address, 56 | posting via an official social media account, or acting as an appointed 57 | representative at an online or offline event. 58 | 59 | ## Enforcement 60 | 61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 62 | reported (this can be done anonymously) to the Project Maintainers responsible for enforcement at http://eddiejaoude.io/contact. 63 | All complaints will be reviewed and investigated promptly and fairly. 64 | 65 | All Project Maintainers are obligated to respect the privacy and security of the 66 | reporter of any incident. 67 | 68 | ## Enforcement Guidelines 69 | 70 | Project Maintainers will follow these Guidelines in determining 71 | the consequences for any action they deem in violation of this Code of Conduct: 72 | 73 | ### 1. Correction 74 | 75 | **Community Impact**: Use of inappropriate language or other behavior deemed 76 | unprofessional or unwelcome in the community. 77 | 78 | **Consequence**: A private, written warning from Project Maintainers, providing 79 | clarity around the nature of the violation and an explanation of why the 80 | behavior was inappropriate. A public apology may be requested. 81 | 82 | ### 2. Warning 83 | 84 | **Community Impact**: A violation through a single incident or series of 85 | actions. 86 | 87 | **Consequence**: A warning with consequences for continued behavior. No 88 | interaction with the people involved, including unsolicited interaction with 89 | those enforcing the Code of Conduct, for a specified period of time. This 90 | includes avoiding interactions in community spaces as well as external channels 91 | like social media. Violating these terms may lead to a temporary or permanent 92 | ban. 93 | 94 | ### 3. Temporary Ban 95 | 96 | **Community Impact**: A serious violation of community standards, including 97 | sustained inappropriate behavior. 98 | 99 | **Consequence**: A temporary ban from any sort of interaction or public 100 | communication with the community for a specified period of time. No public or 101 | private interaction with the people involved, including unsolicited interaction 102 | with those enforcing the Code of Conduct, is allowed during this period. 103 | Violating these terms may lead to a permanent ban. 104 | 105 | ### 4. Permanent Ban 106 | 107 | **Community Impact**: Demonstrating a pattern of violation of community 108 | standards, including sustained inappropriate behavior, harassment of an 109 | individual, or aggression toward or disparagement of classes of individuals. 110 | 111 | **Consequence**: A permanent ban from any sort of public interaction within the 112 | community. 113 | 114 | ## Attribution 115 | 116 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 117 | version 2.1, available at 118 | [https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1]. 119 | 120 | Community Impact Guidelines were inspired by 121 | [Mozilla's code of conduct enforcement ladder][mozilla coc]. 122 | 123 | For answers to common questions about this code of conduct, see the FAQ at 124 | [https://www.contributor-covenant.org/faq][faq]. Translations are available at 125 | [https://www.contributor-covenant.org/translations][translations]. 126 | 127 | [homepage]: https://www.contributor-covenant.org 128 | [v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html 129 | [mozilla coc]: https://github.com/mozilla/diversity 130 | [faq]: https://www.contributor-covenant.org/faq 131 | [translations]: https://www.contributor-covenant.org/translations 132 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to EddieBot 2 | 3 | Thank you for taking the time to contribute. Please read the [CODE of CONDUCT](CODE_OF_CONDUCT.md). 4 | As a contributor, here are the guidelines we would like you to follow: 5 | 6 | - [Commit Message Guidelines](#commit) 7 | 8 | --- 9 | 10 | ## Commit Message Guidelines 😎 11 | 12 | To make git commit messages **easier to read** and faster to reason about, we follow some guidelines on most commits to keep the **format predictable**. Check [Conventional Commits specification](https://conventionalcommits.org) for more information about our guidelines. 13 | 14 | **Examples**: 15 | 16 | ``` 17 | docs(changelog): update changelog to beta.5 18 | docs: add API documentation to the bot 19 | test(server): add cache tests to the movie resource 20 | fix(web): add validation to the phone input field 21 | fix(web): remove avatar image from being required in the form 22 | fix(release): need to depend on latest rxjs and zone.js 23 | ``` 24 | 25 | ### Type 26 | 27 | Must be one of the following: 28 | 29 | - **build**: Changes that affect the build system or external dependencies (example scopes: gulp, broccoli, npm). 30 | - **ci**: Changes to our CI configuration files and scripts (example scopes: Circle, BrowserStack, SauceLabs) 31 | - **docs**: Documentation only changes 32 | - **feat**: A new feature 33 | - **fix**: A bug fix 34 | - **perf**: A code change that improves performance 35 | - **refactor**: A code change that neither fixes a bug nor adds a feature 36 | - **style**: Changes that do not affect the meaning of the code (white-space, formatting, missing semi-colons, etc) 37 | - **test**: Adding missing tests or correcting existing tests 38 | 39 | ### Scope 40 | 41 | The following is the list of supported scopes: 42 | 43 | - **bot** 44 | - **deploy** 45 | - **reactions** 46 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:20 As development 2 | LABEL org.opencontainers.image.source https://github.com/EddieHubCommunity/EddieBot 3 | 4 | WORKDIR /usr/src/app 5 | 6 | COPY package*.json ./ 7 | 8 | RUN npm install --only=development 9 | 10 | COPY . . 11 | 12 | RUN npm run build 13 | 14 | FROM node:20 as production 15 | 16 | ARG NODE_ENV=production 17 | ENV NODE_ENV=${NODE_ENV} 18 | ENV HUSKY=0 19 | ENV VERSION="v0.0.0" 20 | 21 | WORKDIR /usr/src/app 22 | 23 | COPY package*.json ./ 24 | 25 | RUN npm install --only=production 26 | 27 | COPY . . 28 | 29 | COPY --from=development /usr/src/app/prod ./prod 30 | 31 | CMD ["npm", "start"] 32 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020-2021 EddieHub 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # EddieBot 2 | 3 | The official **EddieBot** for the EddieHub [Discord server](http://discord.eddiehub.org). Join us at [Discord](http://discord.eddiehub.org) today! 4 | 5 | 6 | 7 | Open EddieBot in Gitpod 8 | 9 | 10 | ## Features 11 | 12 | - Checking peoples' messages for inclusive language. 13 | 14 | ![Eddie bit warning](https://user-images.githubusercontent.com/624760/200577618-af25764f-a9ce-4ce8-a1f2-f8808c682c77.png) 15 | 16 | ## Config / Secrets environment variables 17 | 18 | Copy `.env.example` to `.env` and add your private information 19 | 20 | *Note: never commit this file, it is ignored by Git* 21 | 22 | ``` 23 | # .env file 24 | 25 | # required: discord API token 26 | DISCORD_TOKEN= 27 | 28 | # required: mongo URL connection string 29 | EDDIEBOT_MONGO_CONNECTION_STRING= 30 | 31 | # optional 32 | DEBUG_HOOK= 33 | 34 | # required: discord server id 35 | HOME_GUILD="699608417039286293" 36 | 37 | # optional 38 | NODE_ENV="development" 39 | 40 | # required: channel id for logs 41 | ADMIN_CHANNEL= 42 | ``` 43 | 44 | ## Installation 45 | 46 | **1.** Start by making a fork of the repository. Click on the "Fork" symbol at the top right corner. 47 | 48 | **2.** Clone your new fork of the repository: 49 | 50 | ### SSH [Github Docs](https://docs.github.com/en/authentication/connecting-to-github-with-ssh) 51 | 52 | ```bash 53 | $ git clone git@github.com:EddieHubCommunity/EddieBot.git 54 | ``` 55 | 56 | *note: recommended* 57 | 58 | ### GitHub CLI 59 | 60 | ```bash 61 | $ gh repo clone EddieHubCommunity/EddieBot 62 | ``` 63 | 64 | ### HTTPS 65 | 66 | ```bash 67 | $ git clone https://github.com/EddieHubCommunity/EddieBot.git 68 | ``` 69 | 70 | **3.** Set upstream command: 71 | ```bash 72 | git remote add upstream https://github.com/EddieHubCommunity/EddieBot.git 73 | ``` 74 | 75 | **4.** Navigate to the new project directory: 76 | 77 | ```bash 78 | cd EddieBot 79 | ``` 80 | 81 | **5.** Create a new branch: 82 | ```bash 83 | git checkout -b 84 | ``` 85 | 86 | **6.** Sync your fork or a local repository with the origin repository: 87 | - In your forked repository click on "Fetch upstream" 88 | - Click "Fetch and merge". 89 | 90 | ### Alternatively, Git CLI way to Sync forked repository with origin repository: 91 | ```bash 92 | git fetch upstream 93 | ``` 94 | ```bash 95 | git merge upstream/main 96 | ``` 97 | 98 | **7.** Make your changes to the source code. 99 | 100 | **8.** Stage your changes and commit: 101 | 102 | ```bash 103 | git add 104 | ``` 105 | 106 | ```bash 107 | git commit -m "" 108 | ``` 109 | 110 | **9.** Push your local commits to the remote repository: 111 | 112 | ```bash 113 | git push origin 114 | ``` 115 | 116 | **10.** Create a [Pull Request](https://help.github.com/en/github/collaborating-with-issues-and-pull-requests/creating-a-pull-request)! 117 | 118 | **11.** **Congratulations!** You've made your first contribution to [**EddieBot**](https://github.com/EddieHubCommunity/EddieBot/graphs/contributors)! 🙌🏼 119 | 120 | 121 | ### Discord Docs 122 | 123 | - https://discord.com/developers/docs/intro#bots-and-apps 124 | 125 | ## Running the app 126 | 127 | ```bash 128 | $ npm ci 129 | 130 | # development 131 | $ npm run build 132 | $ npm start 133 | ``` 134 | 135 | ## Running the tests 136 | 137 | ```bash 138 | $ npm test 139 | ``` 140 | 141 | ## Support 142 | 143 | EddieBot is an MIT-licensed open source project. It can grow thanks to the contributors and the community members. If you'd like to join them, feel free to make a pull request and we'll review it. 144 | 145 | Stuck? Have any questions or comments? Join us on [Discord](http://discord.eddiehub.org/) and ask for help. 146 | 147 | ## License 148 | 149 | The EddieBot is licensed under the [MIT](https://github.com/EddieHubCommunity/EddieBot/blob/main/LICENSE) license. 150 | 151 | ## Thanks to all Contributors 💪 152 | 153 | Thanks a lot for spending your time helping EddieBot grow. Thanks a lot! Keep rocking 🍻 154 | 155 | [![Contributors](https://contrib.rocks/image?repo=EddieHubCommunity/EddieBot)](https://github.com/EddieHubCommunity/EddieBot/graphs/contributors) 156 | 157 | ## Our Pledge 158 | 159 | We take participation in our community as a harassment-free experience for everyone and we pledge to act in ways to contribute to an open, welcoming, diverse and inclusive community. 160 | 161 | If you have experienced or been made aware of unacceptable behaviour, please remember that you can report this. Read our [Code of Conduct](https://github.com/EddieHubCommunity/EddieBot/blob/main/CODE_OF_CONDUCT.md). 162 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | - Please do not create GitHub issues for security vulnerabilities 2 | - Instead, report them via http://eddiejaoude.io/contact 3 | -------------------------------------------------------------------------------- /captain-definition: -------------------------------------------------------------------------------- 1 | { 2 | "schemaVersion": 2, 3 | "imageName": "ghcr.io/eddiehubcommunity/eddiebot:latest" 4 | } 5 | -------------------------------------------------------------------------------- /commitlint.config.js: -------------------------------------------------------------------------------- 1 | export default { extends: ['@commitlint/config-conventional'] }; 2 | -------------------------------------------------------------------------------- /eslint.config.mjs: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | import globals from 'globals'; 4 | import eslint from '@eslint/js'; 5 | import tseslint from 'typescript-eslint'; 6 | import eslintPluginPrettierRecommended from 'eslint-plugin-prettier/recommended'; 7 | 8 | export default tseslint.config( 9 | { 10 | ignores: ['prod/**/*', 'jest.config.ts'], 11 | }, 12 | eslint.configs.recommended, 13 | ...tseslint.configs.recommended, 14 | { 15 | languageOptions: { 16 | globals: { 17 | ...globals.jest, 18 | ...globals.node, 19 | }, 20 | }, 21 | }, 22 | eslintPluginPrettierRecommended, 23 | ); 24 | -------------------------------------------------------------------------------- /jest.config.ts: -------------------------------------------------------------------------------- 1 | import type { JestConfigWithTsJest } from "ts-jest"; 2 | 3 | const config: JestConfigWithTsJest = { 4 | verbose: true, 5 | transform: { 6 | "^.+\\.ts?$": [ 7 | "ts-jest", 8 | { 9 | useESM: true, 10 | }, 11 | ], 12 | }, 13 | extensionsToTreatAsEsm: [".ts"], 14 | moduleNameMapper: { 15 | "^(\\.{1,2}/.*)\\.js$": "$1", 16 | }, 17 | }; 18 | 19 | export default config; 20 | -------------------------------------------------------------------------------- /kubernetes/deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: eddiebot-deployment 5 | labels: 6 | app: eddiebot 7 | spec: 8 | replicas: 1 9 | selector: 10 | matchLabels: 11 | app: eddiebot 12 | template: 13 | metadata: 14 | labels: 15 | app: eddiebot 16 | spec: 17 | volumes: 18 | - name: mongo-crt 19 | secret: 20 | secretName: mongo-crt 21 | containers: 22 | - name: eddiebot 23 | image: ghcr.io/eddiehubcommunity/eddiebot:latest 24 | imagePullPolicy: Always 25 | volumeMounts: 26 | - mountPath: '/etc/mongo/crt' 27 | name: mongo-crt 28 | readOnly: true 29 | env: 30 | - name: DISCORD_TOKEN 31 | valueFrom: 32 | secretKeyRef: 33 | name: discord-token 34 | key: DISCORD_TOKEN 35 | - name: EDDIEBOT_MONGO_CONNECTION_STRING 36 | valueFrom: 37 | secretKeyRef: 38 | name: eddiebot-mongo-connection-string 39 | key: EDDIEBOT_MONGO_CONNECTION_STRING 40 | - name: HOME_GUILD 41 | valueFrom: 42 | secretKeyRef: 43 | name: home-guild 44 | key: HOME_GUILD 45 | -------------------------------------------------------------------------------- /mongo-compose.yml: -------------------------------------------------------------------------------- 1 | # docker compose -f mongo-compose.yml up 2 | services: 3 | mongodb: 4 | image: mongo:6.0 5 | ports: 6 | - "27017:27017" -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "eddiehub", 3 | "version": "1.8.8", 4 | "description": "Discord bot built using NestJS", 5 | "author": "", 6 | "private": true, 7 | "license": "MIT", 8 | "type": "module", 9 | "repository": { 10 | "type": "git", 11 | "url": "git+https://github.com/EddieHubCommunity/EddieBot.git" 12 | }, 13 | "bugs": { 14 | "url": "https://github.com/EddieHubCommunity/EddieBot/issues" 15 | }, 16 | "homepage": "https://github.com/EddieHubCommunity/EddieBot#readme", 17 | "scripts": { 18 | "prebuild": "rimraf prod", 19 | "build": "tsc", 20 | "format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"", 21 | "start": "node -r dotenv/config prod/main.js", 22 | "lint": "eslint \"{src,apps,libs,test}/**/*.ts\"", 23 | "lint:fix": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix", 24 | "prepare": "if test \"$HUSKY\" != \"0\" ; then husky ; fi", 25 | "test": "node --experimental-vm-modules node_modules/.bin/jest" 26 | }, 27 | "engines": { 28 | "node": ">=20" 29 | }, 30 | "dependencies": { 31 | "@discordjs/rest": "^2.4.0", 32 | "alex": "^11.0.1", 33 | "discord.js": "^14.16.2", 34 | "dotenv": "^16.4.5", 35 | "express": "^4.21.0", 36 | "mongoose": "^8.6.3", 37 | "rimraf": "^6.0.1", 38 | "typo-js-ts": "^2.0.4", 39 | "winston": "^3.14.2" 40 | }, 41 | "devDependencies": { 42 | "@commitlint/cli": "^19.5.0", 43 | "@commitlint/config-conventional": "^19.5.0", 44 | "@eslint/js": "^9.9.0", 45 | "@jest/globals": "^29.7.0", 46 | "@total-typescript/tsconfig": "^1.0.4", 47 | "@types/express": "^4.17.21", 48 | "@types/jest": "^29.5.13", 49 | "@types/node": "^20.16.5", 50 | "eslint": "^9.11.0", 51 | "eslint-config-prettier": "^9.1.0", 52 | "eslint-plugin-prettier": "^5.2.1", 53 | "husky": "^9.1.6", 54 | "jest": "^29.7.0", 55 | "prettier": "^3.3.3", 56 | "ts-jest": "^29.2.5", 57 | "ts-node": "^10.9.2", 58 | "typescript": "^5.6.2", 59 | "typescript-eslint": "^8.6.0" 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/alexjs/__tests__/stripSpecialCharacters.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, test } from '@jest/globals'; 2 | import { stripSpecialCharacters } from '../stripSpecialCharacters.js'; 3 | describe('Strip special characters module tests', () => { 4 | test('Should remove special characters from words.', async () => { 5 | // given 6 | const expectedOutput = 'K ing'; 7 | const inputWord = 'K?ing'; 8 | 9 | // when 10 | const strippedOutput = stripSpecialCharacters(inputWord); 11 | 12 | // then 13 | expect(strippedOutput).toBe(expectedOutput); 14 | }); 15 | }); 16 | -------------------------------------------------------------------------------- /src/alexjs/checkBannedWords.ts: -------------------------------------------------------------------------------- 1 | import { EmbedBuilder } from 'discord.js'; 2 | 3 | import type { ExtendedClient } from '../interfaces/ExtendedClient.js'; 4 | import { errorHandler } from '../utils/errorHandler.js'; 5 | import { BannedWordsOptions } from '../config/BannedWordsOptions.js'; 6 | import { getBannedWordConfig } from '../utils/getBannedWordConfig.js'; 7 | import { urlPattern } from '../config/UrlRegex.js'; 8 | 9 | export const checkBannedWords = async ( 10 | bot: ExtendedClient, 11 | content: string, 12 | serverId: string, 13 | ): Promise => { 14 | const embeds: EmbedBuilder[] = []; 15 | try { 16 | const text: string = content.replace(urlPattern, ''); 17 | const config = await getBannedWordConfig(bot, serverId); 18 | const checkWords = config?.bannedWordConfig 19 | ? config.bannedWordConfig 20 | : BannedWordsOptions; 21 | 22 | text.split(' ').forEach((word) => { 23 | if (checkWords.includes(word.toLowerCase())) { 24 | const embed = new EmbedBuilder(); 25 | embed.setTitle(`You used the word "${word}"`); 26 | embed.setDescription( 27 | 'This might not be inclusive or welcoming language', 28 | ); 29 | embeds.push(embed); 30 | } 31 | }); 32 | 33 | return embeds; 34 | } catch (error) { 35 | await errorHandler(bot, error, 'alexjs check content'); 36 | return []; 37 | } 38 | }; 39 | -------------------------------------------------------------------------------- /src/alexjs/checkContent.ts: -------------------------------------------------------------------------------- 1 | import { markdown } from 'alex'; 2 | import { EmbedBuilder } from 'discord.js'; 3 | 4 | import type { ExtendedClient } from '../interfaces/ExtendedClient.js'; 5 | import { errorHandler } from '../utils/errorHandler.js'; 6 | import { getAlexConfig } from '../utils/getAlexConfig.js'; 7 | import { AlexJsOptions } from '../config/AlexJsOptions.js'; 8 | 9 | export const checkContent = async ( 10 | bot: ExtendedClient, 11 | content: string, 12 | serverId: string, 13 | ): Promise => { 14 | try { 15 | const config = await getAlexConfig(bot, serverId); 16 | const rawResult = markdown(content, { 17 | ...AlexJsOptions.alexWhitelist, 18 | ...config?.alexConfig, 19 | }).messages; 20 | const embeds: EmbedBuilder[] = []; 21 | 22 | for (const message of rawResult) { 23 | const embed = new EmbedBuilder(); 24 | embed.setTitle(`You used the word "${message.actual}"`); 25 | embed.setDescription( 26 | 'This might not be inclusive or welcoming language. Please update / edit your message with the following suggestions instead:', 27 | ); 28 | if (message.reason) { 29 | embed.addFields({ 30 | name: message.reason, 31 | value: message.note || 'see above :)', 32 | }); 33 | } 34 | embeds.push(embed); 35 | } 36 | 37 | return embeds; 38 | } catch (error) { 39 | await errorHandler(bot, error, 'alexjs check content'); 40 | return []; 41 | } 42 | }; 43 | -------------------------------------------------------------------------------- /src/alexjs/stripSpecialCharacters.ts: -------------------------------------------------------------------------------- 1 | export const stripSpecialCharacters = (str: string): string => { 2 | // alexMatch special symbols and replace with ' ' 3 | str = str.replace(/[.,/\\#!$%?&*;:{}=\-_'"“”~()]/g, ' '); 4 | // alexMatch double whitespace with single space for cleaner string 5 | return str.replace(/\s{2,}/g, ' '); 6 | }; 7 | -------------------------------------------------------------------------------- /src/commands/help.ts: -------------------------------------------------------------------------------- 1 | import { EmbedBuilder, SlashCommandBuilder } from 'discord.js'; 2 | import type { Command } from '../interfaces/Command.js'; 3 | import { errorHandler } from '../utils/errorHandler.js'; 4 | 5 | export const help: Command = { 6 | data: new SlashCommandBuilder() 7 | .setName('help') 8 | .setDescription('Display information about our bot.'), 9 | run: async (bot, interaction) => { 10 | try { 11 | await interaction.deferReply(); 12 | 13 | const embed = new EmbedBuilder(); 14 | embed.setTitle('Introducing EddieBot!!!'); 15 | embed.setDescription( 16 | 'EddieBot is a bot designed to help you keep the language within your community safe and inclusive. We run on the AlexJS library with extensive options and custom modules to allow you to configure your servers allowed terminology.', 17 | ); 18 | embed.addFields([ 19 | { 20 | name: 'How do I configure my server?', 21 | value: 22 | 'At this time, our bot is still in beta and we are working on the configuration feature.', 23 | }, 24 | { 25 | name: 'How do I help with new features?', 26 | value: 27 | "We are completely open source - you can check out [the bot's repository](https://github.com/EddieHubCommunity/EddieBot) to see what issues are available for contribution.", 28 | }, 29 | ]); 30 | 31 | await interaction.editReply({ embeds: [embed] }); 32 | } catch (err) { 33 | await errorHandler(bot, err, 'help command'); 34 | } 35 | }, 36 | }; 37 | -------------------------------------------------------------------------------- /src/commands/stats.ts: -------------------------------------------------------------------------------- 1 | import { EmbedBuilder, SlashCommandBuilder } from 'discord.js'; 2 | import type { Command } from '../interfaces/Command.js'; 3 | import { errorHandler } from '../utils/errorHandler.js'; 4 | import Statistics from '../database/models/Statistics.js'; 5 | 6 | export const stats: Command = { 7 | data: new SlashCommandBuilder() 8 | .setName('stats') 9 | .setDescription('Display stats info about our bot.'), 10 | run: async (bot, interaction) => { 11 | try { 12 | await interaction.deferReply(); 13 | // TODO: We will need to set up pagination at some point or this will be too much to fit in one embed. 14 | const stats = await Statistics.find({}); 15 | 16 | const embed = new EmbedBuilder(); 17 | embed.setTitle('Statistics about EddieBot'); 18 | 19 | embed.setDescription(`Total servers: ${stats.length}`); 20 | 21 | embed.addFields( 22 | stats.map((stat) => ({ 23 | name: stat.serverId, 24 | value: `Total triggers: ${stat.totalTriggers} and total triggers fixed: ${stat.totalTriggersFixed}`, 25 | })), 26 | ); 27 | 28 | await interaction.editReply({ embeds: [embed] }); 29 | } catch (err) { 30 | await errorHandler(bot, err, 'version command'); 31 | } 32 | }, 33 | }; 34 | -------------------------------------------------------------------------------- /src/commands/version.ts: -------------------------------------------------------------------------------- 1 | import { EmbedBuilder, SlashCommandBuilder } from 'discord.js'; 2 | import type { Command } from '../interfaces/Command.js'; 3 | import { errorHandler } from '../utils/errorHandler.js'; 4 | 5 | export const version: Command = { 6 | data: new SlashCommandBuilder() 7 | .setName('version') 8 | .setDescription('Display version info about our bot.'), 9 | run: async (bot, interaction) => { 10 | try { 11 | await interaction.deferReply(); 12 | 13 | const embed = new EmbedBuilder(); 14 | embed.setTitle('Introducing EddieBot!!!'); 15 | embed.setDescription(`Currently running version ${process.env.VERSION}`); 16 | 17 | await interaction.editReply({ embeds: [embed] }); 18 | } catch (err) { 19 | await errorHandler(bot, err, 'version command'); 20 | } 21 | }, 22 | }; 23 | -------------------------------------------------------------------------------- /src/config/AlexJsOptions.ts: -------------------------------------------------------------------------------- 1 | import type { Options } from 'alex'; 2 | 3 | export const AlexJsOptions: { 4 | allowedWords: string[]; 5 | alexWhitelist: Options; 6 | } = { 7 | // words we allow even if AlexJS blocks (words are sometimes grouped by we want to be more granular) 8 | allowedWords: ['fellow'], 9 | alexWhitelist: { 10 | profanitySureness: 1, 11 | noBinary: true, 12 | // AlexJS to ignore these grouped words https://github.com/retextjs/retext-equality/blob/main/rules.md 13 | allow: [ 14 | 'add', 15 | 'basically', 16 | 'clearly', 17 | 'dad-mom', 18 | 'daft', 19 | 'fellow', 20 | 'fellowship', 21 | 'gimp', 22 | 'hero-heroine', 23 | 'host-hostess', 24 | 'hostesses-hosts', 25 | 'husband-wife', 26 | 'jesus', 27 | 'king', 28 | 'kushi', 29 | 'latino', 30 | 'long-time-no-see', 31 | 'master', 32 | 'moan', 33 | 'moaning', 34 | 'obvious', 35 | 'of-course', 36 | 'postman-postwoman', 37 | 'special', 38 | 'superman-superwoman', 39 | 'simple', 40 | 'just', 41 | 'nephew-niece', 42 | 'nephews-nieces', 43 | ], 44 | }, 45 | }; 46 | -------------------------------------------------------------------------------- /src/config/BannedWordsOptions.ts: -------------------------------------------------------------------------------- 1 | // words we block in addition to AlexJS 2 | // please check words are not already covered by AlexJS and the libraries it uses first 3 | // http://alexjs.com 4 | export const BannedWordsOptions = [ 5 | 'broo', 6 | 'bros', 7 | 'gys', 8 | 'guyz', 9 | 'guyzz', 10 | 'guyss', 11 | 'boys', 12 | 'bruh', 13 | 'duude', 14 | 'women', 15 | 'sir', 16 | 'sirr', 17 | 'bruhh', 18 | 'man', 19 | 'maan', 20 | 'mann', 21 | 'fella', 22 | 'fellas', 23 | 'gang', 24 | 'madam', 25 | 'maam', 26 | "ma'am", 27 | 'yessir', 28 | 'simp', 29 | 'simps', 30 | 'simping', 31 | 'mate', 32 | 'matey', 33 | 'mateys', 34 | 'sis', 35 | 'dork', 36 | 'bhaiya', 37 | 'bhai', 38 | 'dudez', 39 | ]; 40 | -------------------------------------------------------------------------------- /src/config/DictionaryOptions.ts: -------------------------------------------------------------------------------- 1 | import { Typo } from 'typo-js-ts'; 2 | 3 | export const dict = new Typo('en_US', undefined, undefined, { 4 | dictionaryPath: 'src/typo/dict', 5 | flags: {}, 6 | }); 7 | -------------------------------------------------------------------------------- /src/config/IntentOptions.ts: -------------------------------------------------------------------------------- 1 | import { GatewayIntentBits } from 'discord.js'; 2 | 3 | export const IntentOptions = [ 4 | GatewayIntentBits.Guilds, 5 | GatewayIntentBits.GuildMessages, 6 | GatewayIntentBits.MessageContent, 7 | ]; 8 | -------------------------------------------------------------------------------- /src/config/UrlRegex.ts: -------------------------------------------------------------------------------- 1 | export const urlPattern = 2 | /[Hh][Tt][Tt][Pp][Ss]?:\/\/([\w-]+(\.[\w-]+)+)(\/[\w-./?%&=]*)?/g; 3 | -------------------------------------------------------------------------------- /src/database/connectDb.ts: -------------------------------------------------------------------------------- 1 | import { connect } from 'mongoose'; 2 | import type { ExtendedClient } from '../interfaces/ExtendedClient.js'; 3 | import { errorHandler } from '../utils/errorHandler.js'; 4 | 5 | export const connectDb = async (bot: ExtendedClient) => { 6 | // DigitalOcean Apps has cert as environment variable but Mongo needs a file path 7 | // Write Mongo cert file to disk 8 | // fs.writeFileSync('cert.pem', process.env.CA_CERT!); no longer needed should remove 9 | 10 | try { 11 | await connect(bot.config.dbUri); 12 | } catch (err) { 13 | await errorHandler(bot, err, 'database connection'); 14 | } 15 | }; 16 | -------------------------------------------------------------------------------- /src/database/models/ServerConfig.ts: -------------------------------------------------------------------------------- 1 | import { Document, model, Schema } from 'mongoose'; 2 | 3 | export interface ServerConfig extends Document { 4 | serverId: string; 5 | alexConfig?: { 6 | allow: string[]; 7 | profanitySureness: 0 | 1 | 2; 8 | noBinary: boolean; 9 | }; 10 | bannedWordConfig?: []; 11 | } 12 | 13 | const ServerConfigSchema = new Schema({ 14 | serverId: { 15 | type: String, 16 | required: true, 17 | unique: true, 18 | }, 19 | alexConfig: { 20 | type: { 21 | allow: Array, 22 | profanitySureness: Number, 23 | noBinary: Boolean, 24 | }, 25 | }, 26 | bannedWordConfig: { 27 | type: Array, 28 | }, 29 | }); 30 | 31 | export default model('ServerConfig', ServerConfigSchema); 32 | -------------------------------------------------------------------------------- /src/database/models/Statistics.ts: -------------------------------------------------------------------------------- 1 | import { Document, model, Schema } from 'mongoose'; 2 | 3 | export interface Statistics extends Document { 4 | serverId: string; 5 | totalTriggers: number; 6 | totalTriggersFixed: number; 7 | totalWordTriggers: { 8 | [key: string]: number; 9 | }; 10 | } 11 | 12 | const Statistics = new Schema({ 13 | serverId: { 14 | type: String, 15 | required: true, 16 | unique: true, 17 | }, 18 | totalTriggers: { 19 | type: Number, 20 | required: false, 21 | unique: false, 22 | default: 0, 23 | }, 24 | totalTriggersFixed: { 25 | type: Number, 26 | required: false, 27 | unique: false, 28 | default: 0, 29 | }, 30 | }); 31 | 32 | export default model('Statistics', Statistics); 33 | -------------------------------------------------------------------------------- /src/database/models/Warnings.ts: -------------------------------------------------------------------------------- 1 | import { Document, model, Schema } from 'mongoose'; 2 | 3 | export interface Warnings extends Document { 4 | serverId: string; 5 | messageId: string; 6 | channelId: string; 7 | warningId: string; 8 | } 9 | 10 | const WarningsSchema = new Schema({ 11 | serverId: { 12 | type: String, 13 | required: true, 14 | unique: false, 15 | }, 16 | messageId: { 17 | type: String, 18 | required: true, 19 | unique: false, 20 | }, 21 | channelId: { 22 | type: String, 23 | required: true, 24 | unique: false, 25 | }, 26 | warningId: { 27 | type: String, 28 | required: true, 29 | unique: true, 30 | }, 31 | }); 32 | 33 | export default model('Warnings', WarningsSchema); 34 | -------------------------------------------------------------------------------- /src/events/_handleEvents.ts: -------------------------------------------------------------------------------- 1 | import type { ExtendedClient } from '../interfaces/ExtendedClient.js'; 2 | import { onInteraction } from './onInteraction.js'; 3 | import { onMessage } from './onMessage.js'; 4 | import { onUpdate } from './onUpdate.js'; 5 | import { onDelete } from './onDelete.js'; 6 | 7 | export const handleEvents = (bot: ExtendedClient) => { 8 | bot.on('ready', () => { 9 | console.log('Connected to Discord!'); 10 | }); 11 | 12 | bot.on('messageCreate', async (message) => { 13 | await onMessage(bot, message); 14 | }); 15 | 16 | bot.on('messageUpdate', async (oldMessage, newMessage) => { 17 | await onUpdate(bot, oldMessage, newMessage); 18 | }); 19 | 20 | bot.on('messageDelete', async (message) => { 21 | await onDelete(bot, message); 22 | }); 23 | 24 | bot.on('interactionCreate', async (interaction) => { 25 | await onInteraction(bot, interaction); 26 | }); 27 | }; 28 | -------------------------------------------------------------------------------- /src/events/onDelete.ts: -------------------------------------------------------------------------------- 1 | import type { Message, PartialMessage } from 'discord.js'; 2 | import type { ExtendedClient } from '../interfaces/ExtendedClient.js'; 3 | import { errorHandler } from '../utils/errorHandler.js'; 4 | import Warnings from '../database/models/Warnings.js'; 5 | import Statistics from '../database/models/Statistics.js'; 6 | 7 | export const onDelete = async ( 8 | bot: ExtendedClient, 9 | message: Message | PartialMessage, 10 | ) => { 11 | try { 12 | const savedWarning = await Warnings.findOne({ 13 | serverId: message.guildId, 14 | messageId: message.id, 15 | channelId: message.channelId, 16 | }); 17 | 18 | if (savedWarning) { 19 | const notificationMessage = await message.channel.messages.fetch( 20 | savedWarning.warningId, 21 | ); 22 | 23 | if (notificationMessage) { 24 | await notificationMessage.delete(); 25 | } 26 | await savedWarning.deleteOne(); 27 | 28 | await Statistics.findOneAndUpdate( 29 | { 30 | serverId: message.guildId, 31 | }, 32 | { $inc: { totalTriggersFixed: 1 } }, 33 | { upsert: true }, 34 | ).exec(); 35 | return; 36 | } 37 | 38 | return; 39 | } catch (error) { 40 | await errorHandler(bot, error, 'on message'); 41 | } 42 | }; 43 | -------------------------------------------------------------------------------- /src/events/onInteraction.ts: -------------------------------------------------------------------------------- 1 | import type { Interaction } from 'discord.js'; 2 | import type { ExtendedClient } from '../interfaces/ExtendedClient.js'; 3 | import { errorHandler } from '../utils/errorHandler.js'; 4 | 5 | export const onInteraction = async ( 6 | bot: ExtendedClient, 7 | interaction: Interaction, 8 | ) => { 9 | try { 10 | if (interaction.isChatInputCommand()) { 11 | for (const command of bot.commands) { 12 | if (command.data.name === interaction.commandName) { 13 | await command.run(bot, interaction); 14 | break; 15 | } 16 | } 17 | } 18 | } catch (err) { 19 | await errorHandler(bot, err, 'on interaction'); 20 | } 21 | }; 22 | -------------------------------------------------------------------------------- /src/events/onMessage.ts: -------------------------------------------------------------------------------- 1 | import { EmbedBuilder, Message, TextChannel } from 'discord.js'; 2 | import { checkContent } from '../alexjs/checkContent.js'; 3 | import { checkBannedWords } from '../alexjs/checkBannedWords.js'; 4 | import { stripSpecialCharacters } from '../alexjs/stripSpecialCharacters.js'; 5 | import type { ExtendedClient } from '../interfaces/ExtendedClient.js'; 6 | import { errorHandler } from '../utils/errorHandler.js'; 7 | import Warnings from '../database/models/Warnings.js'; 8 | import Statistics from '../database/models/Statistics.js'; 9 | import { sentenceTypoFixer } from '../utils/typoFixer.js'; 10 | import { checkLinks } from '../links/checkLinks.js'; 11 | 12 | export const onMessage = async (bot: ExtendedClient, message: Message) => { 13 | try { 14 | if (message.author.bot || !message.content || !message.guild) { 15 | return; 16 | } 17 | 18 | const linkMessage = await checkLinks(bot, message); 19 | if (linkMessage) { 20 | const adminChannel = bot.channels.cache.get( 21 | process.env.ADMIN_CHANNEL!, 22 | ) as TextChannel; 23 | if (adminChannel) { 24 | await adminChannel.send({ 25 | embeds: [linkMessage], 26 | }); 27 | } 28 | return; // Return as message is deleted 29 | } 30 | 31 | const cleaned = await sentenceTypoFixer( 32 | stripSpecialCharacters(message.content), 33 | ); 34 | 35 | const triggeredWarnings: EmbedBuilder[] = []; 36 | triggeredWarnings.push( 37 | ...(await checkContent(bot, cleaned, message.guild.id)), 38 | ); 39 | triggeredWarnings.push( 40 | ...(await checkBannedWords(bot, cleaned, message.guild.id)), 41 | ); 42 | 43 | triggeredWarnings.map((warning) => 44 | warning.setColor('#ff0000').addFields([ 45 | { 46 | name: 'TIP: ', 47 | value: 'Edit your message as suggested to make this warning go away', 48 | }, 49 | { 50 | name: 'Open Source Improvements: ', 51 | value: 52 | 'EddieBot is Open Source, you can find it here https://github.com/EddieHubCommunity/EddieBot', 53 | }, 54 | ]), 55 | ); 56 | 57 | if (!triggeredWarnings.length) { 58 | return; 59 | } 60 | 61 | const channel = message.channel as TextChannel; 62 | const sent = await channel.send({ 63 | embeds: triggeredWarnings.slice(0, 1), 64 | }); 65 | await Warnings.create({ 66 | serverId: message.guild.id, 67 | messageId: message.id, 68 | channelId: message.channel.id, 69 | warningId: sent.id, 70 | }); 71 | 72 | await Statistics.findOneAndUpdate( 73 | { 74 | serverId: message.guild.id, 75 | }, 76 | { $inc: { totalTriggers: 1 } }, 77 | { upsert: true }, 78 | ).exec(); 79 | } catch (error) { 80 | await errorHandler(bot, error, 'on message'); 81 | } 82 | }; 83 | -------------------------------------------------------------------------------- /src/events/onUpdate.ts: -------------------------------------------------------------------------------- 1 | import { 2 | EmbedBuilder, 3 | type APIEmbed, 4 | type Message, 5 | type PartialMessage, 6 | type TextChannel, 7 | } from 'discord.js'; 8 | import { checkContent } from '../alexjs/checkContent.js'; 9 | import { checkBannedWords } from '../alexjs/checkBannedWords.js'; 10 | import { stripSpecialCharacters } from '../alexjs/stripSpecialCharacters.js'; 11 | import type { ExtendedClient } from '../interfaces/ExtendedClient.js'; 12 | import { errorHandler } from '../utils/errorHandler.js'; 13 | import Warnings from '../database/models/Warnings.js'; 14 | import Statistics from '../database/models/Statistics.js'; 15 | import { sentenceTypoFixer } from '../utils/typoFixer.js'; 16 | import { checkLinks } from '../links/checkLinks.js'; 17 | 18 | export const onUpdate = async ( 19 | bot: ExtendedClient, 20 | oldMessage: Message | PartialMessage, 21 | newMessage: Message | PartialMessage, 22 | ) => { 23 | if (newMessage.partial) { 24 | try { 25 | newMessage = await newMessage.fetch(); 26 | } catch (error) { 27 | return await errorHandler(bot, error, 'fetching partial message'); 28 | } 29 | } 30 | 31 | if (newMessage.author.bot || !newMessage.content || !newMessage.guild) { 32 | return; 33 | } 34 | 35 | const linkMessage = await checkLinks(bot, newMessage); 36 | if (linkMessage) { 37 | const adminChannel = bot.channels.cache.get( 38 | process.env.ADMIN_CHANNEL!, 39 | ) as TextChannel; 40 | if (adminChannel && adminChannel.isTextBased()) { 41 | await adminChannel.send({ 42 | embeds: [linkMessage], 43 | }); 44 | } 45 | return; // Return as message is deleted 46 | } 47 | 48 | // log to admin channel updates 49 | const oldContent = oldMessage.content; 50 | const newContent = newMessage.content; 51 | 52 | if (oldContent !== newContent) { 53 | const logChannel = bot.channels.cache.get( 54 | process.env.ADMIN_CHANNEL!, 55 | ) as TextChannel; 56 | if (logChannel && logChannel.isTextBased()) { 57 | const logEmbed = new EmbedBuilder() 58 | .setTitle(`Message Updated by "${newMessage.author?.username}"`) 59 | .setDescription(`Message updated in ${newMessage.channel} channel`) 60 | .addFields( 61 | { 62 | name: 'Old Message', 63 | value: oldContent ?? 'No old message available', 64 | }, 65 | { 66 | name: 'New Message', 67 | value: newContent ?? 'No new message available', 68 | }, 69 | { name: 'Author', value: newMessage.author.toString() }, 70 | { name: 'Channel', value: newMessage.channel.toString() }, 71 | ) 72 | .setTimestamp(); 73 | await logChannel.send({ embeds: [logEmbed] }); 74 | } 75 | } 76 | 77 | try { 78 | const triggeredWarnings: EmbedBuilder[] = []; 79 | const cleaned = await sentenceTypoFixer( 80 | stripSpecialCharacters(newMessage.content), 81 | ); 82 | triggeredWarnings.push( 83 | ...(await checkContent(bot, cleaned, newMessage.guild.id)), 84 | ); 85 | triggeredWarnings.push( 86 | ...(await checkBannedWords(bot, cleaned, newMessage.guild.id)), 87 | ); 88 | 89 | triggeredWarnings.map((warning) => 90 | warning.setColor('#ff0000').addFields([ 91 | { 92 | name: 'TIP: ', 93 | value: 'Edit your message as suggested to make this warning go away', 94 | }, 95 | { 96 | name: 'Open Source Improvements: ', 97 | value: 98 | 'EddieBot is Open Source, you can find it here https://github.com/EddieHubCommunity/EddieBot', 99 | }, 100 | ]), 101 | ); 102 | 103 | const savedWarning = await Warnings.findOne({ 104 | serverId: newMessage.guild.id, 105 | messageId: newMessage.id, 106 | channelId: newMessage.channel.id, 107 | }); 108 | 109 | // when edit results in new warning, but no existing warning 110 | if (!savedWarning && triggeredWarnings.length) { 111 | const channel = newMessage.channel as TextChannel; 112 | const sent = await channel.send({ 113 | embeds: triggeredWarnings.slice(0, 1), 114 | }); 115 | await Warnings.create({ 116 | serverId: newMessage.guild.id, 117 | messageId: newMessage.id, 118 | channelId: newMessage.channel.id, 119 | warningId: sent.id, 120 | }); 121 | 122 | await Statistics.findOneAndUpdate( 123 | { 124 | serverId: newMessage.guild.id, 125 | }, 126 | { $inc: { totalTriggers: 1 } }, 127 | { upsert: true }, 128 | ).exec(); 129 | } 130 | 131 | // when edit results in no new warning, but has existing warning, so fixed 132 | if (savedWarning && !triggeredWarnings.length) { 133 | const notificationMessage = await newMessage.channel.messages.fetch( 134 | savedWarning.warningId, 135 | ); 136 | if (notificationMessage) { 137 | await notificationMessage.delete(); 138 | } 139 | await savedWarning.deleteOne(); 140 | 141 | await Statistics.findOneAndUpdate( 142 | { 143 | serverId: newMessage.guild.id, 144 | }, 145 | { $inc: { totalTriggersFixed: 1 } }, 146 | { upsert: true }, 147 | ).exec(); 148 | return; 149 | } 150 | 151 | // when edit results in new warning AND has existing warning 152 | if (savedWarning && triggeredWarnings.length) { 153 | const notificationMessage = await newMessage.channel.messages.fetch( 154 | savedWarning.warningId, 155 | ); 156 | if (notificationMessage) { 157 | await notificationMessage.edit({ 158 | embeds: [triggeredWarnings[0] as APIEmbed], 159 | }); 160 | return; 161 | } 162 | 163 | await Statistics.findOneAndUpdate( 164 | { 165 | serverId: newMessage.guild.id, 166 | }, 167 | { $inc: { totalTriggers: 1 } }, 168 | { upsert: true }, 169 | ).exec(); 170 | } 171 | 172 | // when edit results in no new and no old 173 | return; 174 | } catch (error) { 175 | await errorHandler(bot, error, 'on message'); 176 | } 177 | }; 178 | -------------------------------------------------------------------------------- /src/interfaces/Command.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ChatInputCommandInteraction, 3 | SlashCommandBuilder, 4 | SlashCommandSubcommandBuilder, 5 | } from 'discord.js'; 6 | import type { ExtendedClient } from './ExtendedClient.js'; 7 | 8 | export interface Command { 9 | data: SlashCommandBuilder | SlashCommandSubcommandBuilder; 10 | run: ( 11 | bot: ExtendedClient, 12 | interaction: ChatInputCommandInteraction, 13 | ) => Promise; 14 | } 15 | -------------------------------------------------------------------------------- /src/interfaces/ExtendedClient.ts: -------------------------------------------------------------------------------- 1 | import { Client, WebhookClient } from 'discord.js'; 2 | import { Document } from 'mongoose'; 3 | import type { ServerConfig } from '../database/models/ServerConfig.js'; 4 | import type { Command } from './Command.js'; 5 | 6 | export interface ExtendedClient extends Client { 7 | commands: Command[]; 8 | cache: { 9 | [key: string]: Omit; 10 | }; 11 | config: { 12 | token: string; 13 | dbUri: string; 14 | debugHook: WebhookClient | undefined; 15 | homeGuild: string; 16 | }; 17 | } 18 | -------------------------------------------------------------------------------- /src/links/checkLinks.ts: -------------------------------------------------------------------------------- 1 | import { EmbedBuilder, Message, type PartialMessage } from 'discord.js'; 2 | 3 | import type { ExtendedClient } from '../interfaces/ExtendedClient.js'; 4 | import { errorHandler } from '../utils/errorHandler.js'; 5 | import { urlPattern } from '../config/UrlRegex.js'; 6 | 7 | const allowedLinks = [ 8 | 'github.com', 9 | 'eddiejaoude.io', 10 | 'gitlab.com', 11 | 'github.blog', 12 | ]; 13 | 14 | export const checkLinks = async ( 15 | bot: ExtendedClient, 16 | message: Message | PartialMessage, 17 | ): Promise => { 18 | const content = message.content; 19 | 20 | // ignore link check in message from users who have the "moderators" role 21 | if (message.member?.roles.cache.some((role) => role.name === 'moderators')) { 22 | return null; 23 | } 24 | 25 | try { 26 | const foundUrls = content?.match(urlPattern); 27 | if (!foundUrls) { 28 | return null; 29 | } 30 | 31 | if (foundUrls) { 32 | if ( 33 | foundUrls.every((found) => 34 | allowedLinks.some((allowed) => found.includes(allowed)), 35 | ) 36 | ) { 37 | return null; 38 | } 39 | } 40 | 41 | await message.delete(); 42 | 43 | const embed = new EmbedBuilder(); 44 | embed.setTitle( 45 | `The user "${message.author?.username}" message contains a link`, 46 | ); 47 | embed.setDescription(message.content); 48 | 49 | return embed; 50 | } catch (error) { 51 | await errorHandler(bot, error, 'link checking'); 52 | return null; 53 | } 54 | }; 55 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import { Client, Partials } from 'discord.js'; 2 | import { IntentOptions } from './config/IntentOptions.js'; 3 | import { connectDb } from './database/connectDb.js'; 4 | import { handleEvents } from './events/_handleEvents.js'; 5 | import type { ExtendedClient } from './interfaces/ExtendedClient.js'; 6 | import { errorHandler } from './utils/errorHandler.js'; 7 | import { loadCommands } from './utils/loadCommands.js'; 8 | import { registerCommands } from './utils/registerCommands.js'; 9 | import { validateEnv } from './utils/validateEnv.js'; 10 | 11 | import express from 'express'; 12 | const app = express(); 13 | const port = 8080; 14 | 15 | (async () => { 16 | const bot = new Client({ 17 | intents: IntentOptions, 18 | partials: [Partials.Message, Partials.Channel, Partials.Reaction], 19 | }) as ExtendedClient; 20 | validateEnv(bot); 21 | bot.cache = {}; 22 | bot.commands = await loadCommands(bot); 23 | 24 | handleEvents(bot); 25 | 26 | await connectDb(bot); 27 | 28 | await bot 29 | .login(process.env.DISCORD_TOKEN) 30 | .catch((err) => errorHandler(bot, err, 'login')); 31 | await registerCommands(bot); 32 | 33 | // used for DigitalOcean's app health check otherwise it kills the app 34 | app.get('/', (req, res: express.Response) => { 35 | res.send('Discord bot is running'); 36 | }); 37 | 38 | app.listen(port, () => { 39 | console.log('API is running'); 40 | }); 41 | })(); 42 | -------------------------------------------------------------------------------- /src/typo/dict/en_US/dictionaries.md: -------------------------------------------------------------------------------- 1 | ## Hunspell Dictionaries 2 | 3 | These dictionaries are downloaded from LibreOffice [repo](https://cgit.freedesktop.org/libreoffice/dictionaries/tree/en) -------------------------------------------------------------------------------- /src/typo/dict/en_US/en_US.aff: -------------------------------------------------------------------------------- 1 | # 2024-01-29 (Marco A.G.Pinto) 2 | # - Fix: apostrophe handling, by adding: WORDCHARS 0123456789’ to the .aff. 3 | # 4 | 5 | SET UTF-8 6 | TRY esianrtolcdugmphbyfvkwzESIANRTOLCDUGMPHBYFVKWZ' 7 | ICONV 1 8 | ICONV ’ ' 9 | NOSUGGEST ! 10 | 11 | # ordinal numbers 12 | COMPOUNDMIN 1 13 | # only in compounds: 1th, 2th, 3th 14 | ONLYINCOMPOUND c 15 | # compound rules: 16 | # 1. [0-9]*1[0-9]th (10th, 11th, 12th, 56714th, etc.) 17 | # 2. [0-9]*[02-9](1st|2nd|3rd|[4-9]th) (21st, 22nd, 123rd, 1234th, etc.) 18 | COMPOUNDRULE 2 19 | COMPOUNDRULE n*1t 20 | COMPOUNDRULE n*mp 21 | WORDCHARS 0123456789’ 22 | 23 | PFX A Y 1 24 | PFX A 0 re . 25 | 26 | PFX I Y 1 27 | PFX I 0 in . 28 | 29 | PFX U Y 1 30 | PFX U 0 un . 31 | 32 | PFX C Y 1 33 | PFX C 0 de . 34 | 35 | PFX E Y 1 36 | PFX E 0 dis . 37 | 38 | PFX F Y 1 39 | PFX F 0 con . 40 | 41 | PFX K Y 1 42 | PFX K 0 pro . 43 | 44 | SFX V N 2 45 | SFX V e ive e 46 | SFX V 0 ive [^e] 47 | 48 | SFX N Y 3 49 | SFX N e ion e 50 | SFX N y ication y 51 | SFX N 0 en [^ey] 52 | 53 | SFX X Y 3 54 | SFX X e ions e 55 | SFX X y ications y 56 | SFX X 0 ens [^ey] 57 | 58 | SFX H N 2 59 | SFX H y ieth y 60 | SFX H 0 th [^y] 61 | 62 | SFX Y Y 1 63 | SFX Y 0 ly . 64 | 65 | SFX G Y 2 66 | SFX G e ing e 67 | SFX G 0 ing [^e] 68 | 69 | SFX J Y 2 70 | SFX J e ings e 71 | SFX J 0 ings [^e] 72 | 73 | SFX D Y 4 74 | SFX D 0 d e 75 | SFX D y ied [^aeiou]y 76 | SFX D 0 ed [^ey] 77 | SFX D 0 ed [aeiou]y 78 | 79 | SFX T N 4 80 | SFX T 0 st e 81 | SFX T y iest [^aeiou]y 82 | SFX T 0 est [aeiou]y 83 | SFX T 0 est [^ey] 84 | 85 | SFX R Y 4 86 | SFX R 0 r e 87 | SFX R y ier [^aeiou]y 88 | SFX R 0 er [aeiou]y 89 | SFX R 0 er [^ey] 90 | 91 | SFX Z Y 4 92 | SFX Z 0 rs e 93 | SFX Z y iers [^aeiou]y 94 | SFX Z 0 ers [aeiou]y 95 | SFX Z 0 ers [^ey] 96 | 97 | SFX S Y 4 98 | SFX S y ies [^aeiou]y 99 | SFX S 0 s [aeiou]y 100 | SFX S 0 es [sxzh] 101 | SFX S 0 s [^sxzhy] 102 | 103 | SFX P Y 3 104 | SFX P y iness [^aeiou]y 105 | SFX P 0 ness [aeiou]y 106 | SFX P 0 ness [^y] 107 | 108 | SFX M Y 1 109 | SFX M 0 's . 110 | 111 | SFX B Y 3 112 | SFX B 0 able [^aeiou] 113 | SFX B 0 able ee 114 | SFX B e able [^aeiou]e 115 | 116 | SFX L Y 1 117 | SFX L 0 ment . 118 | 119 | REP 90 120 | REP a ei 121 | REP ei a 122 | REP a ey 123 | REP ey a 124 | REP ai ie 125 | REP ie ai 126 | REP alot a_lot 127 | REP are air 128 | REP are ear 129 | REP are eir 130 | REP air are 131 | REP air ere 132 | REP ere air 133 | REP ere ear 134 | REP ere eir 135 | REP ear are 136 | REP ear air 137 | REP ear ere 138 | REP eir are 139 | REP eir ere 140 | REP ch te 141 | REP te ch 142 | REP ch ti 143 | REP ti ch 144 | REP ch tu 145 | REP tu ch 146 | REP ch s 147 | REP s ch 148 | REP ch k 149 | REP k ch 150 | REP f ph 151 | REP ph f 152 | REP gh f 153 | REP f gh 154 | REP i igh 155 | REP igh i 156 | REP i uy 157 | REP uy i 158 | REP i ee 159 | REP ee i 160 | REP j di 161 | REP di j 162 | REP j gg 163 | REP gg j 164 | REP j ge 165 | REP ge j 166 | REP s ti 167 | REP ti s 168 | REP s ci 169 | REP ci s 170 | REP k cc 171 | REP cc k 172 | REP k qu 173 | REP qu k 174 | REP kw qu 175 | REP o eau 176 | REP eau o 177 | REP o ew 178 | REP ew o 179 | REP oo ew 180 | REP ew oo 181 | REP ew ui 182 | REP ui ew 183 | REP oo ui 184 | REP ui oo 185 | REP ew u 186 | REP u ew 187 | REP oo u 188 | REP u oo 189 | REP u oe 190 | REP oe u 191 | REP u ieu 192 | REP ieu u 193 | REP ue ew 194 | REP ew ue 195 | REP uff ough 196 | REP oo ieu 197 | REP ieu oo 198 | REP ier ear 199 | REP ear ier 200 | REP ear air 201 | REP air ear 202 | REP w qu 203 | REP qu w 204 | REP z ss 205 | REP ss z 206 | REP shun tion 207 | REP shun sion 208 | REP shun cion 209 | REP size cise 210 | -------------------------------------------------------------------------------- /src/utils/__tests__/typoFixer.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, test } from '@jest/globals'; 2 | import { sentenceTypoFixer } from '../typoFixer.js'; 3 | describe('TypoFixer module tests', () => { 4 | test('Should replace the words with issues.', async () => { 5 | // given 6 | const expectedOutput = 'Hello King King new'; 7 | const inputWord = 'Hello Kking K?king new'; 8 | 9 | // when 10 | const output = await sentenceTypoFixer(inputWord); 11 | 12 | // then 13 | expect(output).toBe(expectedOutput); 14 | }); 15 | 16 | test('Should leave sentence in the same condition.', async () => { 17 | // given 18 | const expectedOutput = 'Hello King King new'; 19 | const inputWord = 'Hello King King new'; 20 | 21 | // when 22 | const output = await sentenceTypoFixer(inputWord); 23 | 24 | // then 25 | expect(output).toBe(expectedOutput); 26 | }); 27 | 28 | test('Should replace FFoo by Foo', async () => { 29 | // given 30 | const expectedOutput = 'Foo'; 31 | const inputWord = 'Ffoo'; 32 | 33 | // when 34 | const output = await sentenceTypoFixer(inputWord); 35 | 36 | // then 37 | expect(output).toBe(expectedOutput); 38 | }); 39 | 40 | test('Should not replace picking', async () => { 41 | // given 42 | const expectedOutput = 'picking'; 43 | const inputWord = 'picking'; 44 | 45 | // when 46 | const output = await sentenceTypoFixer(inputWord); 47 | 48 | // then 49 | expect(output).toBe(expectedOutput); 50 | }); 51 | }); 52 | -------------------------------------------------------------------------------- /src/utils/errorHandler.ts: -------------------------------------------------------------------------------- 1 | import { EmbedBuilder } from 'discord.js'; 2 | import type { ExtendedClient } from '../interfaces/ExtendedClient.js'; 3 | import { logHandler } from './logHandler.js'; 4 | 5 | export const errorHandler = async ( 6 | bot: ExtendedClient, 7 | err: unknown, 8 | context: string, 9 | ) => { 10 | const error = err as Error; 11 | 12 | logHandler.log('error', `${context}: ${error.message}`); 13 | logHandler.log( 14 | 'error', 15 | `Stack trace:\n${JSON.stringify( 16 | error.stack || { stack: 'not found' }, 17 | null, 18 | 2, 19 | )}`, 20 | ); 21 | 22 | if (bot.config.debugHook) { 23 | const embed = new EmbedBuilder(); 24 | embed.setTitle(`There was an error message in the ${context}!`); 25 | embed.setDescription( 26 | `\`\`\`\n${JSON.stringify( 27 | error.stack || { stack: 'not found' }, 28 | null, 29 | 2, 30 | )}\n\`\`\``, 31 | ); 32 | embed.addFields([{ name: `Error message`, value: error.message }]); 33 | 34 | await bot.config.debugHook.send({ embeds: [embed] }); 35 | } 36 | }; 37 | -------------------------------------------------------------------------------- /src/utils/getAlexConfig.ts: -------------------------------------------------------------------------------- 1 | import ServerConfig from '../database/models/ServerConfig.js'; 2 | import type { ExtendedClient } from '../interfaces/ExtendedClient.js'; 3 | 4 | export const getAlexConfig = async (bot: ExtendedClient, serverId: string) => { 5 | if (bot.cache[serverId]) { 6 | return bot.cache[serverId]; 7 | } 8 | 9 | const config = await ServerConfig.findOne({ serverId }); 10 | 11 | if (config) { 12 | bot.cache[serverId] = { alexConfig: config.alexConfig }; 13 | return { alexConfig: config.alexConfig }; 14 | } 15 | 16 | const newConfig = await ServerConfig.create({ 17 | serverId, 18 | alexConfig: { 19 | profanitySureness: 1, 20 | noBinary: false, 21 | allow: [], 22 | }, 23 | }); 24 | 25 | bot.cache[serverId] = { alexConfig: newConfig.alexConfig }; 26 | return { alexConfig: newConfig.alexConfig }; 27 | }; 28 | -------------------------------------------------------------------------------- /src/utils/getBannedWordConfig.ts: -------------------------------------------------------------------------------- 1 | import ServerConfig from '../database/models/ServerConfig.js'; 2 | import type { ExtendedClient } from '../interfaces/ExtendedClient.js'; 3 | 4 | export const getBannedWordConfig = async ( 5 | bot: ExtendedClient, 6 | serverId: string, 7 | ) => { 8 | if (bot.cache[serverId]) { 9 | return bot.cache[serverId]; 10 | } 11 | 12 | const config = await ServerConfig.findOne({ serverId }); 13 | 14 | if (config) { 15 | bot.cache[serverId] = { bannedWordConfig: config.bannedWordConfig }; 16 | return { bannedWordConfig: config.bannedWordConfig }; 17 | } 18 | 19 | const newConfig = await ServerConfig.create({ 20 | serverId, 21 | bannedWordConfig: [], 22 | }); 23 | 24 | bot.cache[serverId] = { bannedWordConfig: newConfig.bannedWordConfig }; 25 | return { bannedWordConfig: newConfig.bannedWordConfig }; 26 | }; 27 | -------------------------------------------------------------------------------- /src/utils/loadCommands.ts: -------------------------------------------------------------------------------- 1 | import { readdir } from 'node:fs/promises'; 2 | import { join } from 'node:path'; 3 | import type { Command } from '../interfaces/Command.js'; 4 | import type { ExtendedClient } from '../interfaces/ExtendedClient.js'; 5 | import { errorHandler } from './errorHandler.js'; 6 | 7 | export const loadCommands = async (bot: ExtendedClient): Promise => { 8 | try { 9 | const result: Command[] = []; 10 | const files = await readdir( 11 | join(process.cwd(), 'prod', 'commands'), 12 | 'utf-8', 13 | ); 14 | for (const file of files) { 15 | const name = file.split('.')[0] || ''; 16 | const command = await import( 17 | join(process.cwd(), 'prod', 'commands', file) 18 | ); 19 | result.push(command[name] as Command); 20 | } 21 | return result; 22 | } catch (err) { 23 | await errorHandler(bot, err, 'command loader'); 24 | return []; 25 | } 26 | }; 27 | -------------------------------------------------------------------------------- /src/utils/logHandler.ts: -------------------------------------------------------------------------------- 1 | import { createLogger, format, transports, config } from 'winston'; 2 | 3 | const { combine, timestamp, colorize, printf } = format; 4 | 5 | /** 6 | * Standard log handler, using winston to wrap and format 7 | * messages. Call with `logHandler.log(level, message)`. 8 | * 9 | * @param {string} level - The log level to use. 10 | * @param {string} message - The message to log. 11 | */ 12 | export const logHandler = createLogger({ 13 | levels: config.npm.levels, 14 | level: 'silly', 15 | transports: [new transports.Console()], 16 | format: combine( 17 | timestamp({ 18 | format: 'YYYY-MM-DD HH:mm:ss', 19 | }), 20 | colorize(), 21 | printf((info) => `${info.level}: ${[info.timestamp]}: ${info.message}`), 22 | ), 23 | exitOnError: false, 24 | }); 25 | -------------------------------------------------------------------------------- /src/utils/registerCommands.ts: -------------------------------------------------------------------------------- 1 | import { REST } from '@discordjs/rest'; 2 | import { Routes } from 'discord.js'; 3 | import type { ExtendedClient } from '../interfaces/ExtendedClient.js'; 4 | import { errorHandler } from './errorHandler.js'; 5 | import { logHandler } from './logHandler.js'; 6 | 7 | /** 8 | * TODO: We want to be able to run this via a text command rather than on load. 9 | */ 10 | export const registerCommands = async (bot: ExtendedClient) => { 11 | try { 12 | if (!bot.user?.id) { 13 | logHandler.log( 14 | 'error', 15 | 'Cannot register commands as bot has not authenticated to Discord.', 16 | ); 17 | return; 18 | } 19 | const rest = new REST({ version: '10' }).setToken(bot.config.token); 20 | const commandData = bot.commands.map((command) => command.data.toJSON()); 21 | 22 | if (!commandData.length) { 23 | logHandler.log('warn', 'No commands found to register.'); 24 | return; 25 | } 26 | 27 | if (process.env.NODE_ENV === 'production') { 28 | logHandler.log('info', 'Registering commands globally'); 29 | await rest.put(Routes.applicationCommands(bot.user.id), { 30 | body: commandData, 31 | }); 32 | } else { 33 | logHandler.log('info', 'Registering to home guild only!'); 34 | await rest.put( 35 | Routes.applicationGuildCommands(bot.user.id, bot.config.homeGuild), 36 | { body: commandData }, 37 | ); 38 | } 39 | } catch (err) { 40 | await errorHandler(bot, err, 'register commands'); 41 | } 42 | }; 43 | -------------------------------------------------------------------------------- /src/utils/typoFixer.ts: -------------------------------------------------------------------------------- 1 | import { dict } from '../config/DictionaryOptions.js'; 2 | 3 | export async function sentenceTypoFixer(sentence: string) { 4 | const sentenceWords = sentence.split(/\s+/); 5 | const updatedWords = await Promise.all( 6 | sentenceWords.map(transformAndTypoFixer), 7 | ); 8 | return updatedWords.join(' '); 9 | } 10 | async function wordTypoFixer(word: string) { 11 | return dict.ready.then(() => { 12 | return dict.check(word) ? word : dict.suggest(word, 1).at(0); 13 | }); 14 | } 15 | 16 | async function transformAndTypoFixer(word: string) { 17 | const cleanWord = word.replace(/[^a-zA-Z]/g, ''); 18 | return await wordTypoFixer(cleanWord); 19 | } 20 | -------------------------------------------------------------------------------- /src/utils/validateEnv.ts: -------------------------------------------------------------------------------- 1 | import { WebhookClient } from 'discord.js'; 2 | import type { ExtendedClient } from '../interfaces/ExtendedClient.js'; 3 | import { logHandler } from './logHandler.js'; 4 | 5 | export const validateEnv = (bot: ExtendedClient) => { 6 | if (!process.env.DISCORD_TOKEN) { 7 | logHandler.log('error', 'Missing "DISCORD_TOKEN" environment variables!'); 8 | process.exit(1); 9 | } 10 | if (!process.env.EDDIEBOT_MONGO_CONNECTION_STRING) { 11 | logHandler.log( 12 | 'error', 13 | 'Missing "EDDIEBOT_MONGO_CONNECTION_STRING" environment variables!', 14 | ); 15 | process.exit(1); 16 | } 17 | if (!process.env.HOME_GUILD) { 18 | logHandler.log('error', 'Missing "HOME_GUILD" environment variables!'); 19 | process.exit(1); 20 | } 21 | 22 | bot.config = { 23 | token: process.env.DISCORD_TOKEN, 24 | dbUri: process.env.EDDIEBOT_MONGO_CONNECTION_STRING, 25 | debugHook: process.env.DEBUG_HOOK 26 | ? new WebhookClient({ 27 | url: process.env.DEBUG_HOOK, 28 | }) 29 | : undefined, 30 | homeGuild: process.env.HOME_GUILD, 31 | }; 32 | }; 33 | -------------------------------------------------------------------------------- /tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "exclude": ["node_modules", "test", "dist", "**/*spec.ts"] 4 | } 5 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends" : "@total-typescript/tsconfig/tsc/no-dom/app", 3 | "compilerOptions": { 4 | "outDir": "./prod", 5 | "sourceMap": false, 6 | "verbatimModuleSyntax": false, 7 | }, 8 | "include": ["src/**/*"], 9 | "exclude": ["src/**/__test__/*"] 10 | } 11 | --------------------------------------------------------------------------------