refactor: improvements to codebase

This commit is contained in:
Jon Deaves
2020-08-14 19:20:34 +01:00
committed by GitHub
parent e426eac92d
commit 174524e8dc
35 changed files with 4080 additions and 280 deletions

View File

@@ -15,4 +15,4 @@ indent_size = 2
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
insert_final_newline = true

41
.eslintignore Normal file
View File

@@ -0,0 +1,41 @@
.eslintrc.js
# compiled output
/dist
/node_modules
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
# OS
.DS_Store
# Tests
**/test-results/**
/.nyc_output
# IDEs and editors
/.idea
.project
.classpath
.c9/
*.launch
.settings/
*.sublime-workspace
# IDE - VSCode
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
# Project specific
.env
.vscode/
**/logs/**

59
.eslintrc.js Normal file
View File

@@ -0,0 +1,59 @@
module.exports = {
parserOptions: {
project: './tsconfig.json',
tsconfigRootDir: __dirname,
},
plugins: [
"@typescript-eslint",
"eslint-comments",
"jest",
"promise",
"unicorn",
],
extends: [
"airbnb-typescript",
"plugin:@typescript-eslint/recommended",
"plugin:eslint-comments/recommended",
"plugin:jest/recommended",
"plugin:promise/recommended",
"plugin:unicorn/recommended",
"prettier",
"prettier/react",
"prettier/@typescript-eslint",
],
env: {
node: true,
browser: true,
jest: true,
},
rules: {
// Too restrictive, writing ugly code to defend against a very unlikely scenario: https://eslint.org/docs/rules/no-prototype-builtins
"no-prototype-builtins": "off",
// https://basarat.gitbooks.io/typescript/docs/tips/defaultIsBad.html
"import/prefer-default-export": "off",
"import/no-default-export": "off",
// Too restrictive: https://github.com/yannickcr/eslint-plugin-react/blob/master/docs/rules/destructuring-assignment.md
"react/destructuring-assignment": "off",
// No jsx extension: https://github.com/facebook/create-react-app/issues/87#issuecomment-234627904
"react/jsx-filename-extension": "off",
// Use function hoisting to improve code readability
"no-use-before-define": [
"error",
{ functions: false, classes: true, variables: true },
],
// Makes no sense to allow type inferrence for expression parameters, but require typing the response
"@typescript-eslint/explicit-function-return-type": [
"error",
{ allowExpressions: true, allowTypedFunctionExpressions: true },
],
"@typescript-eslint/no-use-before-define": [
"error",
{ functions: false, classes: true, variables: true, typedefs: true },
],
// Common abbreviations are known and readable
"unicorn/prevent-abbreviations": "off",
"unicorn/filename-case": "off",
"unicorn/no-fn-reference-in-iterator": "off",
"no-underscore-dangle": ["error", { "allowAfterThis": true }]
},
}

32
.github/ISSUE_TEMPLATE.md vendored Normal file
View File

@@ -0,0 +1,32 @@
Issue tracker is **ONLY** used for reporting bugs. New features should be discussed on our slack channel. Please use [stackoverflow](https://stackoverflow.com) for supporting issues.
<!--- Provide a general summary of the issue in the Title above -->
## Expected Behavior
<!--- Tell us what should happen -->
## Current Behavior
<!--- Tell us what happens instead of the expected behavior -->
## Possible Solution
<!--- Not obligatory, but suggest a fix/reason for the bug, -->
## Steps to Reproduce
<!--- Provide a link to a live example, or an unambiguous set of steps to -->
<!--- reproduce this bug. Include code to reproduce, if relevant -->
1.
2.
3.
4.
## Context (Environment)
<!--- How has this issue affected you? What are you trying to accomplish? -->
<!--- Providing context helps us come up with a solution that is most useful in the real world -->
<!--- Provide a general summary of the issue in the Title above -->
## Detailed Description
<!--- Provide a detailed description of the change or addition you are proposing -->
## Possible Implementation
<!--- Not obligatory, but suggest an idea for implementing addition or change -->

View File

@@ -2,14 +2,15 @@
<!-- PR Context:
A quick overview of the "why" of this PR to help reviewers
If applicable, provide links to GitHub tickets
If applicable, provide links to GitHub issues
-->
[GitHub Ticket](https://github.com/jondeaves/venom/issues/XXX)
[GitHub Issue](https://github.com/jondeaves/venom/issues/XXX)
## Description
<!-- Description of PR:
List of bullet points and screenshots are appreciated
List of bullet points are appreciated
If adding new features to the bot then screenshots of the interactions with your own test bot would be very helpful
If applicable, have you thought of updating existing docs or
create a new one?
-->

16
.markdownlint.yml Normal file
View File

@@ -0,0 +1,16 @@
default: true
# Some services use a linebreak-sensitive renderer, e.g. GitHub comment and BitBucket
line-length: false
# This project needs to be able to document a code block with a space
no-space-in-code: false
no-trailing-punctuation:
# Allow headings to end with question mark for FAQ
punctuation: ".,;:"
# Prettier handles these:
list-marker-space: false
no-multiple-blanks: false

39
.markdownlintignore Normal file
View File

@@ -0,0 +1,39 @@
# compiled output
/dist
/node_modules
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
# OS
.DS_Store
# Tests
**/test-results/**
/.nyc_output
# IDEs and editors
/.idea
.project
.classpath
.c9/
*.launch
.settings/
*.sublime-workspace
# IDE - VSCode
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
# Project specific
.env
.vscode/
**/logs/**

View File

@@ -1,8 +0,0 @@
{
"endOfLine": "lf",
"semi": true,
"singleQuote": true,
"tabWidth": 2,
"trailingComma": "all",
"printWidth": 120
}

74
CODE-OF-CONDUCT.md Normal file
View File

@@ -0,0 +1,74 @@
# Contributor Covenant Code of Conduct
## Our Pledge
In the interest of fostering an open and welcoming environment, we as
contributors and maintainers pledge to making participation in our project and
our community a harassment-free experience for everyone, regardless of age, body
size, disability, ethnicity, gender identity and expression, level of experience,
nationality, personal appearance, race, religion, or sexual identity and
orientation.
## Our Standards
Examples of behavior that contributes to creating a positive environment
include:
* Using welcoming and inclusive language
* Being respectful of differing viewpoints and experiences
* Gracefully accepting constructive criticism
* Focusing on what is best for the community
* Showing empathy towards other community members
Examples of unacceptable behavior by participants include:
* The use of sexualized language or imagery and unwelcome sexual attention or
advances
* Trolling, insulting/derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or electronic
address, without explicit permission
* Other conduct which could reasonably be considered inappropriate in a
professional setting
## Our Responsibilities
Project maintainers are responsible for clarifying the standards of acceptable
behavior and are expected to take appropriate and fair corrective action in
response to any instances of unacceptable behavior.
Project maintainers have the right and responsibility to remove, edit, or
reject comments, commits, code, wiki edits, issues, and other contributions
that are not aligned to this Code of Conduct, or to ban temporarily or
permanently any contributor for other behaviors that they deem inappropriate,
threatening, offensive, or harmful.
## Scope
This Code of Conduct applies both within project spaces and in public spaces
when an individual is representing the project or its community. Examples of
representing a project or community include using an official project e-mail
address, posting via an official social media account, or acting as an appointed
representative at an online or offline event. Representation of a project may be
further defined and clarified by project maintainers.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported by contacting the project team at hello@jondeaves.me. All
complaints will be reviewed and investigated and will result in a response that
is deemed necessary and appropriate to the circumstances. The project team is
obligated to maintain confidentiality with regard to the reporter of an incident.
Further details of specific enforcement policies may be posted separately.
Project maintainers who do not follow or enforce the Code of Conduct in good
faith may face temporary or permanent repercussions as determined by other
members of the project's leadership.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
available at [https://contributor-covenant.org/version/1/4][version]
[homepage]: https://contributor-covenant.org
[version]: https://contributor-covenant.org/version/1/4/

198
CONTRIBUTING.md Normal file
View File

@@ -0,0 +1,198 @@
# Contributing to Venom
First off, thanks for taking the time to contribute! ❤️
All types of contributions are encouraged and valued. See the [Table of Contents](#table-of-contents) for different ways to help and details about how this project handles them. Please make sure to read the relevant section before making your contribution. It will make it a lot easier for us maintainers and smooth out the experience for all involved. The community looks forward to your contributions. 🎉
<!-- omit in toc -->
## Table of Contents
- [I Have a Question](#i-have-a-question)
- [I Want To Contribute](#i-want-to-contribute)
- [Reporting Bugs](#reporting-bugs)
- [Suggesting Enhancements](#suggesting-enhancements)
- [Your First Code Contribution](#your-first-code-contribution)
- [Improving The Documentation](#improving-the-documentation)
- [Styleguides](#styleguides)
- [Commit Messages](#commit-messages)
- [Join The Project Team](#join-the-project-team)
## I Have a Question
Before you ask a question, it is best to search for existing [Issues](https://github.com/jondeaves/venom/issues) that might help you. In case you have found a suitable issue and still need clarification, you can write your question in this issue. It is also advisable to search the internet for answers first.
If you then still feel the need to ask a question and need clarification, we recommend the following:
- Open an [Issue](https://github.com/jondeaves/venom/issues/new).
- Provide as much context as you can about what you're running into.
- Provide project and platform versions (nodejs, npm, etc), depending on what seems relevant.
- Add the "question" label
We will then take care of the issue as soon as possible.
<!--
You might want to create a separate issue tag for questions and include it in this description. People should then tag their issues accordingly.
Depending on how large the project is, you may want to outsource the questioning, e.g. to Stack Overflow or Gitter. You may add additional contact and information possibilities:
- IRC
- Slack
- Gitter
- Stack Overflow tag
- Blog
- FAQ
- Roadmap
- E-Mail List
- Forum
-->
## I Want To Contribute
### Legal Notice
> When contributing to this project, you must agree that you have authored 100% of the content, that you have the necessary rights to the content and that the content you contribute may be provided under the project license.
### Reporting Bugs
#### Before Submitting a Bug Report
A good bug report shouldn't leave others needing to chase you up for more information. Therefore, we ask you to investigate carefully, collect information and describe the issue in detail in your report. Please complete the following steps in advance to help us fix any potential bug as fast as possible.
- Make sure that you are using the latest version.
- Determine if your bug is really a bug and not an error on your side e.g. using incompatible environment components/versions (Make sure that you have read the [documentation](README.md).
- To see if other users have experienced (and potentially already solved) the same issue you are having, check if there is not already a bug report existing for your bug or error in the [bug tracker](https://github.com/jondeaves/venom/issues?q=label%3Abug).
- Also make sure to search the internet (including Stack Overflow) to see if users outside of the GitHub community have discussed the issue.
- Collect information about the bug:
- Stack trace (Traceback)
- OS, Platform and Version (Windows, Linux, macOS, x86, ARM)
- Version of the interpreter, compiler, SDK, runtime environment, package manager, depending on what seems relevant.
- Possibly your input and the output
- Can you reliably reproduce the issue? And can you also reproduce it with older versions?
#### How Do I Submit a Good Bug Report?
> You must never report security related issues, vulnerabilities or bugs to the issue tracker, or elsewhere in public. Instead sensitive bugs must be sent by email to hello@jondeaves.me.
We use GitHub issues to track bugs and errors. If you run into an issue with the project:
- Open an [Issue](https://github.com/jondeaves/venom/issues/new). (Since we can't be sure at this point whether it is a bug or not, we ask you not to label the issue.)
- Fill in as much of the template as possible. In particular;
- Explain the behavior you would expect and the actual behavior.
- Please provide as much context as possible and describe the _reproduction steps_ that someone else can follow to recreate the issue on their own. This usually includes your code. For good bug reports you should isolate the problem and create a reduced test case.
- Provide the information you collected in the previous section.
Once it's filed:
- The project team will triage the issue accordingly.
- A team member will try to reproduce the issue with your provided steps. If there are no reproduction steps or no obvious way to reproduce the issue, the team will ask you for those steps and mark the issue as `needs-detail`. Bugs with the `needs-detail` tag will not be addressed until they have more information added.
- If the team is able to reproduce the issue, it will be moved onto the project board and will be picked up by a project member.
### Suggesting Enhancements
This section guides you through submitting an enhancement suggestion for Venom, **including completely new features and minor improvements to existing functionality**. Following these guidelines will help maintainers and the community to understand your suggestion and find related suggestions.
#### Before Submitting an Enhancement
- Make sure that you are using the latest version.
- Read the [documentation](README.md) carefully and find out if the functionality is already covered, maybe by an individual configuration.
- Perform a [search](https://github.com/jondeaves/venom/issues) to see if the enhancement has already been suggested. If it has, add a comment to the existing issue instead of opening a new one.
- Find out whether your idea fits with the scope and aims of the project. It's up to you to make a strong case to convince the project's developers of the merits of this feature. Keep in mind that we want features that will be useful to the majority of our users and not just a small subset. If you're just targeting a minority of users, consider writing an add-on/plugin library.
#### How Do I Submit a Good Enhancement Suggestion?
Enhancement suggestions are tracked as [GitHub issues](https://github.com/jondeaves/venom/issues).
- Use a **clear and descriptive title** for the issue to identify the suggestion.
- Provide a **step-by-step description of the suggested enhancement** in as many details as possible.
- **Describe the current behavior** and **explain which behavior you expected to see instead** and why. At this point you can also tell which alternatives do not work for you.
- **Explain why this enhancement would be useful** to most Venom users. You may also want to point out the other projects that solved it better and which could serve as inspiration.
### Your First Code Contribution
There are a few steps a developer can take to make contributing to this project go more smoothly.
- Given this project is a Discord bot it means that testing changes can be difficult without the right tools set up and so the first step to contributing would be to [setup your own test environments](docs/development/environments.md).
- The project requires Node (We recommend the latest LTS) and Yarn.
- You can reference the [README.md](README.md) for steps to get the project running.
#### VSCode
If you are using VSCode then we recommend installing the ESLint and Prettier plugins and you can use the below configuration in `.vscode/settings.json` to have Prettier auto-format code for you.
```json
{
"eslint.packageManager": "yarn",
"javascript.format.enable": false,
"editor.formatOnSave": true
}
```
### Improving The Documentation
If your change adds new features, configuration options or anything else that may require relevant documentation then it is important that those updates are done as part of your work. It is important that anything that goes into the main branch is complete, documented and usable by others.
## Styleguides
We follow [Angular's Commit Conventions](https://docs.google.com/document/d/1QrDFcIiPjSLDn3EL15IJygNPiHORgU1_OOAqWjiDU5Y/edit#)
### Commit Message Format
Each commit message consists of a **header**, a **body** and a **footer**. The header has a special
format that includes a **type**, a **scope** and a **subject**:
```txt
<type>(<scope>): <subject>
<BLANK LINE>
<body>
<BLANK LINE>
<footer>
```
The **header** is mandatory and the **scope** of the header is optional.
Any line of the commit message cannot be longer 100 characters! This allows the message to be easier
to read on GitHub as well as in various git tools.
### Revert
If the commit reverts a previous commit, it should begin with `revert:`, followed by the header of the reverted commit. In the body it should say: `This reverts commit <hash>.`, where the hash is the SHA of the commit being reverted.
### Type
Must be one of the following:
- **feat**: A new feature
- **fix**: A bug fix
- **docs**: Documentation only changes
- **style**: Changes that do not affect the meaning of the code (white-space, formatting, missing
semi-colons, etc)
- **refactor**: A code change that neither fixes a bug nor adds a feature
- **perf**: A code change that improves performance
- **test**: Adding missing tests
- **chore**: Changes to the build process or auxiliary tools and libraries such as documentation
generation
- **misc**: Changes that don't quite fit into other categories but are worth mentioning in the CHANGELOG - use sparingly.
### Subject
The subject contains succinct description of the change:
- use the imperative, present tense: "change" not "changed" nor "changes"
- don't capitalize first letter
- no dot (.) at the end
### Body
Just as in the **subject**, use the imperative, present tense: "change" not "changed" nor "changes".
The body should include the motivation for the change and contrast this with previous behavior.
### Footer
The footer should contain any information about **Breaking Changes** and is also the place to
reference GitHub issues that this commit **Closes**.
**Breaking Changes** should start with the word `BREAKING CHANGE:` with a space or two newlines. The rest of the commit message is then used for this.
## Attribution
This guide is based on the **contributing-gen**. [Make your own](https://github.com/bttger/contributing-gen)!

21
LICENSE.md Normal file
View File

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

View File

@@ -5,21 +5,39 @@ This is the Discord bot for Creation Asylum.
## Development
- Requires [NodeJS](https://nodejs.org/), recommend at least the latest LTS version.
- Run `yarn` or `npm install` to install dependencies
- Requires [Yarn](https://classic.yarnpkg.com/lang/en/), recommend latest stable 1.x
- Run `yarn` to install dependencies
### VSCode
For VSCode install the following plugins;
- [ESLint](https://marketplace.visualstudio.com/items?itemName=dbaeumer.vscode-eslint)
- [Prettier](https://marketplace.visualstudio.com/items?itemName=esbenp.prettier-vscode)
You wil also want to use the below configuration in `.vscode/settings.json` to have Prettier auto-format code for you.
```json
{
"eslint.packageManager": "yarn",
"javascript.format.enable": false,
"editor.formatOnSave": true
}
```
### Environment Variables
At a minimum you need to provide the Discord bots Token, which can be found on the Bot tab of a Discord application. See table below for possible values.
At a minimum you need to provide the Discord bots Token (which can be found on the Bot tab of a Discord application), MONGODB_URI and MONGODB_DB_NAME values. See table below for possible values.
| key | description | example |
|-------------------|-------------|---------|
| BOT_TRIGGER | Prefix of message to let bot know you are speaking to it
| DISCORD_BOT_TOKEN | Discord bots Token
| NODE_ENV | What environment the bot is running in | `production`, `development` or `test` |
| LOG_LEVEL | What level of logs should be displayed in console | `error`, `warn`, `info`, `verbose`, `debug` or `silly` |
| MONGODB_URI | Full connection string for MongoDB database, include db_name if user is scoped to single database | mongodb://user:password@localhost:27017/venom_db |
| MONGODB_DB_NAME | The name of the database to use for this project | venom_db |
| key | description | example |
| ----------------- | ------------------------------------------------------------------------------------------------- | ------------------------------------------------------ |
| BOT_TRIGGER | Prefix of message to let bot know you are speaking to it |
| DISCORD_BOT_TOKEN | Discord bots Token |
| NODE_ENV | What environment the bot is running in | `production`, `development` or `test` |
| LOG_LEVEL | What level of logs should be displayed in console | `error`, `warn`, `info`, `verbose`, `debug` or `silly` |
| MONGODB_URI | Full connection string for MongoDB database, include db_name if user is scoped to single database | mongodb://user:password@localhost:27017/venom_db |
| MONGODB_DB_NAME | The name of the database to use for this project | venom_db |
### Bot commands
To add a command you create a Typescript file in `src/bot/commands/[filename].ts` and ensure it implements the \src/bot/commands/ICommand.ts` interface. You can see the other files in this directory for implementation examples. Also ensure you export this file in `src/bot/commands/index.ts`.
To add a command you create a Typescript file in `src/bot/commands/[filename].ts` and ensure it implements the `src/bot/commands/ICommand.ts` interface. You can see the other files in this directory for implementation examples. Also ensure you export this file in `src/bot/commands/index.ts`.

3
commitlint.config.js Normal file
View File

@@ -0,0 +1,3 @@
module.exports = {
extends: ['@commitlint/config-conventional'],
};

View File

@@ -0,0 +1,41 @@
# Developer Environments
Due to the nature of developing a Discord bot it can be helpful to have your own versions of certain systems used by the bot, in order to manually verify your changes.
## Discord Test Bot
Creation your own Bot will be critical to test any new changes made to how the bot is interacted with and indeed testing that most things work.
### Creation a Bot
- Visit the [Discord developer page](https://discord.com/developers/applications)
- Click "New Application" in the top right
- Name your Application, we like to use the format `VenomBotTest[Your name or initials]`, this helps identify the bot easily.
- Once you hit create you will be presented with your application page. Take a note of the `CLIENT ID` that is found just next to your Applications Icon.
- Setting an Icon isn't necessary but can be helpful to identify it.
- In the left navigation select the "Bot" tab and then select "Add Bot" on the right of the new content.
- Confirm you wish to add a bot.
- Disable the "Public Bot" flag, given this is a test bot you don't want others finding it.
- Copy the Bot token that can be found next to the Bots icon (This will be used in the environment variables ). You can just click the "Copy" button.
- Scroll to the bottom of this page and enable "Permissions" for your bot, at this time only Text Permissions are needed and these are;
- Send Messages
- [You can add additional permissions if the feature you are development would require this]
### Adding your bot to a server
The best way to test new work is to add the test bot you created above to a different Discord server, possibly even a private server just for this purpose. Once you have a server that you are able to invite bots to then [follow this guide](https://discordjs.guide/preparations/adding-your-bot-to-servers.html) to add your bot. You will need the `CLIENT ID` from before.
## MongoDB Instance
The bot uses MongoDB to store it's data, as a consequence of this you will need to have your own instance of MongoDB if you wish to run the bot yourself. If you don't want to install an instance on your own machine then we can recommend [mLab](https://mlab.com/), as this will be as close to the production version as possible.
## Final thoughts
Once the above has been completed you can then run your bot locally, ensuring you set the values in `src/.env` to match those provided throughtout the steps above. You may also wish to set the following values.
- Set `BOT_TRIGGER` to `venom test ` or something similarly unique. Ensuring you don't confuse your bot with another.
- Set `LOG_LEVEL` to `verbose` to see all possible log information.
- Set `ENVIRONMENT` to `development`.
- As mentioned above, set `DISCORD_BOT_TOKEN` to the value taken from the "Bot" tab of the Discord developer Application.
- Set `MONGODB_URI` to the value of either your local Mongo instance or mLab. The format looks like; `mongodb://[username]:[password]@[host]:[port]/[db_name]`
- Set `MONGODB_DB_NAME` to the database name you are using locally, matching the same value in the previous environment variable.

3
extensions.json Normal file
View File

@@ -0,0 +1,3 @@
{
"recommendations": ["dbaeumer.vscode-eslint", "esbenp.prettier-vscode"]
}

View File

@@ -13,7 +13,15 @@
"scripts": {
"build": "tsc -p tsconfig.build.json",
"start": "node dist/main.js",
"start:dev": "nodemon"
"start:dev": "nodemon",
"lint": "run-p lint:code lint:markdown",
"lint:code": "eslint src/**/*.{ts,js}",
"lint:markdown": "markdownlint **/*.md",
"format": "prettier \"**/*.ts\" --ignore-path ./.prettierignore --write",
"typecheck": "tsc --noEmit",
"release:patch": "standard-version --release-as patch",
"release:minor": "standard-version --release-as minor",
"release:major": "standard-version --release-as major"
},
"dependencies": {
"discord.js": "~12.2.0",
@@ -24,17 +32,51 @@
"winston": "~3.3.3"
},
"devDependencies": {
"@commitlint/config-conventional": "^9.1.1",
"@types/dotenv": "~8.2.0",
"@types/mongodb": "~3.5.25",
"@types/node": "~14.0.27",
"@types/winston": "~2.4.4",
"@typescript-eslint/eslint-plugin": "^3.6.1",
"chalk": "^4.1.0",
"commitlint": "^9.1.0",
"eslint": "^7.6.0",
"eslint-config-airbnb-typescript": "^9.0.0",
"eslint-config-prettier": "^6.11.0",
"eslint-formatter-pretty": "^4.0.0",
"eslint-plugin-eslint-comments": "^3.2.0",
"eslint-plugin-import": "^2.22.0",
"eslint-plugin-jest": "^23.20.0",
"eslint-plugin-jsx-a11y": "^6.3.1",
"eslint-plugin-promise": "^4.2.1",
"eslint-plugin-react": "^7.20.6",
"eslint-plugin-unicorn": "^21.0.0",
"husky": "^4.2.5",
"lint-staged": "^10.2.11",
"markdownlint-cli": "^0.23.2",
"nodemon": "~2.0.4",
"npm-run-all": "^4.1.5",
"prettier": "~2.0.5",
"standard-version": "^8.0.2",
"ts-node": "~8.10.2",
"tsconfig-paths": "~3.9.0",
"tslint": "~6.1.3",
"tslint-config-prettier": "~1.18.0",
"tslint-react": "~5.0.0",
"typescript": "~3.9.7"
},
"husky": {
"hooks": {
"commit-msg": "commitlint -E HUSKY_GIT_PARAMS",
"pre-commit": "./node_modules/.bin/lint-staged"
}
},
"lint-staged": {
"*.{ts,tsx,js}": [
"yarn format",
"yarn lint:code",
"yarn lint:markdown"
]
},
"engines": {
"node": ">=10 <=14",
"yarn": ">=1.10.0"
}
}

18
prettier.config.js Normal file
View File

@@ -0,0 +1,18 @@
module.exports = {
endOfLine: 'lf',
semi: true,
singleQuote: true,
tabWidth: 2,
trailingComma: "all",
printWidth: 120,
overrides: [
{
files: ".editorconfig",
options: { parser: "yaml" },
},
{
files: ["CODE-OF-CONDUCT.md", "CONTRIBUTING.md", "LICENSE.md", "README.md"],
options: { parser: "markdown" },
},
],
}

View File

@@ -1,5 +1,5 @@
import { exit } from 'process';
import Discord from 'discord.js';
import { exit } from 'process';
import container from './inversity.config';
@@ -7,12 +7,14 @@ import ConfigService from './core/services/config.service';
import LoggerService from './core/services/logger.service';
import MongoService from './core/services/mongo.service';
import ICommand from './bot/commands/ICommand';
import rawCommands from './bot/commands';
import ICommand from './bot/commands/ICommand';
export default class App {
private _configService: ConfigService = container.resolve<ConfigService>(ConfigService);
private _loggerService: LoggerService = container.resolve<LoggerService>(LoggerService);
private _dbService: MongoService = container.resolve<MongoService>(MongoService);
private _discordClient: Discord.Client;
@@ -20,15 +22,15 @@ export default class App {
public async init(): Promise<void> {
try {
await this._dbService.connect();
} catch (err) {
this._loggerService.log('error', 'Cannot connect to database, exiting.');
} catch (error) {
this._loggerService.log('error', 'Cannot connect to database, exiting.', { error });
exit(1);
}
this._discordClient = new Discord.Client();
const commandList = new Discord.Collection<string, ICommand>();
rawCommands.forEach(rawCommand => {
rawCommands.forEach((rawCommand) => {
commandList.set(rawCommand.name, rawCommand);
});
@@ -38,7 +40,7 @@ export default class App {
});
// Triggers on every message the bot can see
this._discordClient.on('message', async message => {
this._discordClient.on('message', async (message) => {
const prefix = this._configService.get('BOT_TRIGGER');
// If the message either doesn't start with the prefix or was sent by a bot, exit early.
@@ -46,44 +48,50 @@ export default class App {
const args = message.content.slice(prefix.length).trim().split(/ +/);
const commandName = args.shift().toLowerCase();
const command = commandList.get(commandName) || commandList.find(cmd => cmd.aliases && cmd.aliases.includes(commandName));
const command =
commandList.get(commandName) || commandList.find((cmd) => cmd.aliases && cmd.aliases.includes(commandName));
if (!command) {
return message.reply('looks like I haven\'t learned that trick yet!');
};
try {
await command.execute(message, args, prefix, commandList, this._dbService);
} catch (error) {
this._loggerService.log('error', error.message);
message.reply('there was an error trying to follow that command!');
message.reply("looks like I haven't learned that trick yet!");
} else {
try {
await command.execute(message, args, prefix, commandList, this._dbService);
} catch (error) {
this._loggerService.log('error', error.message);
message.reply('there was an error trying to follow that command!');
}
}
});
this._discordClient.on('guildMemberAdd', member => {
this._discordClient.on('guildMemberAdd', (member) => {
// base
const greetings = ["Hello, {name}! CA greets you!", "Welcome to CA, {name}!", "Hi {name}! Welcome to CA!"];
const greetings = ['Hello, {name}! CA greets you!', 'Welcome to CA, {name}!', 'Hi {name}! Welcome to CA!'];
const greeting = greetings[Math.floor(Math.random() * greetings.length - 1)];
// favor
const flavors = [
"As PROMISED, grab a free pie! Courtesy of {random}!",
"The water is pure here! You should ask {random} for their water purified water for a sip!",
"Home of the sane, the smart and {random}!"
'As PROMISED, grab a free pie! Courtesy of {random}!',
'The water is pure here! You should ask {random} for their water purified water for a sip!',
'Home of the sane, the smart and {random}!',
];
const randomMember = member.guild.members.cache.random();
const flavor = flavors[Math.floor(Math.random() * flavors.length - 1)];
// result
member.guild.systemChannel.send(greeting.replace('{name}', member.displayName) + " " + flavor.replace('{random}', randomMember.displayName));
member.guild.systemChannel.send(
`${greeting.replace('{name}', member.displayName)} ${flavor.replace('{random}', randomMember.displayName)}`,
);
});
this._discordClient.login(this._configService.get('DISCORD_BOT_TOKEN'))
.catch((reason) => {
this._loggerService.log('error', `Cannot initialise Discord client. Check the token: ${this._configService.get('DISCORD_BOT_TOKEN')}`);
exit(1);
});
this._discordClient.login(this._configService.get('DISCORD_BOT_TOKEN')).catch((error) => {
this._loggerService.log(
'error',
`Cannot initialise Discord client. Check the token: ${this._configService.get('DISCORD_BOT_TOKEN')}`,
{ error },
);
exit(1);
});
}
public exit() {
public exit(): void {
this._dbService.disconnect();
}
}

View File

@@ -7,18 +7,38 @@ const command: ICommand = {
aliases: ['eightball', 'magicball', 'ball', 'wisdomball'],
description: 'Ask the magic eightball for advice!',
async execute(message: Discord.Message, args: string[]) {
if (args.length == 0)
{
message.reply('where\'s the question?');
} else {
const responses = ["as I see it, yes.", "err, ask again later.", "better not tell you now.", "that's hard to predict right now.",
"concentrate... and ask again.", "don't count on it.", "it is certain.", "it is decidedly so, yes.", "most likely.", "no.",
"likely not.", "my sources say no.", "hmm, outlook not so good.", "okay, outlook is good.", "not sure, ask again later.",
"as dubealex commands it: maybe!", "googliano'd the answer and uh, it's a yes?", "careful, but yes",
"signs point to a yes.", "very doubtful, very doubtful.", "without a doubt.","yes.", "yes - definitely.", "yeah, you can rely on it."];
message.reply(responses[Math.floor(Math.random() * responses.length - 1)]);
}
if (args.length === 0) {
return message.reply("where's the question?");
}
const responses = [
'as I see it, yes.',
'err, ask again later.',
'better not tell you now.',
"that's hard to predict right now.",
'concentrate... and ask again.',
"don't count on it.",
'it is certain.',
'it is decidedly so, yes.',
'most likely.',
'no.',
'likely not.',
'my sources say no.',
'hmm, outlook not so good.',
'okay, outlook is good.',
'not sure, ask again later.',
'as dubealex commands it: maybe!',
"googliano'd the answer and uh, it's a yes?",
'careful, but yes',
'signs point to a yes.',
'very doubtful, very doubtful.',
'without a doubt.',
'yes.',
'yes - definitely.',
'yeah, you can rely on it.',
];
return message.reply(responses[Math.floor(Math.random() * responses.length - 1)]);
},
};
export default command;
export default command;

View File

@@ -1,10 +1,17 @@
import Discord, { Collection } from 'discord.js';
import MongoService from 'src/core/services/mongo.service';
import MongoService from '../../core/services/mongo.service';
export default interface ICommand {
name: string;
aliases?: string[];
description: string;
example?: string;
execute: (message: Discord.Message, args: string[], prefix?: string, commands?: Collection<string, ICommand>, dbService?: MongoService) => Promise<any>,
}
execute: (
message: Discord.Message,
args: string[],
prefix?: string,
commands?: Collection<string, ICommand>,
dbService?: MongoService,
) => Promise<Discord.Message>;
}

View File

@@ -1,7 +1,7 @@
import Discord, { Collection } from 'discord.js';
import ConfigService from '../../core/services/config.service';
import MongoService from '../../core/services/mongo.service';
import ConfigService from '../../core/services/config.service'
import container from '../../inversity.config';
@@ -12,16 +12,23 @@ const prefix = container.resolve<ConfigService>(ConfigService).get('BOT_TRIGGER'
const command: ICommand = {
name: 'addgreeting',
aliases: ['ag'],
description: 'Adds a string to the list greetings used when new users connect to server! Include `{name}` in your message to replace with the new users name.',
description:
'Adds a string to the list greetings used when new users connect to server! Include `{name}` in your message to replace with the new users name.',
example: `\`${prefix}addgreeting Welcome to the club {name}\``,
async execute(message: Discord.Message, args: string[], prefix?: string, commands?: Collection<string, ICommand>, dbService?: MongoService) {
async execute(
message: Discord.Message,
args: string[],
_prefix?: string,
_commands?: Collection<string, ICommand>,
dbService?: MongoService,
) {
// Only certain users can use this command
// TODO: Better handling of permissions for commands in a generic way
const permittedRoles = ['staff', 'mod', 'bot-devs']
const isPermitted = message.member.roles.cache.some(r => permittedRoles.indexOf(r.name) !== -1);
const permittedRoles = new Set(['staff', 'mod', 'bot-devs']);
const isPermitted = message.member.roles.cache.some((r) => permittedRoles.has(r.name));
if (!isPermitted) {
return message.author.send('Sorry but I can\'t let you add greetings!');
return message.author.send("Sorry but I can't let you add greetings!");
}
// Can't do much without a message
@@ -39,11 +46,11 @@ const command: ICommand = {
const result = await dbService.insert(message.author.id, 'greetings', [{ message: greetingStr }]);
if (!result) {
message.author.send('Uh-oh! Couldn\'t add that greeting!');
return message.author.send("Uh-oh! Couldn't add that greeting!");
}
message.author.send('I\'ve added the greeting you told me about!');
return message.author.send("I've added the greeting you told me about!");
},
};
export default command;
export default command;

View File

@@ -1,25 +1,29 @@
import Discord, { Collection } from 'discord.js';
import ICommand from './ICommand';
import ConfigService from '../../core/services/config.service'
import ConfigService from '../../core/services/config.service';
import LoggerService from '../../core/services/logger.service';
import container from '../../inversity.config';
const prefix = container.resolve<ConfigService>(ConfigService).get('BOT_TRIGGER');
const tmpPrefix = container.resolve<ConfigService>(ConfigService).get('BOT_TRIGGER');
const loggerService = container.resolve<LoggerService>(LoggerService);
const command: ICommand = {
name: 'help',
aliases: ['commands'],
example: `\`${prefix}help ping\``,
example: `\`${tmpPrefix}help ping\``,
description: 'Lists available commands!',
async execute(message: Discord.Message, args: string[], prefix: string, commands: Collection<string, ICommand>) {
const data = [];
if (!args.length) {
if (!args || args.length === 0) {
// Get for all commands
data.push('here\'s a list of all my commands:\n');
data.push("here's a list of all my commands:\n");
const cmds = commands.map(command => command.name);
let cmd;
cmds.forEach(element => {
cmd = commands.get(element) || commands.find(c => c.aliases && c.aliases.includes(element));
const cmds = commands.map((c) => c.name);
cmds.forEach((element) => {
const cmd = commands.get(element) || commands.find((c) => c.aliases && c.aliases.includes(element));
let response = `\`${prefix}${cmd.name}\` `;
if (cmd.description) {
response += `**${cmd.description}** `;
@@ -34,30 +38,31 @@ const command: ICommand = {
} else {
// Get description of single command
const name = args[0].toLowerCase();
const command = commands.get(name) || commands.find(c => c.aliases && c.aliases.includes(name));
const cmd = commands.get(name) || commands.find((c) => c.aliases && c.aliases.includes(name));
if (!command) {
message.reply('that\'s not a valid command!');
if (!cmd) {
message.reply("that's not a valid command!");
} else {
data.push(`**Name:** ${command.name}`);
data.push(`**Name:** ${cmd.name}`);
if (command.aliases) data.push(`**Aliases:** ${command.aliases.join(', ')}`);
if (command.description) data.push(`**Description:** ${command.description}`);
if (cmd.aliases) {
data.push(`**Aliases:** ${cmd.aliases.join(', ')}`);
}
if (cmd.description) {
data.push(`**Description:** ${cmd.description}`);
}
}
}
try {
//await message.author.send(data, { split: true });
//if (message.channel.type === 'dm')
// return;
// message.reply('I\'ve sent you a DM with all my commands!');
message.reply(data, { split: true })
}
catch (error) {
console.error(`Could not send help DM to ${message.author.tag}.\n`, error);
message.reply('it seems like I can\'t DM you! Do you have DMs disabled?');
return message.reply(data, { split: true });
} catch (error) {
loggerService.log('error', `Could not send help DM to ${message.author.tag}.\n`, error);
return message.reply("it seems like I can't DM you! Do you have DMs disabled?");
}
},
};
export default command;
export default command;

View File

@@ -1,13 +1,7 @@
import magicball from './8ball';
import addgreeting from './addgreeting';
import help from './help';
import ping from './ping';
import see from './see';
import magicball from './8ball';
import addgreeting from './addgreeting'
export default [
help,
ping,
see,
magicball,
addgreeting
]
export default [help, ping, see, magicball, addgreeting];

View File

@@ -5,9 +5,9 @@ const command: ICommand = {
name: 'ping',
aliases: ['hello', 'hi'],
description: 'Responds, kind of like telling you the bot is alive.',
async execute(message: Discord.Message, args: string[]) {
message.reply('Pong!');
async execute(message: Discord.Message) {
return message.reply('Pong!');
},
};
export default command;
export default command;

View File

@@ -1,17 +1,21 @@
import Discord from 'discord.js';
import ICommand from './ICommand';
import ConfigService from '../../core/services/config.service'
import ConfigService from '../../core/services/config.service';
import container from '../../inversity.config';
import ICommand from './ICommand';
const prefix = container.resolve<ConfigService>(ConfigService).get('BOT_TRIGGER');
const command: ICommand = {
name: 'see',
example: `\`${prefix}see\``,
description: 'Sends a DM telling you information about your user on given server.',
async execute(message: Discord.Message, args: string[]) {
message.author.send(`Server: ${message.guild.name}\nYour username: ${message.author.username}\nYour ID: ${message.author.id}`);
async execute(message: Discord.Message) {
return message.author.send(
`Server: ${message.guild.name}\nYour username: ${message.author.username}\nYour ID: ${message.author.id}`,
);
},
};
export default command;
export default command;

View File

@@ -1,10 +1,10 @@
import path from 'path';
import dotenv from 'dotenv';
import { injectable } from "inversify";
import { injectable } from 'inversify';
import path from 'path';
import Config from '../types/Config';
import LogLevel from '../types/LogLevel';
import Environment from '../types/Environment';
import LogLevel from '../types/LogLevel';
@injectable()
export default class ConfigService {
@@ -21,12 +21,9 @@ export default class ConfigService {
}
constructor() {
this.load();
this.setup();
}
private load(): void {
dotenv.config({ path: path.resolve(__dirname, '../../', '.env') });
this.setup();
}
private setup(): void {
@@ -40,7 +37,7 @@ export default class ConfigService {
};
}
public get(key: keyof Config): any {
public get(key: keyof Config): string {
return this.config[key];
}
}

View File

@@ -1,9 +1,10 @@
import { injectable } from 'inversify';
import path from 'path';
import winston from 'winston';
import { injectable } from 'inversify';
import LogLevel from '../types/LogLevel';
// eslint-disable-next-line import/no-cycle
import container from '../../inversity.config';
import LogLevel from '../types/LogLevel';
import ConfigService from './config.service';
@@ -18,7 +19,10 @@ export default class LoggerService {
level: this._configService.get('LOG_LEVEL'),
format: winston.format.json(),
transports: [
new winston.transports.File({ filename: path.resolve(__dirname, '../../', 'logs', 'error.log'), level: 'error' }),
new winston.transports.File({
filename: path.resolve(__dirname, '../../', 'logs', 'error.log'),
level: 'error',
}),
new winston.transports.Console({
format: winston.format.simple(),
}),
@@ -26,7 +30,7 @@ export default class LoggerService {
});
}
log(level: LogLevel, message: string, payload?: object) {
log(level: LogLevel, message: string, payload?: unknown): void {
if (this._logger[level]) {
this._logger.log(level, message, payload);
} else {

View File

@@ -1,18 +1,21 @@
import mongodb from 'mongodb';
import { injectable } from "inversify";
import assert from 'assert';
import { injectable } from 'inversify';
import mongodb, { Collection, CollectionInsertOneOptions } from 'mongodb';
// eslint-disable-next-line import/no-cycle
import container from '../../inversity.config';
import ConfigService from './config.service';
// eslint-disable-next-line import/no-cycle
import LoggerService from './logger.service';
@injectable()
export default class MongoService {
private _configService: ConfigService = container.resolve<ConfigService>(ConfigService);
private _loggerService: LoggerService = container.resolve<LoggerService>(LoggerService);
private _mongoClient: mongodb.MongoClient;
public _db: mongodb.Db;
public async connect(): Promise<void> {
@@ -34,7 +37,7 @@ export default class MongoService {
});
}
public disconnect() {
public disconnect(): void {
if (this._mongoClient && this._mongoClient.isConnected) {
this._loggerService.log('info', 'Closing connection to MongoDB');
this._mongoClient.close();
@@ -51,7 +54,7 @@ export default class MongoService {
*
* @example findOne('123456', 'collectionName', { ident: 'generated-slug' })
*/
public async findOne(userId: string, collection: string, query: any) {
public async findOne(userId: string, collection: string, query: unknown): Promise<Collection | boolean> {
try {
this.verifyConnection();
@@ -65,10 +68,10 @@ export default class MongoService {
});
return resp;
} catch (err) {
} catch (error) {
this._loggerService.log('error', 'Could not find document', {
userId,
error: err,
error,
collection,
query,
});
@@ -87,7 +90,7 @@ export default class MongoService {
*
* @example find('123456', 'collectionName', { key: 'value' })
*/
public async find(userId: string, collection: string, query: any) {
public async find(userId: string, collection: string, query: unknown): Promise<Collection[]> {
try {
this.verifyConnection();
@@ -101,10 +104,10 @@ export default class MongoService {
});
return resp;
} catch (err) {
} catch (error) {
this._loggerService.log('error', 'Could not find documents', {
userId,
error: err,
error,
collection,
query,
});
@@ -123,7 +126,7 @@ export default class MongoService {
*
* @example `insert('123456', 'collectionName', [{ body: 'This is a document!' }])`
*/
public async insert(userId: string, collection: string, payload: any[]) {
public async insert(userId: string, collection: string, payload: unknown[]): Promise<boolean> {
try {
this.verifyConnection();
@@ -137,16 +140,16 @@ export default class MongoService {
userId,
collection,
payload,
resp
resp,
});
return true;
} catch (err) {
} catch (error) {
this._loggerService.log('error', 'Could not insert documents to database', {
userId,
error: err,
error,
collection,
payload
payload,
});
return false;
@@ -164,7 +167,7 @@ export default class MongoService {
*
* @example `updateMany('123456', 'collectionName', { ident: 'generated-slug' }, { secondKey: 'new data'})`
*/
public async updateMany(userId: string, collection: string, query: any, payload: any) {
public async updateMany(userId: string, collection: string, query: unknown, payload: unknown[]): Promise<boolean> {
try {
this.verifyConnection();
@@ -179,18 +182,20 @@ export default class MongoService {
collection,
query,
payload,
resp
resp,
});
return true;
} catch (err) {
} catch (error) {
this._loggerService.log('error', 'Could not update documents', {
userId,
collection,
query,
payload,
err
error,
});
return false;
}
}
@@ -204,7 +209,7 @@ export default class MongoService {
*
* @example `deleteMany('123456', 'collectionName', { ident: 'generated-slug' })`
*/
public async deleteMany(userId: string, collection: string, query: any) {
public async deleteMany(userId: string, collection: string, query: unknown): Promise<boolean> {
try {
this.verifyConnection();
@@ -218,23 +223,23 @@ export default class MongoService {
userId,
collection,
query,
resp
resp,
});
return true;
} catch (err) {
} catch (error) {
this._loggerService.log('error', 'Could not delete documents', {
userId,
collection,
query,
err
error,
});
return false
return false;
}
}
private verifyConnection() {
private verifyConnection(): void {
if (!this._mongoClient.isConnected) {
this._loggerService.log('error', 'No database connection available');
throw new Error('No database connection available');

View File

@@ -1,5 +1,5 @@
import LogLevel from "./LogLevel";
import Environment from "./Environment";
import Environment from './Environment';
import LogLevel from './LogLevel';
export default interface Config {
BOT_TRIGGER: string;

View File

@@ -1,3 +1,3 @@
type Environment = 'production' | 'development' | 'test';
export default Environment;
export default Environment;

View File

@@ -1,3 +1,3 @@
type LogLevel = 'error' | 'warn' | 'info' | 'verbose' | 'debug' | 'silly';
export default LogLevel;
export default LogLevel;

View File

@@ -1,12 +1,14 @@
import { Container } from "inversify";
import { Container } from 'inversify';
import ConfigService from "./core/services/config.service";
import LoggerService from "./core/services/logger.service";
import MongoService from "./core/services/mongo.service";
import ConfigService from './core/services/config.service';
// eslint-disable-next-line import/no-cycle
import LoggerService from './core/services/logger.service';
// eslint-disable-next-line import/no-cycle
import MongoService from './core/services/mongo.service';
const container = new Container();
container.bind<ConfigService>(ConfigService).toSelf();
container.bind<LoggerService>(LoggerService).toSelf();
container.bind<MongoService>(MongoService).toSelf();
export default container;
export default container;

View File

@@ -1,31 +1,31 @@
import { exit } from 'process';
import 'reflect-metadata';
import App from './app';
import { exit } from 'process';
const app = new App();
function exitHandler(): void {
// Cleans up the application
app.exit();
exit();
}
try {
app.init();
function exitHandler() {
// Cleans up the application
app.exit();
exit();
}
// do something when app is closing
process.on('exit', exitHandler);
//catches ctrl+c event
// catches ctrl+c event
process.on('SIGINT', exitHandler);
// catches "kill pid" (for example: nodemon restart)
process.on('SIGUSR1', exitHandler);
process.on('SIGUSR2', exitHandler);
//catches uncaught exceptions
// catches uncaught exceptions
process.on('uncaughtException', exitHandler);
} catch (e) {
} catch {
exit(1);
}
}

View File

@@ -1,16 +0,0 @@
{
"defaultSeverity": "error",
"extends": ["tslint:latest", "tslint-react", "tslint-config-prettier"],
"jsRules": {},
"rules": {
"interface-name": false,
"semicolon": [true, "always"],
"member-access": [false],
"ordered-imports": [true],
"no-console": [false],
"no-var-requires": true,
"quotemark": [true, "single", "jsx-double"],
"no-implicit-dependencies": [true, "dev"]
},
"rulesDirectory": []
}

3337
yarn.lock

File diff suppressed because it is too large Load Diff