From c605d68a5e73214499fe5c09446c40ca0df8e8cd Mon Sep 17 00:00:00 2001 From: fourjr <28086837+fourjr@users.noreply.github.com> Date: Sun, 1 Nov 2020 17:55:11 +0800 Subject: [PATCH] Force push to dev --- .env.example | 3 +- .../CODE_OF_CONDUCT.md | 0 CONTRIBUTING.md => .github/CONTRIBUTING.md | 17 +- .github/ISSUE_TEMPLATE/command-request.md | 2 +- .github/ISSUE_TEMPLATE/feature_request.md | 2 +- .github/workflows/lints.yml | 2 +- .github/workflows/stale.yml | 17 - .lint.py | 17 - .pylintrc | 511 ------- CHANGELOG.md | 333 +++-- Pipfile | 14 +- Pipfile.lock | 621 +++++---- Procfile | 2 +- README.md | 50 +- SPONSORS.json | 37 - app.json | 58 +- bot.py | 266 ++-- cogs/modmail.py | 385 +++--- cogs/plugins.py | 184 ++- cogs/utility.py | 438 +++--- core/clients.py | 180 ++- core/config.py | 11 +- core/config_help.json | 14 +- core/paginator.py | 2 +- core/thread.py | 64 +- core/time.py | 2 +- core/translations.py | 25 - languages/en.csv | 1172 ----------------- modmail.sh | 3 + plugins/registry.json | 133 +- poetry.lock | 157 ++- pyproject.toml | 20 +- requirements.min.txt | 28 +- runtime.txt | 2 +- translation_files.py | 127 -- 35 files changed, 1638 insertions(+), 3261 deletions(-) rename CODE_OF_CONDUCT.md => .github/CODE_OF_CONDUCT.md (100%) rename CONTRIBUTING.md => .github/CONTRIBUTING.md (83%) delete mode 100644 .github/workflows/stale.yml delete mode 100644 .lint.py delete mode 100644 .pylintrc delete mode 100644 core/translations.py delete mode 100644 languages/en.csv create mode 100644 modmail.sh delete mode 100644 translation_files.py diff --git a/.env.example b/.env.example index b3554bf9c9..44c91c59c7 100644 --- a/.env.example +++ b/.env.example @@ -1,5 +1,6 @@ TOKEN=MyBotToken LOG_URL=https://logviewername.herokuapp.com/ GUILD_ID=1234567890 +MODMAIL_GUILD_ID=1234567890 OWNERS=Owner1ID,Owner2ID,Owner3ID -MONGO_URI=mongodb+srv://mongodburi +CONNECTION_URI=mongodb+srv://mongodburi diff --git a/CODE_OF_CONDUCT.md b/.github/CODE_OF_CONDUCT.md similarity index 100% rename from CODE_OF_CONDUCT.md rename to .github/CODE_OF_CONDUCT.md diff --git a/CONTRIBUTING.md b/.github/CONTRIBUTING.md similarity index 83% rename from CONTRIBUTING.md rename to .github/CONTRIBUTING.md index a95e344610..568e5f2175 100644 --- a/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -15,33 +15,32 @@ We use GitHub to host code, to track issues and feature requests, as well as acc ## We Use [Git Flow](https://atlassian.com/git/tutorials/comparing-workflows/gitflow-workflow) ![Simple Image Of A Git Flow Workflow](https://nvie.com/img/hotfix-branches@2x.png) -When contributing to this project please make sure you follow this and name your branches appropriately! +When contributing to this project, please make sure you follow this and name your branches appropriately! ## All Code Changes Happen Through Pull Requests Make sure you know how Git Flow works before contributing! Pull requests are the best way to propose changes to the codebase. We actively welcome your pull requests: 1. Fork the repo and create your branch from `master` or `development` according to Git Flow. -2. If you've added code that should be tested, add tests. -3. If you've changed APIs, update the documentation. -4. Ensure the test suite passes. -5. Make sure your code lints. -6. Issue that pull request! +2. Update the CHANGELOG. +3. If you've changed `core/*` or `bot.py`, mark changelog as "BREAKING" since plugins may break. +4. Make sure your code passes the lint checks. +5. Create Issues and pull requests! ## Any contributions you make will be under the GNU Affero General Public License v3.0 In short, when you submit code changes, your submissions are understood to be under the same [GNU Affero General Public License v3.0](https://www.gnu.org/licenses/agpl-3.0.en.html) that covers the project. Feel free to contact the maintainers if that's a concern. ## Report bugs using [Github Issues](https://github.com/kyb3r/modmail/issues) -We use GitHub issues to track public bugs. Report a bug by [opening a new issue](https://github.com/kyb3r/modmail/issues/new); it's that easy! +We use GitHub issues to track public bugs. Report a bug by [opening a new Issue](https://github.com/kyb3r/modmail/issues/new); it's that easy! ## Write bug reports with detail, background, and sample code **Great Bug Reports** tend to have: -- A quick summary and/or background +- A quick summary and background - Steps to reproduce - Be specific! - What you expected would happen -- What actually happens +- What *actually* happens - Notes (possibly including why you think this might be happening, or stuff you tried that didn't work) diff --git a/.github/ISSUE_TEMPLATE/command-request.md b/.github/ISSUE_TEMPLATE/command-request.md index 44902f2cf3..d3c1673a6b 100644 --- a/.github/ISSUE_TEMPLATE/command-request.md +++ b/.github/ISSUE_TEMPLATE/command-request.md @@ -1,7 +1,7 @@ --- name: Command request about: Request a new command -title: "[COMMAND-REQUEST] your title here" +title: "your title here" labels: command-request assignees: '' diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md index 48b986344d..7cd7a506cd 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.md +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -1,7 +1,7 @@ --- name: Feature request about: Suggest an idea for this project -title: "[FEATURE-REQUEST] your title here" +title: "your title here" labels: feature-request assignees: '' diff --git a/.github/workflows/lints.yml b/.github/workflows/lints.yml index c84c9af773..52a538bef7 100644 --- a/.github/workflows/lints.yml +++ b/.github/workflows/lints.yml @@ -31,4 +31,4 @@ jobs: continue-on-error: true - name: Black and flake8 run: | - black . --check + black . --diff diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml deleted file mode 100644 index b3003ccf92..0000000000 --- a/.github/workflows/stale.yml +++ /dev/null @@ -1,17 +0,0 @@ -name: "Close Stale Issues" - -on: - schedule: - - cron: "0 0 * * *" - -jobs: - stale: - runs-on: ubuntu-latest - steps: - - uses: actions/stale@v1 - with: - repo-token: ${{ secrets.GITHUB_TOKEN }} - stale-issue-message: 'This issue is stale because it has been open for 100 days with no activity. Remove stale label or comment or this will be closed in 5 days. Please do not un-stale this issue unless it carries significant contribution.' - days-before-stale: 100 - days-before-close: 5 - exempt-issue-label: 'high priority' diff --git a/.lint.py b/.lint.py deleted file mode 100644 index 9d29372fd8..0000000000 --- a/.lint.py +++ /dev/null @@ -1,17 +0,0 @@ -if __name__ == "__main__": - import sys - from os import listdir - from os.path import join - - from pylint.lint import Run - - THRESHOLD = 9.75 - - cogs = [join("cogs", c) for c in listdir("cogs") if c.endswith(".py")] - core = [join("core", c) for c in listdir("core") if c.endswith(".py")] - - results = Run(["bot.py", *cogs, *core], do_exit=False) - - score = results.linter.stats["global_note"] - if score <= THRESHOLD: - sys.exit(1) diff --git a/.pylintrc b/.pylintrc deleted file mode 100644 index 21087a91f7..0000000000 --- a/.pylintrc +++ /dev/null @@ -1,511 +0,0 @@ -[MASTER] - -# A comma-separated list of package or module names from where C extensions may -# be loaded. Extensions are loading into the active Python interpreter and may -# run arbitrary code. -extension-pkg-whitelist= - -# Add files or directories to the blacklist. They should be base names, not -# paths. -ignore=CVS - -# Add files or directories matching the regex patterns to the blacklist. The -# regex matches against base names, not paths. -ignore-patterns= - -# Python code to execute, usually for sys.path manipulation such as -# pygtk.require(). -#init-hook= - -# Use multiple processes to speed up Pylint. Specifying 0 will auto-detect the -# number of processors available to use. -jobs=0 - -# Control the amount of potential inferred values when inferring a single -# object. This can help the performance when dealing with large functions or -# complex, nested conditions. -limit-inference-results=100 - -# List of plugins (as comma separated values of python modules names) to load, -# usually to register additional checkers. -load-plugins= - -# Pickle collected data for later comparisons. -persistent=yes - -# Specify a configuration file. -#rcfile= - -# When enabled, pylint would attempt to guess common misconfiguration and emit -# user-friendly hints instead of false-positive error messages. -suggestion-mode=yes - -# Allow loading of arbitrary C extensions. Extensions are imported into the -# active Python interpreter and may run arbitrary code. -unsafe-load-any-extension=no - - -[MESSAGES CONTROL] - -# Only show warnings with the listed confidence levels. Leave empty to show -# all. Valid levels: HIGH, INFERENCE, INFERENCE_FAILURE, UNDEFINED. -confidence= - -# Disable the message, report, category or checker with the given id(s). You -# can either give multiple identifiers separated by comma (,) or put this -# option multiple times (only on the command line, not in the configuration -# file where it should appear only once). You can also use "--disable=all" to -# disable everything first and then reenable specific checks. For example, if -# you want to run only the similarities checker, you can use "--disable=all -# --enable=similarities". If you want to run only the classes checker, but have -# no Warning level messages displayed, use "--disable=all --enable=classes -# --disable=W". -disable=raw-checker-failed, - bad-inline-option, - locally-disabled, - file-ignored, - suppressed-message, - useless-suppression, - deprecated-pragma, - use-symbolic-message-instead, - metaclass-assignment, - missing-docstring, # No doc-string - no-name-in-module, # No name 'file' in module 'core' - fixme, - too-many-public-methods, - too-many-locals, - too-many-statements, - too-many-branches, - too-many-instance-attributes, - too-many-arguments, - too-few-public-methods, - too-many-lines, - line-too-long, - bad-continuation, - invalid-name, - logging-too-many-args - - -# Enable the message, report, category or checker with the given id(s). You can -# either give multiple identifier separated by comma (,) or put this option -# multiple time (only on the command line, not in the configuration file where -# it should appear only once). See also the "--disable" option for examples. -enable=c-extension-no-member - - -[REPORTS] - -# Python expression which should return a note less than 10 (10 is the highest -# note). You have access to the variables errors warning, statement which -# respectively contain the number of errors / warnings messages and the total -# number of statements analyzed. This is used by the global evaluation report -# (RP0004). -evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10) - -# Template used to display messages. This is a python new-style format string -# used to format the message information. See doc for all details. -#msg-template= - -# Set the output format. Available formats are text, parseable, colorized, json -# and msvs (visual studio). You can also give a reporter class, e.g. -# mypackage.mymodule.MyReporterClass. -output-format=text - -# Tells whether to display a full report or only the messages. -reports=no - -# Activate the evaluation score. -score=yes - - -[REFACTORING] - -# Maximum number of nested blocks for function / method body -max-nested-blocks=5 - -# Complete name of functions that never returns. When checking for -# inconsistent-return-statements if a never returning function is called then -# it will be considered as an explicit return statement and no message will be -# printed. -never-returning-functions=sys.exit - - -[LOGGING] - -# Format style used to check logging format string. `old` means using % -# formatting, while `new` is for `{}` formatting. -logging-format-style=new - -# Logging modules to check that the string format arguments are in logging -# function parameter format. -logging-modules=logging - - -[SPELLING] - -# Limits count of emitted suggestions for spelling mistakes. -max-spelling-suggestions=4 - -# Spelling dictionary name. Available dictionaries: none. To make it working -# install python-enchant package.. -spelling-dict= - -# List of comma separated words that should not be checked. -spelling-ignore-words= - -# A path to a file that contains private dictionary; one word per line. -spelling-private-dict-file= - -# Tells whether to store unknown words to indicated private dictionary in -# --spelling-private-dict-file option instead of raising a message. -spelling-store-unknown-words=no - - -[MISCELLANEOUS] - -# List of note tags to take in consideration, separated by a comma. -notes=FIXME, - TODO, - BUG - - -[TYPECHECK] - -# List of decorators that produce context managers, such as -# contextlib.contextmanager. Add to this list to register other decorators that -# produce valid context managers. -contextmanager-decorators=contextlib.contextmanager - -# List of members which are set dynamically and missed by pylint inference -# system, and so shouldn't trigger E1101 when accessed. Python regular -# expressions are accepted. -generated-members= - -# Tells whether missing members accessed in mixin class should be ignored. A -# mixin class is detected if its name ends with "mixin" (case insensitive). -ignore-mixin-members=yes - -# Tells whether to warn about missing members when the owner of the attribute -# is inferred to be None. -ignore-none=yes - -# This flag controls whether pylint should warn about no-member and similar -# checks whenever an opaque object is returned when inferring. The inference -# can return multiple potential results while evaluating a Python object, but -# some branches might not be evaluated, which results in partial inference. In -# that case, it might be useful to still emit no-member and other checks for -# the rest of the inferred objects. -ignore-on-opaque-inference=yes - -# List of class names for which member attributes should not be checked (useful -# for classes with dynamically set attributes). This supports the use of -# qualified names. -ignored-classes=optparse.Values,thread._local,_thread._local - -# List of module names for which member attributes should not be checked -# (useful for modules/projects where namespaces are manipulated during runtime -# and thus existing member attributes cannot be deduced by static analysis. It -# supports qualified module names, as well as Unix pattern matching. -ignored-modules= - -# Show a hint with possible names when a member name was not found. The aspect -# of finding the hint is based on edit distance. -missing-member-hint=yes - -# The minimum edit distance a name should have in order to be considered a -# similar match for a missing member name. -missing-member-hint-distance=1 - -# The total number of similar names that should be taken in consideration when -# showing a hint for a missing member. -missing-member-max-choices=1 - - -[VARIABLES] - -# List of additional names supposed to be defined in builtins. Remember that -# you should avoid defining new builtins when possible. -additional-builtins= - -# Tells whether unused global variables should be treated as a violation. -allow-global-unused-variables=yes - -# List of strings which can identify a callback function by name. A callback -# name must start or end with one of those strings. -callbacks=cb_, - _cb - -# A regular expression matching the name of dummy variables (i.e. expected to -# not be used). -dummy-variables-rgx=_+$|(_[a-zA-Z0-9_]*[a-zA-Z0-9]+?$)|dummy|^ignored_|^unused_ - -# Argument names that match this expression will be ignored. Default to name -# with leading underscore. -ignored-argument-names=_.*|^ignored_|^unused_ - -# Tells whether we should check for unused import in __init__ files. -init-import=no - -# List of qualified module names which can have objects that can redefine -# builtins. -redefining-builtins-modules=six.moves,past.builtins,future.builtins,builtins,io - - -[FORMAT] - -# Expected format of line ending, e.g. empty (any line ending), LF or CRLF. -expected-line-ending-format= - -# Regexp for a line that is allowed to be longer than the limit. -ignore-long-lines=^\s*(# )??$ - -# Number of spaces of indent required inside a hanging or continued line. -indent-after-paren=4 - -# String used as indentation unit. This is usually " " (4 spaces) or "\t" (1 -# tab). -indent-string=' ' - -# Maximum number of characters on a single line. -max-line-length=99 - -# Maximum number of lines in a module. -max-module-lines=1000 - -# List of optional constructs for which whitespace checking is disabled. `dict- -# separator` is used to allow tabulation in dicts, etc.: {1 : 1,\n222: 2}. -# `trailing-comma` allows a space between comma and closing bracket: (a, ). -# `empty-line` allows space-only lines. -no-space-check=trailing-comma, - dict-separator - -# Allow the body of a class to be on the same line as the declaration if body -# contains single statement. -single-line-class-stmt=no - -# Allow the body of an if to be on the same line as the test if there is no -# else. -single-line-if-stmt=no - - -[SIMILARITIES] - -# Ignore comments when computing similarities. -ignore-comments=yes - -# Ignore docstrings when computing similarities. -ignore-docstrings=yes - -# Ignore imports when computing similarities. -ignore-imports=no - -# Minimum lines number of a similarity. -min-similarity-lines=4 - - -[BASIC] - -# Naming style matching correct argument names. -argument-naming-style=snake_case - -# Regular expression matching correct argument names. Overrides argument- -# naming-style. -#argument-rgx= - -# Naming style matching correct attribute names. -attr-naming-style=snake_case - -# Regular expression matching correct attribute names. Overrides attr-naming- -# style. -#attr-rgx= - -# Bad variable names which should always be refused, separated by a comma. -bad-names=foo, - bar, - baz, - toto, - tutu, - tata - -# Naming style matching correct class attribute names. -class-attribute-naming-style=any - -# Regular expression matching correct class attribute names. Overrides class- -# attribute-naming-style. -#class-attribute-rgx= - -# Naming style matching correct class names. -class-naming-style=PascalCase - -# Regular expression matching correct class names. Overrides class-naming- -# style. -#class-rgx= - -# Naming style matching correct constant names. -const-naming-style=UPPER_CASE - -# Regular expression matching correct constant names. Overrides const-naming- -# style. -#const-rgx= - -# Minimum line length for functions/classes that require docstrings, shorter -# ones are exempt. -docstring-min-length=-1 - -# Naming style matching correct function names. -function-naming-style=snake_case - -# Regular expression matching correct function names. Overrides function- -# naming-style. -#function-rgx= - -# Good variable names which should always be accepted, separated by a comma. -good-names=i, - j, - ex, - _, - id, - db, - f, - dt, - ch, - ts - -# Include a hint for the correct naming format with invalid-name. -include-naming-hint=yes - -# Naming style matching correct inline iteration names. -inlinevar-naming-style=any - -# Regular expression matching correct inline iteration names. Overrides -# inlinevar-naming-style. -#inlinevar-rgx= - -# Naming style matching correct method names. -method-naming-style=snake_case - -# Regular expression matching correct method names. Overrides method-naming- -# style. -#method-rgx= - -# Naming style matching correct module names. -module-naming-style=snake_case - -# Regular expression matching correct module names. Overrides module-naming- -# style. -#module-rgx= - -# Colon-delimited sets of names that determine each other's naming style when -# the name regexes allow several styles. -name-group= - -# Regular expression which should only match function or class names that do -# not require a docstring. -no-docstring-rgx=^_ - -# List of decorators that produce properties, such as abc.abstractproperty. Add -# to this list to register other decorators that produce valid properties. -# These decorators are taken in consideration only for invalid-name. -property-classes=abc.abstractproperty - -# Naming style matching correct variable names. -variable-naming-style=snake_case - -# Regular expression matching correct variable names. Overrides variable- -# naming-style. -#variable-rgx= - - -[IMPORTS] - -# Allow wildcard imports from modules that define __all__. -allow-wildcard-with-all=no - -# Analyse import fallback blocks. This can be used to support both Python 2 and -# 3 compatible code, which means that the block might have code that exists -# only in one or another interpreter, leading to false positives when analysed. -analyse-fallback-blocks=no - -# Deprecated modules which should not be used, separated by a comma. -deprecated-modules=optparse,tkinter.tix - -# Create a graph of external dependencies in the given file (report RP0402 must -# not be disabled). -ext-import-graph= - -# Create a graph of every (i.e. internal and external) dependencies in the -# given file (report RP0402 must not be disabled). -import-graph= - -# Create a graph of internal dependencies in the given file (report RP0402 must -# not be disabled). -int-import-graph= - -# Force import order to recognize a module as part of the standard -# compatibility libraries. -known-standard-library= - -# Force import order to recognize a module as part of a third party library. -known-third-party=enchant - - -[CLASSES] - -# List of method names used to declare (i.e. assign) instance attributes. -defining-attr-methods=__init__, - __new__, - setUp - -# List of member names, which should be excluded from the protected access -# warning. -exclude-protected=_asdict, - _fields, - _replace, - _source, - _make - -# List of valid names for the first argument in a class method. -valid-classmethod-first-arg=cls - -# List of valid names for the first argument in a metaclass class method. -valid-metaclass-classmethod-first-arg=cls - - -[DESIGN] - -# Maximum number of arguments for function / method. -max-args=5 - -# Maximum number of attributes for a class (see R0902). -max-attributes=7 - -# Maximum number of boolean expressions in an if statement. -max-bool-expr=5 - -# Maximum number of branch for function / method body. -max-branches=12 - -# Maximum number of locals for function / method body. -max-locals=15 - -# Maximum number of parents for a class (see R0901). -max-parents=7 - -# Maximum number of public methods for a class (see R0904). -max-public-methods=20 - -# Maximum number of return / yield for function / method body. -max-returns=6 - -# Maximum number of statements in function / method body. -max-statements=50 - -# Minimum number of public methods for a class (see R0903). -min-public-methods=2 - - -[EXCEPTIONS] - -# Exceptions that will emit a warning when being caught. Defaults to -# "Exception". -overgeneral-exceptions=BaseException diff --git a/CHANGELOG.md b/CHANGELOG.md index 16c89216f4..57b9d415b2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,12 +4,93 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). This project mostly adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html); -however, insignificant breaking changes does not guarantee a major version bump, see the reasoning [here](https://github.com/kyb3r/modmail/issues/319). +however, insignificant breaking changes do not guarantee a major version bump, see the reasoning [here](https://github.com/kyb3r/modmail/issues/319). If you're a plugins developer, note the "BREAKING" section. +# v3.6.3-dev0 -# v3.5.0-dev0 +### Improved + +- Plugins installations have clearer error messages + +# v3.6.2 + +### Fixed + +- Plugins downloading requirements in virtual environments + + +# v3.6.1 + +### Added + +- Proper error message if privileged intents not explicitly granted to bot. + + +# v3.6.0 + +### Added + +- Added `thread_move_title` to specify title of thread moved embed. +- Mark NSFW logs in log message. ([GH #2792](https://github.com/kyb3r/modmail/issues/2792)) +- Icon for moderator that closed the thread in log message. ([GH #2828](https://github.com/kyb3r/modmail/issues/2828)) +- Ability to set mentions via user/role ID. ([GH #2796](https://github.com/kyb3r/modmail/issues/2796)) + +### Changed + +- `?move` now consumes rest in category name, which means `?move Long Category Name` works without quotes! +- `?help` shows "No command description" if no description provided. ([PR #2845](https://github.com/kyb3r/modmail/pull/2845)) + +### Fixed +- Unicode errors raised during windows selfhosting + +### Internal + +- Bump discord.py version to 1.5.1 +- Explicitly state intents used for connection +- Use `--diff` for black CI instead of `--check` ([GH #2816](https://github.com/kyb3r/modmail/issues/2816)) + + +# v3.5.0 + +Fixed discord.py issue. + +### Added + +- A confirmation when you manually delete a thread message embed. +- Config var `enable_eval` defaults true, set `enable_eval=no` to disable the eval command. ([GH #2803](https://github.com/kyb3r/modmail/issues/2803)) +- Added `?plugins reset` command to completely reset everything related to plugins. This will fix some problems caused by broken plugins in the file system. +- Support private GitHub repos for plugins (thanks to @officialpiyush pr#2767) + +### Changed + +- Bump discord.py version to v1.3.3. +- Renamed `bot.owner_ids` to `bot.bot_owner_ids` as the attribute is now defined internally for team support. +- Deleting channel manually will now close the thread. +- Deleting messages will no longer cause the bot to produce warnings. +- Plugins will automatically be removed when it fails to load. +- Moved all database-related activities to clients.py under MongoDBClient, with possible future hook for additional database support. +- `bot.db` is deprecated in favour of `bot.api.db` and will be removed in the future. +- Deprecated `bot.plugin_db.get_partition` in favour of `bot.api.get_plugin_partition` (not final). +- Deprecated `MONGO_URI` config var (but will keep support in the future) in favour of `CONNECTION_URI` and `DATABASE_TYPE`. Right now there is one supported database - "mongodb", which is the default. + +### Fixed + +- Plugins not loading in Windows OS. Now uses proactor event loop for asyncio which should fix this. + + +# v3.4.1 + +### Fixed + +- Masked a bunch of noise errors when deleting messages. +- Added more checks for deleting messages. + +### Breaking + +- `thread_initiate` will be dispatched at the beginning of the setup process. +- `thread_create` is dispatched when the thread is registered as a thread by Modmail (i.e., when channel topic is edited). +- `thread_ready` is dispatched when a thread finishes its setup steps. -Translations WIP. # v3.4.0 @@ -26,35 +107,35 @@ Translations WIP. - Multi-command alias is now more stable. With support for a single quote escape `\"`. - New command `?freply`, which behaves exactly like `?reply` with the addition that you can substitute `{channel}`, `{recipient}`, and `{author}` to be their respective values. - New command `?repair`, repair any broken Modmail thread (with help from @officialpiyush). -- Recipient get feedback when they edit message. +- Recipients get feedback when they edit their messages. - Chained delete for DMs now comes with a message. - poetry (in case someone needs it). ### Changed - The look of alias and snippet when previewing. -- Message ID of the thread embed is saved in DB, instead of the original message. +- The database now saves the message ID of the thread embed, instead of the original message. - Swapped the position of user and category for `?contact`. - The log file will no longer grow infinitely large. -- Hard limit of maximum 25 steps for alias. +- A hard limit of a maximum of 25 steps for aliases. - `?disable` is now `?disable new`. ### Fixed - Setting config vars using human time wasn't working. - Fixed some bugs with aliases. -- Fixed a lot of issues with `?edit` and `?delete` and recipient message edit. +- Fixed many issues with `?edit` and `?delete` and recipient message edit. - Masked the error: "AttributeError: 'int' object has no attribute 'name'" - Channel delete event will not be checked until discord.py fixes this issue. -- Chained reaction add / remove. +- Chained reaction add/remove. - Chained delete for thread channels. ### Internal -- Commit to black format line width max = 99, consistent with pylint. -- Alias parser is rewritten without shlex. +- Commit to black format line width max = 99, consistent with PyLint. +- No longer requires shlex for alias parsing. - New checks with thread create / find. -- No more flake8 and travis. +- No more flake8 and Travis. # v3.3.2 @@ -78,19 +159,19 @@ Translations WIP. - Three new config vars: - `enable_plugins` (yes/no default yes) - - When set to no, plugins will not be loaded into the bot. + - When set to no, Modmail will not load plugins. - `error_color` (color format, defaults discord red) - The color of error messages. - `anon_reply_without_command` (yes/no default no) (Thanks to papiersnipper PR#288) - When set, all non-command messages sent to thread channels are forwarded to the recipient anonymously without the need of `?anonreply`. - This config takes precedence over `reply_without_command`. -- `?logs responded [user]` command, it will show all logs that the user has sent an reply. (Thanks to papiersnipper PR#288) +- `?logs responded [user]` command. It will show all the logs that the user has sent a reply. (Thanks to papiersnipper PR#288) - `user` when not provided, defaults to the user who ran the command. -- Open threads in limbo now auto closes if the channel cannot be found. This check is done every time the bot restarts. +- Open threads in limbo now auto-close if Modmail cannot find the channel. Modmail does this check every time the bot restarts. - Ability to disable new threads from getting created. - - `?disable` + - `?disable`. - Ability to fully disable Modmail DM. - - `?disable all` + - `?disable all`. - To re-enable DM: `?enable`, and to see the current status: `?isenable`. - This disabled Modmail interface is customizable with the following config vars: - `disabled_new_thread_title` @@ -104,33 +185,33 @@ Translations WIP. ### Changed -- `?contact` no longer send the "thread created" message to where the command is ran, instead, it's now sent to the newly created thread channel. (Thanks to DAzVise) +- `?contact` no longer send the "thread created" message to where the command was run, instead, it's now sent to the newly created thread channel. (Thanks to DAzVise) - Automatically delete notes command `?note` when there're no attachments attached. - Embed author links used to be inaccessible in many cases, now: - `?anonreply`, `?reply`, and `?note` in the thread channel will link to the sender's profile. - - `?reply` and recipient's DM will also link the sender's profile. + - `?reply` and the recipient's DM will also link the sender's profile. - `?anonreply` in DM channel will link to the first channel of the main guild. - Plugins update (mostly internal). - - `git` is no longer used to install plugins, it now downloads through zip files. + - `git` is no longer used to install plugins; it now downloads through zip files. - `?plugins enabled` renamed to `?plugins loaded` while `enabled` is still an alias to that command. - Reorganized plugins folder structure. - Logging / plugin-related messages change. - - Updating one plugin will not update all other plugins (plugins are no longer separated by repos, but the plugin name itself). -- Help command is in alphabetical order grouped by permissions. -- Notes are no longer always blurple, it's set to `MAIN_COLOR` now. + - Updating one plugin will not update other plugins; repositories no longer separate plugins, but the plugin name itself. +- The help command is in alphabetical order grouped by permissions. +- Notes are no longer always blurple; it's set to `MAIN_COLOR` now. - Added `?plugins update` for updating all installed plugins. - Reintroduce flake8 and use bandit for security issues detection. -- Add travis checks for 3.6 in Linux and 3.7 for MacOS and Windows. -- Eval commands are logged in debug logs. +- Add Travis checks for 3.6 in Linux and 3.7 for macOS and Windows. +- Debug logs not logs eval commands. - Presence updates 30 minutes instead of 45 now. -- Fixed an assortment of problems to do with block. +- Fixed an assortment of problems to do with `?block`. - Existing aliases can be used when creating new aliases. (Thanks to papiersnipper PR#402) ### Internal - Reworked `config.get` and `config.set`, it feeds through the converters before setting/getting. - To get/set the raw value, access through `config[]`. -- Prerelease naming scheme is now `x.x.x-devN`. +- The prerelease naming scheme is now `x.x.x-devN`. - `trigger_typing` has been moved to `core.utils.trigger_typing`, the original location is deprecated. - Simpler status and activity logic. - New logging logic. @@ -143,9 +224,9 @@ Security update! - Supporter permission users used to be able to "hack" snippets to reveal all your config vars, including your token and MongoURI. - Implemented some changes to address this bug: - - All customizable variables used in snippets, close messages, etc, using the `{}` syntax, now forbids chaining 2 or more attributes and attributes that starts with `_`. -- It is advised to update to this version. -- If you felt your credentials have been leaked, consider changing your bot token / mongo uri. + - All customizable variables used in snippets, close messages, etc., using the `{}` syntax, now forbids chaining two or more attributes and attributes that start with `_`. +- We advise you to update to this version. +- If you felt your credentials had been leaked, consider changing your bot token / MongoURI. # v3.2.1 @@ -175,7 +256,7 @@ Security update! ### Internal -- Use regex to parse Changes, Added, Fixed, etc and description. +- Use regex to parse Changes, Added, Fixed, etc. and description. - Adds `PermissionLevel.INVALID` when commands don't have a permission level. # v3.1.1 @@ -195,33 +276,33 @@ Security update! ### Added - `?sfw`, mark a thread as "safe for work", undos `?nsfw`. -- New config variable, `thread_auto_close_silently`, when set to a truthy value, no message will be sent when thread is auto-closed. +- New config variable, `thread_auto_close_silently`, when set to a truthy value, no message will be sent when a thread is auto-closed. - New configuration variable `thread_self_closable_creation_footer` β€” the footer when `recipient_thread_close` is enabled. - Added a minimalistic version of requirements.txt (named requirements.min.txt) that contains only the absolute minimum of Modmail. - For users having trouble with pipenv or any other reason. -- Multi-step alias, see `?help alias add`. Public beta testing, might be unstable. +- Multi-step alias, see `?help alias add`. Public beta testing might be unstable. - Misc commands without cogs are now displayed in `?help`. - `?help` works for alias and snippets. - `?config help ` shows a help embed for the configuration. -- Support setting permissions for sub commands. -- Support numbers (1-5) as substitutes for Permission Level REGULAR - OWNER in `?perms` sub commands. +- Support setting permissions for subcommands. +- Support numbers (1-5) as substitutes for Permission Level REGULAR - OWNER in `?perms` subcommands. ### Changes - `thread_auto_close_response` has a configurable variable `{timeout}`. - `?snippet` is now the default command name instead of `?snippets` (`?snippets` is still usable). This is to make this consistent with `?alias`/`?aliases`. -- `colorama` is no longer a necessity, this is due to some unsupported OS. -- Changelog command can now take a version argument to jump straight to specified version. +- `colorama` is no longer a necessity; this is due to some unsupported OS. +- Changelog command can now take a version argument to jump straight to the specified version. - `?plugin enabled` results are now sorted alphabetically. -- `?plugin registry` results are now sorted alphabetically, helps user find plugins more easily. +- `?plugin registry` results are now sorted alphabetically, helps users find plugins more easily. - `?plugin registry page-number` plugin registry can specify a page number for quick access. - A reworked interface for `?snippet` and `?alias`. - Add an `?snippet raw ` command for viewing the raw content of a snippet (escaped markdown). - - Add an `?alias raw ` command for viewing the raw content of a alias (escaped markdown). + - Add an `?alias raw ` command for displaying the raw content of an alias (escaped markdown). - The placeholder channel for the streaming status changed to https://www.twitch.tv/discordmodmail/. - Removed unclear `rm` alias for some `remove` commands. - Paginate `?config options`. -- All users configured with a permission level greater than REGULAR has access to the main Modmail category. +- All users configured with a permission level higher than REGULAR has access to the main Modmail category. - Category overrides also changes when a level is removed or added to a user or role. - `@everyone` is now accepted for `?perms add`. @@ -229,12 +310,12 @@ Security update! - `?notify` no longer carries over to the next thread. - `discord.NotFound` errors for `on_raw_reaction_add`. -- `mod_typing` ~~and `user_typing`~~ (`user_typing` is now by-design to show) will no longer show when user is blocked. +- `mod_typing` ~~and `user_typing`~~ (`user_typing` is now by-design to show) will no longer show when the user is blocked. - Better `?block` usage message. -- Resolves errors when message was sent by mods after thread is closed somehow. +- Resolved errors when mods sent messages after a thread is closed somehow. - Recipient join/leave server messages are limited to only the guild set by `GUILD_ID`. -- When creating snippets and aliases, it now checks if another snippets/aliases with the same name exists. -- Was looking for `config.json` in the wrong directory. +- When creating snippets and aliases, it now checks if other snippets/aliases with the same name exist. +- Modmail looked for `config.json` in the wrong directory. ### Internal @@ -254,13 +335,13 @@ Security update! ### Added - New commands, `?alias edit ` and `?snippets edit `. - - They can be used to edit aliases and snippets respectively. + - They can be used to edit aliases and snippets, respectively. # v3.0.2 ### Added -- New command, `?blocked whitelist `, this command prevents users from getting blocked by any means. +- A new command, `?blocked whitelist `, this command prevents users from getting blocked by any means. ### Changed @@ -270,30 +351,30 @@ Security update! ### Fixed -- A lot of bugs with `thread_auto_close` πŸ˜… +- Many bugs with `thread_auto_close`. # v3.0.0 ### Added -- Sponsors command that will list sponsors. -- An alert will now be sent to the log channel if a thread channel fails to create. This could be due to a variety of problems such as insufficient permissions or the category channel limit is met. +- `?sponsors` command will list sponsors. +- An alert will now be sent to the log channel if a thread channel fails to create. This could be due to a variety of problems such as insufficient permissions, or the category channel limit is met. - Threads will close automatically after some time when `thread_auto_close` is set. -- Custom closing message can be set with `thread_auto_close_response`. +- Custom closing messages can be configured with `thread_auto_close_response`. ### Breaking Changes -- Removed autoupdate functionality and the `?update` command in favour of the [Pull app](https://github.com/apps/pull). +- Removed auto-update functionality and the `?update` command in favor of the [Pull app](https://github.com/apps/pull). Read more about updating your bot [here](https://github.com/kyb3r/modmail/wiki/updating) ### Changed -- Channel names now can contain unicode characters. -- Debug logs are now located in a unique file for each bot. (Internal change) +- Channel names now can contain Unicode characters. +- Debug logs are now located in a different file for each bot. (Internal change) - Default cogs always appear first in the help command now. ### Fixed -- Editing notes now works, minor bug with edit command is fixed. +- Editing notes now work, minor bug with edit command is fixed. - Bug in the `?oauth` command where the response message fails to send when an ID is provided. - Plugin requirement installation now works in virtual environments @@ -308,7 +389,7 @@ Fixed a bug with branches and `?plugin update`. ### Added -Branch support for `?plugin add` and in registry. Typically for developers. +Branch support for `?plugin add` and in the registry. Typically for developers. # v2.23.0 @@ -316,11 +397,11 @@ Branch support for `?plugin add` and in registry. Typically for developers. Added a "Mutual servers" field to the genesis embed if: a) The user is not in the main guild. -b) The user shares more than 1 server with the bot. +b) The user shares more than one server with the bot. ### Changed -Notes taken with the `?note` command are now automatically pinned within the thread channel. +Notes with the `?note` command are now automatically pinned within the thread channel. # v2.22.0 @@ -362,14 +443,14 @@ Add your plugin in the `plugins/registry.json` file in the main repository. This update contains mostly internal changes. - Implemented support for the new discord.py v1.1.1. - Improved help text for most commands. - - Completely revamped help command, few user changes. - - Removed abc (internal). + - Completely revamped help command, few users changes. + - Removed ABC (internal). # v2.20.0 ### What's new? -New `oauth` whitelist command which allows you to whitelist users so they can log in via discord to view logs. To set up oauth login for your logviewer app check the logviewer [repo](https://github.com/kyb3r/logviewer). +New `?oauth whitelist` command, which allows you to whitelist users so they can log in via discord to view logs. To set up oauth login for your logviewer app, check the logviewer [repo](https://github.com/kyb3r/logviewer). # v2.19.1 @@ -377,7 +458,7 @@ New `oauth` whitelist command which allows you to whitelist users so they can lo - Ability to force an update despite having the same version number. Helpful to keep up-to-date with the latest GitHub commit. - `?update force`. -- Plugin developers now have a new event called `on_plugin_ready`, this is coroutine is awaited when all plugins are loaded. Use `on_plugin_ready` instead of `on_ready` since `on_ready` will not get called in plugins. +- Plugin developers now have a new event called `on_plugin_ready`; this is a coroutine and is awaited when all plugins are loaded. Use `on_plugin_ready` instead of `on_ready` since `on_ready` will not get called in plugins. # v2.19.0 @@ -398,17 +479,17 @@ Fix the teams permission bug. ### Changed -Commands now have better error messages, instead of just sending the help message for a command when an argument fails to be converted to its specified object, the bot now says things like "User 'bob' not found" instead. +Commands now have better error messages. Instead of sending the help message for a command when an argument fails to be converted, the bot now says like "User 'bob' not found" instead. # v2.18.1 -Un-deprecated the `OWNERS` config variable to support discord developer team accounts. +Un-deprecated the `OWNERS` config variable to support Discord developer team accounts. # v2.18.0 ### New Permissions System -- A brand new permission system! Replacing the old guild-based permissions (ie. manage channels, manage messages), the new system enables you to customize your desired permission level specific to a command or a group of commands for a role or user. +- A brand new permission system! Replaced the old guild-based permissions (i.e., manage channels, manage messages), with the new system enables you to customize your desired permission level specific to a command or a group of commands for a role or user. - There are five permission levels: - Owner [5] - Administrator [4] @@ -427,7 +508,7 @@ You may add a role or user to a permission group through any of the following me The same applies to individual commands permissions: - `?permissions add command command-name @member#1234` -- ... and the other methods listed above. +- and the other methods listed above. To revoke permission, use `remove` instead of `add`. @@ -440,11 +521,11 @@ By default, all newly set up Modmail will have `OWNER` set to the owner of the b ### Breaking When updating to this version, all prior permission settings with guild-based permissions will be invalidated. You will need to convert to the above system. -`OWNERS` will also get removed, you will need to set owners through `?permissions add level owner 212931293123129` or any way listed above. +`OWNERS` will also get removed; you will need to set owners through `?permissions add level owner 212931293123129` or any way listed above. ### New Command -- A `?delete` command, which is an alternative to manually deleting a message. This command is created to no longer requires manage messages permission to recall thread messages. +- A `?delete` command, which is an alternative to manually deleting a message. This command is created to no longer require "manage messages" permission to recall thread messages. ### Changed @@ -465,28 +546,28 @@ Stricter fallback genesis embed search. ### Changed -How modmail checks if a channel is a thread: +How Modmail checks if a channel is a thread: -1. The bot first checks if the channel topic is in the format `User ID: xxxx`, this means it is a thread. -2. If a channel topic is not found, the bot searches through the message history of a channel to find the thread creation embed. This step should never yield a thread for a normal user, but in the case of another bot messing up the channel topic (happened to a user before) this extra step was added. +1. The bot first checks if the channel topic is in the format `User ID: XXXX`, this means it is a thread. +2. If a channel topic is not found, the bot searches through the message history of a channel to find the thread creation embed. This step should never yield a thread for an average user. Still, in the case of another bot messing up the channel topic (happened to a user before), this extra step was added. # v2.17.0 ### What's new? -Added a config option `reply_without_command` which when present, enables the bot to forward any message sent in a thread channel to the recipient. (Replying without using a command) +Added a config option `reply_without_command`, which, when present, enables the bot to forward any message sent in a thread channel to the recipient. (Replying without using a command) To enable this functionality, do `?config set reply_without_command true` and to disable it, use `?config del reply_without_command`. ### Changed -The `move` command now only requires `manage_messages` perms instead of `manage_channels` +The `move` command now only requires `manage_messages` perms instead of `manage_channels`. # v2.16.1 ### Fixed -An issue where a scheduled close would not execute over a long period of time if the recipient no shares any servers with the bot. +An issue where a scheduled close would not execute over a long time if the recipient no shares any servers with the bot. # v2.16.0 @@ -516,8 +597,8 @@ Added the ability to change the default close message via the introduction of tw They will be provided by string variables that you can incorporate into them: - `closer` - the user object that closed the thread. -- `logkey` - the key for the thread logs e.g. (`5219ccc82ad4`) -- `loglink` - the full link to the thread logs e.g. (`https://logwebsite.com/logs/5219ccc82ad4`) +- `logkey` - the key for the thread logs, e.g. (`5219ccc82ad4`) +- `loglink` - the full link to the thread logs, e.g. (`https://logwebsite.com/logs/5219ccc82ad4`) Example usage would be: ``?config set thread_close_message {closer.mention} closed the thread, here is the link to your logs: [**`{logkey}`**]({loglink})`` @@ -546,9 +627,9 @@ You now have complete control of the look of the thread creation and close embed ### What's new? -Added the ability to disable the `sent_emoji` and `blocked_emoji` when a user messages modmail. +Added the ability to disable the `sent_emoji` and `blocked_emoji` when a user messages Modmail. -You can do this via `?config set sent_emoji disable` +You can do this via `?config set sent_emoji disable`. ### Fixed @@ -565,17 +646,17 @@ Added image link in title in case discord fails to embed an image. - Introduced a new configuration variable `account_age` for setting a minimum account creation age. - Users blocked by this reason will be stored in `blocked` along with other reasons for being blocked. - `account_age` needs to be an ISO-8601 Duration Format (examples: `P12DT3H` 12 days and 3 hours, `P3Y5M` 3 years and 5 months `PT4H14M999S` 4 hours 14 minutes and 999 seconds). https://en.wikipedia.org/wiki/ISO_8601#Durations. - - You can set `account_age` using `config set account_age time` where "time" can be a simple human readable time string or an ISO-8601 Duration Format string. + - You can set `account_age` using `config set account_age time` where "time" can be a simple human-readable time string or an ISO-8601 Duration Format string. ### Changed -- `block` reason cannot start with `System Message: ` as it is now reserved for internal user blocking. -- `block`, like `close`, now supports a block duration (temp blocking). +- `?block` reason cannot start with `System Message: ` as it is now reserved for internal user blocking. +- `?block`, like `?close`, now supports a block duration (temp blocking). # v2.13.10 ### Fixed - Fixed an issue where status and activity do not work if they were modified wrongly in the database. - - This was especially an issue for older Modmail users, as the old `status` configuration variable clashes with the new `status` variable. + - This was primarily an issue for older Modmail users, as the old `status` configuration variable clashes with the new `status` variable. # v2.13.9 @@ -594,7 +675,7 @@ Added image link in title in case discord fails to embed an image. ### What's new? - The ability to enable typing interactions. - - If you want the bot to type in the thread channel if the user is also typing, add the config variable `user_typing` and set it to "yes" or "true". use `config del` to disable the functionality. The same thing in reverse is also possible if you want the user to see the bot type when someone is typing in the thread channel add the `mod_typing` config variable. + - If you want the bot to type in the thread channel if the user is also typing, add the config variable `user_typing` and set it to "yes" or "true". Use `config del` to disable the functionality. The same thing in reverse is also possible if you want the user to see the bot type when someone is typing in the thread channel add the `mod_typing` config variable. - New `status` command, change the bot's status to `online`, `idle`, `dnd`, `invisible`, or `offline`. - To remove the status (change it back to default), use `status clear`. - This also introduces a new internal configuration variable: `status`. Possible values are `online`, `idle`, `dnd`, `invisible`, and `offline`. @@ -611,15 +692,15 @@ Added image link in title in case discord fails to embed an image. ### What's new? - You will no longer need to view your bot debug logs from Heroku. `debug` will show you the recent logs within 24h through a series of embeds. - - If you don't mind your data (may or may not be limited to: user ID, guild ID, bot name) be on the internet, `debug hastebin` will upload a formatted logs file to https://hasteb.in. + - If you don't mind your data (may or may not be limited to user ID, guild ID, bot name) be on the internet, `debug hastebin` will upload a formatted logs file to https://hasteb.in. - `debug clear` will clear the locally cached logs. - - Local logs are automatically cleared at least once every 27h for bots hosted on Heroku. + - Local logs are automatically erased at least once every 27h for bots hosted on Heroku. ### Fixed -- Will no longer show `Unclosed client session` and `Task was destroyed but it is pending!` when the bot terminates. +- Will no longer show `Unclosed client session` and `Task was destroyed, but it is pending!` when the bot terminates. - `thread.create` is now synchronous so that the first message sent can be queued to be sent as soon as a thread is created. - This fixes a problem where if multiple messages are sent in quick succession, the first message sent (which triggers the thread creation) is not sent in order. -- Trying to reply to someone who has DMs disabled or has blocked the bot is now handled and the bot will send a message saying so. +- Trying to reply to someone who has DMs disabled or has blocked the bot is now handled, and the bot will send a message saying so. ### Changed - `print` is replaced by logging. @@ -653,14 +734,14 @@ Added image link in title in case discord fails to embed an image. ### What's new? - Plugins: - - Think of it like addons! Anyone (with the skills) can create a plugin, make it public and distribute it. Add a welcome message to Modmail, or moderation commands? It's all up to your imagination! Have a niche feature request that you think only your server would benefit from? Plugins are your go-to! + - Think of it like addons! Anyone (with the skills) can create a plugin, make it public and distribute it. Add a welcome message to Modmail, or moderation commands? It's all up to your imagination! Have a niche feature request that you think only your server would benefit? Plugins are your go-to! - [Creating Plugins Documentation](https://github.com/kyb3r/modmail/wiki/Plugins). # v2.12.5 ### Fixed -- `config del` command will now work properly on self-hosted db bots. +- `config del` command will now work correctly on self-hosted DB bots. # v2.12.4 @@ -674,14 +755,14 @@ Added image link in title in case discord fails to embed an image. ### Fixed - Patched a bug where `logs` sub-commands were accessible by anyone. -- Patched a bug where an error was raised if there was an open thread where the recipient had left the server. +- Patched a bug where an error was raised when a thread is open where the recipient left the server. Huge thanks to Sasiko for reporting these issues. # v2.12.2 ### Fixed -- Fixed a bug in self-hosted `update` command. +- Fixed a bug in self-hosted `?update` command. # v2.12.1 @@ -692,12 +773,12 @@ Huge thanks to Sasiko for reporting these issues. # v2.12.0 ### Important -**In the future, the Modmail API (https://modmail.tk) will be deprecated. This is due to the fact that we are providing a free service without getting anything in return, and thus we do not have the resources to scale to accommodate for more users. +**In the future, the Modmail API (https://modmail.tk) will be deprecated. This is because we are providing free service without getting anything in return. Thus we do not have the resources to scale to accommodate more users. We recommend using your own database for logs. In the future you will soon get a `backup` command so you can download all your pre-existing data and migrate to your own database.** ### Changed - A lot of painful code cleanup, which is good for us (the developers), but shouldn't affect you. -- The appearance of the `logs` command. Should be clearer with better info now. +- The appearance of the `?logs` command. It should be clearer with better info now. - Bot owners get access to all commands regardless of server permissions. - Blocked users no longer receive a message, only the blocked emoji will be sent. @@ -705,10 +786,10 @@ We recommend using your own database for logs. In the future you will soon get a - **Note:** The following commands only work if you are self-hosting your logs. We recommend you to use your own database. - Log search queries, in the form of two new commands. - `logs search [query]` - this searches all log messages for a query string. -- `logs closed-by [user]` this returns all logs closed by a certain user +- `logs closed-by [user]` this returns all logs closed by a particular user ### Fixed -- `activity listening to music` no longer result in two "to"s ("listening to to music"). +- `activity listening to music` no longer results in two "to"s ("listening to to music"). - This may require you to change your activity message to accommodate this fix. - A problem where `main_category_id` and `log_channel_id` weren't updated when their corresponding channel or category get deleted. @@ -731,7 +812,7 @@ We recommend using your own database for logs. In the future you will soon get a ### What's new? - `anonreply` command to anonymously reply to the recipient. -The username of the anonymous user defaults to the `mod_tag` (the footer text of a mod reply message). The avatar defaults the guild icon URL. However you can change both of these via the `anon_username`, `anon_avatar_url` and `anon_tag` config variables. +The username of the anonymous user defaults to the `mod_tag` (the footer text of a mod reply message) β€” the avatar defaults to the guild icon URL. However, you can change both of these via the `anon_username`, `anon_avatar_url`, and `anon_tag` config variables. ### Changed - Your bot now logs all messages sent in a thread channel, including discussions that take place. You can now toggle to view them in the log viewer app. @@ -756,7 +837,7 @@ The username of the anonymous user defaults to the `mod_tag` (the footer text of - All commands are now blurple instead of green. ### Fixed -- Bug where the close command wouldn't work if you didnt configure a log channel. +- Bug where the close command wouldn't work if you didn't configure a log channel. ### What's new? - Ability to set your own custom `mod_color` and `recipient_color` for the thread message embeds. @@ -773,13 +854,13 @@ The username of the anonymous user defaults to the `mod_tag` (the footer text of # v2.9.0 ### What's new? -- New command `note` will add a system message to your thread logs. This is useful for noting the context of a conversation. +- New command `note` will add a system message to your thread logs. - - This is useful for noting the context of a conversation. # v2.8.1 ### Fixed - Fixed bug where thread logs were getting duplicated when using the `contact` command. -- Fixed bug where the wrong key was used for logs which caused some `log` command log links to point to an HTTP 404 Not Found. +- Fixed bug where the wrong key was used for logs, which caused some `log` command log links to point to an HTTP 404 Not Found. - A minor oversight from commit 1ba74d9. # v2.8.0 @@ -796,7 +877,7 @@ The username of the anonymous user defaults to the `mod_tag` (the footer text of ### Security Thread channels will now default to being private (`@everyone`'s read message perms set to `false`). - If the thread creation category could not be resolved. - - This will save you from some trouble if for whatever reason your configuration gets messed up. + - This will save you from some trouble if, for whatever reason, your configuration gets messed up. # v2.7.1 @@ -808,20 +889,20 @@ Thread channels will now default to being private (`@everyone`'s read message pe ### Note -- If your Modmail bot was set up a long time ago, you may experience an issue where messages were sent outside of the category. +- If your Modmail bot was set up a long time ago, you might experience an issue where messages were sent outside of the category. - To fix this, set `main_category_id` to the ID of the Modmail category. # v2.7.0 ### Changed -- `move` command now syncs thread channel permissions with the category that it was moved to. +- `move` command now syncs thread channel permissions with the destination category. - `contact` command now supports an optional category argument (where the thread channel will be created). # v2.6.3 ### Fixes -- Fixed small issue with finding thread. +- Fixed small issue with finding threads. # v2.6.2 @@ -841,8 +922,8 @@ Thread channels will now default to being private (`@everyone`'s read message pe ### Changed - Log URLs are moved to their own collection. - Log URLs are now `https://logs.modmail.tk/LOGKEY`, no more numbers before the log key. -- We still support the numbers so as to not break everyone's URLs so quickly but both work at the moment. -- This is a huge change to the backend logging and there might be migration errors. If so, please contact us in our [discord server](https://discord.gg/2fMbf2N). +- We still support the numbers to not break everyone's URLs so quickly, but both work at the moment. +- This is a huge change to the backend logging, and there might be migration errors. If so, please contact us in our [Discord server](https://discord.gg/2fMbf2N). # v2.5.2 @@ -852,13 +933,13 @@ Thread channels will now default to being private (`@everyone`'s read message pe # v2.5.1 ### Fixes -- Emergency patch to save config. +- Emergency patch to save configs. # v2.5.0 ### Background - Bots hosted by Heroku restart at least once every 27 hours. -- During this period, local caches are deleted, which results in the inability to set the scheduled close time to longer than 24 hours. This update resolves this issue. +- During this period, local caches will be deleted, which results in the inability to set the scheduled close time to longer than 24 hours. This update resolves this issue. - [PR #135](https://github.com/kyb3r/modmail/pull/135) ### Changed @@ -876,7 +957,7 @@ Fixed activity setting due to flawed logic in `config.get()` function. # v2.4.4 ### Fixed -Fixed a bug in activity command where it would fail to set the activity on bot restart if the activity type was `playing`. +Fixed a bug in the `?activity` command where it would fail to set the activity on bot restart if the activity type was `playing`. # v2.4.3 @@ -891,17 +972,17 @@ Fixed a bug in activity command where it would fail to set the activity on bot r # v2.4.1 ### Fixed -- Small bug in `activity` command. +- Small bug in `?activity` command. # v2.4.0 ### What's new? -- Added the `activity` command for setting the activity -- [PR #131](https://github.com/kyb3r/modmail/pull/131#issue-244686818) this supports multiple activity types (`playing`, `watching`, `listening` and `streaming`). +- Added the `?activity` command for setting the activity +- [PR #131](https://github.com/kyb3r/modmail/pull/131#issue-244686818) this supports multiple activity types (`playing`, `watching`, `listening`, and `streaming`). ### Removed - Removed the deprecated `status` command. -- This also means you will have to reset your bot status with the `activity` command, as `status` command is removed. +- This also means you will have to reset your bot status with the `?activity` command, as the `?status` command was removed. # v2.3.0 @@ -922,7 +1003,7 @@ Fixed a bug in activity command where it would fail to set the activity on bot r ### What's new? - Notify command `notify [role]`. - Notify a given role or yourself to the next thread message received. - - Once a thread message is received you will be pinged once only. + - Once a thread message is received, you will be pinged once only. - Subscribe command `sub [role]` / `unsub [role]`. - Subscribes yourself or a given role to be notified when thread messages are received. @@ -939,15 +1020,15 @@ Fixed a bug in activity command where it would fail to set the activity on bot r # v2.1.0 ### What's new? -- Ability to set a custom thread creation response message. +- Ability to set a custom thread-creation-response message. - Via `config set thread_creation_response [message]`. ### Changed -- Improve logs command format. -- Improve thread log channel message to have more relevant info. +- Improve `?logs` command format. +- Improve thread log channel messages to have more relevant info. - Improve close command. - - You now can close the thread after a delay and use a custom thread close message. - - You also now have the ability to close a thread silently. + - You can now close the thread after a delay and use a custom thread close message. + - You also now can close a thread silently. # v2.0.10 @@ -962,7 +1043,7 @@ Fixed a bug in activity command where it would fail to set the activity on bot r ### Fixes - Support multiple images and file attachments in one message. -- This is only possible on mobile so its good to handle it in code. +- This is only possible on mobile, so its good to handle it in code. # v2.0.8 @@ -973,7 +1054,7 @@ Fixed a bug in activity command where it would fail to set the activity on bot r - You can do this via the `config set main_category_id ` command. ### Changed -- You now have the ability to supply a reason when blocking a user. +- You can now supply a reason when blocking a user. - Blocked users are now stored in the database instead of in the channel topic. - This means you can delete the top channel in the Modmail category now (after migrating the currently blocked users). @@ -984,8 +1065,8 @@ Fixed a bug in activity command where it would fail to set the activity on bot r ### Changed - `update` command now shows the latest changes directly from CHANGELOG.md. -- Auto update messages also show the latest changes from the GitHub repo. -- Removed "latest changes" section from the `about` command. +- Auto-update messages also show the latest changes from the GitHub repo. +- Removed the "latest changes" section from the `about` command. # v2.0.6 @@ -998,20 +1079,20 @@ Fixed a bug in activity command where it would fail to set the activity on bot r ### Changed - `alias` command now checks if you are adding a valid alias-command combo. -- Deleting a channel manually will now correctly close the thread and post logs. +- Manually deleting a channel will now correctly close the thread and post logs. # v2.0.4 ### Fixed -- Fixed a one-off bug where the channel topic disappears, but Modmail operations should still continue. +- Fixed a one-off bug where the channel topic disappears, but Modmail operations should continue. - Fixed `linked_message_id` issues. # v2.0.3 ### Fixed -- Thread creation embed now shows the correct number of past logs. +- The thread creation embed now shows the correct number of past logs. - If using a separate server setup, roles in the info embed now are shown as names instead of mentions. - - This is due to the fact that you can't mention roles across servers. + - This is because you can't mention roles across servers. # v2.0.2 @@ -1023,7 +1104,7 @@ Fixed a bug in activity command where it would fail to set the activity on bot r ### Changed - Improved `block` / `unblock` commands. - - They now take a wider range of arguments: usernames, nicknames, mentions and user IDs. + - They now take a more comprehensive range of arguments: usernames, nicknames, mentions, and user IDs. ### Fixed - Setup command now configures permissions correctly so that the bot will always be able to see the main operations category. @@ -1031,7 +1112,7 @@ Fixed a bug in activity command where it would fail to set the activity on bot r # v2.0.0 This release introduces the use of our centralized [API service](https://github.com/kyb3r/webserver) to enable dynamic configuration, auto-updates, and thread logs. -To use this release you must acquire an API token from https://modmail.tk. +To use this release, you must acquire an API token from https://modmail.tk. Read the updated installation guide [here](https://github.com/kyb3r/modmail/wiki/installation). ### Changed diff --git a/Pipfile b/Pipfile index 9173c3a5fe..01485b1e63 100644 --- a/Pipfile +++ b/Pipfile @@ -4,27 +4,25 @@ url = "https://pypi.org/simple" verify_ssl = true [dev-packages] -black = "==19.3b0" +black = "==19.10b0" pylint = "*" bandit = "==1.6.2" +flake8 = "*" [packages] colorama = ">=0.4.0" python-dateutil = ">=2.7.0" emoji = ">=0.2" -uvloop = {version=">=0.12.0", sys_platform = "!= 'win32'"} +uvloop = {version = ">=0.12.0",sys_platform = "!= 'win32'"} motor = ">=2.0.0" natural = "==0.2.0" isodate = ">=0.6.0" dnspython = "~=1.16.0" -parsedatetime = "==2.5" -aiohttp = "<3.6.0,>=3.3.0" +parsedatetime = "==2.6" +aiohttp = ">=3.6.0,<3.7.0" python-dotenv = ">=0.10.3" pipenv = "*" -"discord.py" = "==1.2.5" - -[requires] -python_version = "3.7" +"discord.py" = "==1.5.1" [scripts] bot = "python bot.py" diff --git a/Pipfile.lock b/Pipfile.lock index 06d263645b..005ce37f67 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,12 +1,10 @@ { "_meta": { "hash": { - "sha256": "c2eb0898f236534a02cb1c198d74c82fed052b4445e39f99c1af3e58d22aa435" + "sha256": "0491618cb8bd6d70e4ab3337c23b72fc3b1d5f9ca0603d8be6b890b661039102" }, "pipfile-spec": 6, - "requires": { - "python_version": "3.7" - }, + "requires": {}, "sources": [ { "name": "pypi", @@ -18,52 +16,52 @@ "default": { "aiohttp": { "hashes": [ - "sha256:00d198585474299c9c3b4f1d5de1a576cc230d562abc5e4a0e81d71a20a6ca55", - "sha256:0155af66de8c21b8dba4992aaeeabf55503caefae00067a3b1139f86d0ec50ed", - "sha256:09654a9eca62d1bd6d64aa44db2498f60a5c1e0ac4750953fdd79d5c88955e10", - "sha256:199f1d106e2b44b6dacdf6f9245493c7d716b01d0b7fbe1959318ba4dc64d1f5", - "sha256:296f30dedc9f4b9e7a301e5cc963012264112d78a1d3094cd83ef148fdf33ca1", - "sha256:368ed312550bd663ce84dc4b032a962fcb3c7cae099dbbd48663afc305e3b939", - "sha256:40d7ea570b88db017c51392349cf99b7aefaaddd19d2c78368aeb0bddde9d390", - "sha256:629102a193162e37102c50713e2e31dc9a2fe7ac5e481da83e5bb3c0cee700aa", - "sha256:6d5ec9b8948c3d957e75ea14d41e9330e1ac3fed24ec53766c780f82805140dc", - "sha256:87331d1d6810214085a50749160196391a712a13336cd02ce1c3ea3d05bcf8d5", - "sha256:9a02a04bbe581c8605ac423ba3a74999ec9d8bce7ae37977a3d38680f5780b6d", - "sha256:9c4c83f4fa1938377da32bc2d59379025ceeee8e24b89f72fcbccd8ca22dc9bf", - "sha256:9cddaff94c0135ee627213ac6ca6d05724bfe6e7a356e5e09ec57bd3249510f6", - "sha256:a25237abf327530d9561ef751eef9511ab56fd9431023ca6f4803f1994104d72", - "sha256:a5cbd7157b0e383738b8e29d6e556fde8726823dae0e348952a61742b21aeb12", - "sha256:a97a516e02b726e089cffcde2eea0d3258450389bbac48cbe89e0f0b6e7b0366", - "sha256:acc89b29b5f4e2332d65cd1b7d10c609a75b88ef8925d487a611ca788432dfa4", - "sha256:b05bd85cc99b06740aad3629c2585bda7b83bd86e080b44ba47faf905fdf1300", - "sha256:c2bec436a2b5dafe5eaeb297c03711074d46b6eb236d002c13c42f25c4a8ce9d", - "sha256:cc619d974c8c11fe84527e4b5e1c07238799a8c29ea1c1285149170524ba9303", - "sha256:d4392defd4648badaa42b3e101080ae3313e8f4787cb517efd3f5b8157eaefd6", - "sha256:e1c3c582ee11af7f63a34a46f0448fca58e59889396ffdae1f482085061a2889" + "sha256:1a4160579ffbc1b69e88cb6ca8bb0fbd4947dfcbf9fb1e2a4fc4c7a4a986c1fe", + "sha256:206c0ccfcea46e1bddc91162449c20c72f308aebdcef4977420ef329c8fcc599", + "sha256:2ad493de47a8f926386fa6d256832de3095ba285f325db917c7deae0b54a9fc8", + "sha256:319b490a5e2beaf06891f6711856ea10591cfe84fe9f3e71a721aa8f20a0872a", + "sha256:470e4c90da36b601676fe50c49a60d34eb8c6593780930b1aa4eea6f508dfa37", + "sha256:60f4caa3b7f7a477f66ccdd158e06901e1d235d572283906276e3803f6b098f5", + "sha256:66d64486172b032db19ea8522328b19cfb78a3e1e5b62ab6a0567f93f073dea0", + "sha256:687461cd974722110d1763b45c5db4d2cdee8d50f57b00c43c7590d1dd77fc5c", + "sha256:698cd7bc3c7d1b82bb728bae835724a486a8c376647aec336aa21a60113c3645", + "sha256:797456399ffeef73172945708810f3277f794965eb6ec9bd3a0c007c0476be98", + "sha256:a885432d3cabc1287bcf88ea94e1826d3aec57fd5da4a586afae4591b061d40d", + "sha256:c506853ba52e516b264b106321c424d03f3ddef2813246432fa9d1cefd361c81", + "sha256:fb83326d8295e8840e4ba774edf346e87eca78ba8a89c55d2690352842c15ba5" ], "index": "pypi", - "version": "==3.5.4" + "version": "==3.6.3" + }, + "appdirs": { + "hashes": [ + "sha256:7d5d0167b2b1ba821647616af46a749d1c653740dd0d2415100fe26e27afdf41", + "sha256:a841dacd6b99318a741b166adb07e19ee71a274450e68237b4650ca1055ab128" + ], + "version": "==1.4.4" }, "async-timeout": { "hashes": [ "sha256:0c3c816a028d47f659d6ff5c745cb2acf1f966da1fe5c19c77a70282b25f4c5f", "sha256:4291ca197d287d274d0b6cb5d6f8f8f82d434ed288f962539ff18cc9012f9ea3" ], + "markers": "python_full_version >= '3.5.3'", "version": "==3.0.1" }, "attrs": { "hashes": [ - "sha256:08a96c641c3a74e44eb59afb61a24f2cb9f4d7188748e76ba4bb5edfa3cb7d1c", - "sha256:f7b7ce16570fe9965acd6d30101a28f62fb4a7f9e926b3bbc9b61f8b04247e72" + "sha256:26b54ddbbb9ee1d34d5d3668dd37d6cf74990ab23c828c2888dccdceee395594", + "sha256:fce7fc47dfc976152e82d53ff92fa0407700c21acd20886a13777a0d20e655dc" ], - "version": "==19.3.0" + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==20.2.0" }, "certifi": { "hashes": [ - "sha256:017c25db2a153ce562900032d5bc68e9f191e44e9a0f762f373977de9df1fbb3", - "sha256:25b64c7da4cd7479594d035c08c2d809eb4aab3a26e5a990ea98cc450c320f1f" + "sha256:5930595817496dd21bb8dc35dad090f1c2cd0adfaf21204bf6732ca5d8ee34d3", + "sha256:8fc0819f1f30ba15bdb34cceffb9ef04d99f420f68eb75d901e9560b8749fc41" ], - "version": "==2019.11.28" + "version": "==2020.6.20" }, "chardet": { "hashes": [ @@ -74,18 +72,26 @@ }, "colorama": { "hashes": [ - "sha256:7d73d2a99753107a36ac6b455ee49046802e59d9d076ef8e47b61499fa29afff", - "sha256:e96da0d330793e2cb9485e9ddfd918d456036c7149416295932478192f4436a1" + "sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b", + "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2" ], "index": "pypi", - "version": "==0.4.3" + "version": "==0.4.4" }, "discord.py": { "hashes": [ - "sha256:7c843b523bb011062b453864e75c7b675a03faf573c58d14c9f096e85984329d" + "sha256:2367359e31f6527f8a936751fc20b09d7495dd6a76b28c8fb13d4ca6c55b7563", + "sha256:def00dc50cf36d21346d71bc89f0cad8f18f9a3522978dc18c7796287d47de8b" ], "index": "pypi", - "version": "==1.2.5" + "version": "==1.5.1" + }, + "distlib": { + "hashes": [ + "sha256:8c09de2c67b3e7deef7184574fc060ab8a793e7adbb183d942c389c8b13c52fb", + "sha256:edf6116872c863e1aa9d5bb7cb5e05a022c519a4594dc703843343a9ddd9bff1" + ], + "version": "==0.3.1" }, "dnspython": { "hashes": [ @@ -97,17 +103,25 @@ }, "emoji": { "hashes": [ - "sha256:60652d3a2dcee5b8af8acb097c31776fb6d808027aeb7221830f72cdafefc174" + "sha256:e42da4f8d648f8ef10691bc246f682a1ec6b18373abfd9be10ec0b398823bd11" ], "index": "pypi", - "version": "==0.5.4" + "version": "==0.6.0" + }, + "filelock": { + "hashes": [ + "sha256:18d82244ee114f543149c66a6e0c14e9c4f8a1044b5cdaadd0f82159d6a6ff59", + "sha256:929b7d63ec5b7d6b71b0fa5ac14e030b3f70b75747cef1b10da9b879fef15836" + ], + "version": "==3.0.12" }, "idna": { "hashes": [ - "sha256:c357b3f628cf53ae2c4c05627ecc484553142ca23264e593d327bcde5e9c3407", - "sha256:ea8b7f6188e6fa117537c3df7da9fc686d485087abf6ac197f9c46432f7e4a3c" + "sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6", + "sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0" ], - "version": "==2.8" + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==2.10" }, "isodate": { "hashes": [ @@ -119,34 +133,34 @@ }, "motor": { "hashes": [ - "sha256:599719bc6dcddc3b9ea4e09659fb0073d5fadcc24735999b2902f48cef33f909", - "sha256:756c587985d166166e644ccd36fb8b586fb987eb42fc0fc60cce9a3d76d809b4", - "sha256:97b4fc0a00a84df30f866d18693c503eef46c7642f75218a2c44d74d835be38a" + "sha256:428d94750123d19fcd0a89b8671ff9b4656f205217bad9f44161748c64c5fc80", + "sha256:f1692b760d834707e3477996ce8d407af8cd61c1a2abedbf81c22ef14675e61a" ], "index": "pypi", - "version": "==2.1.0" + "version": "==2.3.0" }, "multidict": { "hashes": [ - "sha256:09c19f642e055550c9319d5123221b7e07fc79bda58122aa93910e52f2ab2f29", - "sha256:0c1a5d5f7aa7189f7b83c4411c2af8f1d38d69c4360d5de3eea129c65d8d7ce2", - "sha256:12f22980e7ed0972a969520fb1e55682c9fca89a68b21b49ec43132e680be812", - "sha256:258660e9d6b52de1a75097944e12718d3aa59adc611b703361e3577d69167aaf", - "sha256:3374a23e707848f27b3438500db0c69eca82929337656fce556bd70031fbda74", - "sha256:503b7fce0054c73aa631cc910a470052df33d599f3401f3b77e54d31182525d5", - "sha256:6ce55f2c45ffc90239aab625bb1b4864eef33f73ea88487ef968291fbf09fb3f", - "sha256:725496dde5730f4ad0a627e1a58e2620c1bde0ad1c8080aae15d583eb23344ce", - "sha256:a3721078beff247d0cd4fb19d915c2c25f90907cf8d6cd49d0413a24915577c6", - "sha256:ba566518550f81daca649eded8b5c7dd09210a854637c82351410aa15c49324a", - "sha256:c42362750a51a15dc905cb891658f822ee5021bfbea898c03aa1ed833e2248a5", - "sha256:cf14aaf2ab067ca10bca0b14d5cbd751dd249e65d371734bc0e47ddd8fafc175", - "sha256:cf24e15986762f0e75a622eb19cfe39a042e952b8afba3e7408835b9af2be4fb", - "sha256:d7b6da08538302c5245cd3103f333655ba7f274915f1f5121c4f4b5fbdb3febe", - "sha256:e27e13b9ff0a914a6b8fb7e4947d4ac6be8e4f61ede17edffabd088817df9e26", - "sha256:e53b205f8afd76fc6c942ef39e8ee7c519c775d336291d32874082a87802c67c", - "sha256:ec804fc5f68695d91c24d716020278fcffd50890492690a7e1fef2e741f7172c" - ], - "version": "==4.7.1" + "sha256:1ece5a3369835c20ed57adadc663400b5525904e53bae59ec854a5d36b39b21a", + "sha256:275ca32383bc5d1894b6975bb4ca6a7ff16ab76fa622967625baeebcf8079000", + "sha256:3750f2205b800aac4bb03b5ae48025a64e474d2c6cc79547988ba1d4122a09e2", + "sha256:4538273208e7294b2659b1602490f4ed3ab1c8cf9dbdd817e0e9db8e64be2507", + "sha256:5141c13374e6b25fe6bf092052ab55c0c03d21bd66c94a0e3ae371d3e4d865a5", + "sha256:51a4d210404ac61d32dada00a50ea7ba412e6ea945bbe992e4d7a595276d2ec7", + "sha256:5cf311a0f5ef80fe73e4f4c0f0998ec08f954a6ec72b746f3c179e37de1d210d", + "sha256:6513728873f4326999429a8b00fc7ceddb2509b01d5fd3f3be7881a257b8d463", + "sha256:7388d2ef3c55a8ba80da62ecfafa06a1c097c18032a501ffd4cabbc52d7f2b19", + "sha256:9456e90649005ad40558f4cf51dbb842e32807df75146c6d940b6f5abb4a78f3", + "sha256:c026fe9a05130e44157b98fea3ab12969e5b60691a276150db9eda71710cd10b", + "sha256:d14842362ed4cf63751648e7672f7174c9818459d169231d03c56e84daf90b7c", + "sha256:e0d072ae0f2a179c375f67e3da300b47e1a83293c554450b29c900e50afaae87", + "sha256:f07acae137b71af3bb548bd8da720956a3bc9f9a0b87733e0899226a2317aeb7", + "sha256:fbb77a75e529021e7c4a8d4e823d88ef4d23674a202be4f5addffc72cbb91430", + "sha256:fcfbb44c59af3f8ea984de67ec7c306f618a3ec771c2843804069917a8f2e255", + "sha256:feed85993dbdb1dbc29102f50bca65bdc68f2c0c8d352468c25b54874f23c39d" + ], + "markers": "python_version >= '3.5'", + "version": "==4.7.6" }, "natural": { "hashes": [ @@ -157,78 +171,79 @@ }, "parsedatetime": { "hashes": [ - "sha256:3b835fc54e472c17ef447be37458b400e3fefdf14bb1ffdedb5d2c853acf4ba1", - "sha256:d2e9ddb1e463de871d32088a3f3cea3dc8282b1b2800e081bd0ef86900451667" + "sha256:4cb368fbb18a0b7231f4d76119165451c8d2e35951455dfee97c62a87b04d455", + "sha256:cb96edd7016872f58479e35879294258c71437195760746faffedb692aef000b" ], "index": "pypi", - "version": "==2.5" + "version": "==2.6" }, "pipenv": { "hashes": [ - "sha256:56ad5f5cb48f1e58878e14525a6e3129d4306049cb76d2f6a3e95df0d5fc6330", - "sha256:7df8e33a2387de6f537836f48ac6fcd94eda6ed9ba3d5e3fd52e35b5bc7ff49e", - "sha256:a673e606e8452185e9817a987572b55360f4d28b50831ef3b42ac3cab3fee846" + "sha256:448ac3a36443db633d52a2359cac15ecbc4f429eab4ddd420697602b721d1c5a", + "sha256:eff0e10eadb330f612edfa5051d3d8e775e9e0e918c3c50361da703bd0daa035" ], "index": "pypi", - "version": "==2018.11.26" + "version": "==2020.8.13" }, "pymongo": { "hashes": [ - "sha256:0369136c6e79c5edc16aa5de2b48a1b1c1fe5e6f7fc5915a2deaa98bd6e9dad5", - "sha256:08364e1bea1507c516b18b826ec790cb90433aec2f235033ec5eecfd1011633b", - "sha256:0af1d2bc8cc9503bf92ec3669a77ec3a6d7938193b583fb867b7e9696eed52e8", - "sha256:0cfd1aeeb8c0a634646ab3ebeb4ce6828b94b2e33553a69ff7e6c07c250bf201", - "sha256:1b4a13dff15641e58620524db15d7a323d60572b2b187261c5cb58c36d74778d", - "sha256:22fbdb908257f9aaaa372a7684f3e094a05ca52eb84f8f381c8b1827c49556fd", - "sha256:264272fd1c95fc48002ad85d5e41270831777b4180f2500943e45e12b2a3ab43", - "sha256:3372e98eebbfd05ebf020388003f8a4438bed41e0fef1ef696d2c13633c416c8", - "sha256:339d24ecdc42745d2dc09b26fda8151988e806ca81134a7bd10513c4031d91e1", - "sha256:38281855fc3961ba5510fbb503b8d16cc1fcb326e9f7ba0dd096ed4eb72a7084", - "sha256:4acdd2e16392472bfd49ca49038845c95e5254b5af862b55f7f2cc79aa258886", - "sha256:4e0c006bc6e98e861b678432e05bf64ba3eb889b6ab7e7bf1ebaecf9f1ba0e58", - "sha256:4e4284bcbe4b7be1b37f9641509085b715c478e7fbf8f820358362b5dd359379", - "sha256:4e5e94a5f9823f0bd0c56012a57650bc6772636c29d83d253260c26b908fcfd9", - "sha256:4e61f30800a40f1770b2ec56bbf5dc0f0e3f7e9250eb05fa4feb9ccb7bbe39ca", - "sha256:53577cf57ba9d93b58ab41d45250277828ff83c5286dde14f855e4b17ec19976", - "sha256:681cb31e8631882804a6cc3c8cc8f54a74ff3a82261a78e50f20c5eec05ac855", - "sha256:6dfc2710f43dd1d66991a0f160d196356732ccc8aa9dbc6875aeba78388fa142", - "sha256:72218201b13d8169be5736417987e9a0a3b10d4349e40e4db7a6a5ac670c7ef2", - "sha256:7247fbcdbf7ab574eb70743461b3cfc14d9cfae3f27a9afb6ce14d87f67dd0b5", - "sha256:72651f4b4adf50201891580506c8cca465d94d38f26ed92abfc56440662c723c", - "sha256:87b3aaf12ad6a9b5570b12d2a4b8802757cb3588a903aafd3c25f07f9caf07e3", - "sha256:87c28b7b37617c5a01eb396487f7d3b61a453e1fa0475a175ab87712d6f5d52f", - "sha256:88efe627b628f36ef53f09abb218d4630f83d8ebde7028689439559475c43dae", - "sha256:89bfbca22266f12df7fb80092b7c876734751d02b93789580b68957ad4a8bf56", - "sha256:908a3caf348a672b28b8a06fe7b4a27c2fdcf7f873df671e4027d48bcd7f971f", - "sha256:9128e7bea85f3a3041306fa14a7aa82a24b47881918500e1b8396dd1c933b5a6", - "sha256:9737d6d688a15b8d5c0bfa909638b79261e195be817b9f1be79c722bbb23cd76", - "sha256:98a8305da158f46e99e7e51db49a2f8b5fcdd7683ea7083988ccb9c4450507a6", - "sha256:99285cd44c756f0900cbdb5fe75f567c0a76a273b7e0467f23cb76f47e60aac0", - "sha256:9ed568f8026ffeb00ce31e5351e0d09d704cc19a29549ba4da0ac145d2a26fdf", - "sha256:a006162035032021dfd00a879643dc06863dac275f9210d843278566c719eebc", - "sha256:a03cb336bc8d25a11ff33b94967478a9775b0d2b23b39e952d9cc6cb93b75d69", - "sha256:a863ceb67be163060d1099b7e89b6dd83d6dd50077c7ceae31ac844c4c2baff9", - "sha256:b82628eaf0a16c1f50e1c205fd1dd406d7874037dd84643da89e91b5043b5e82", - "sha256:bc6446a41fb7eeaf2c808bab961b9bac81db0f5de69eab74eebe1b8b072399f7", - "sha256:c42d290ed54096355838421cf9d2a56e150cb533304d2439ef1adf612a986eaf", - "sha256:c43879fe427ea6aa6e84dae9fbdc5aa14428a4cfe613fe0fee2cc004bf3f307c", - "sha256:c566cbdd1863ba3ccf838656a1403c3c81fdb57cbe3fdd3515be7c9616763d33", - "sha256:c5b7a0d7e6ca986de32b269b6dbbd5162c1a776ece72936f55decb4d1b197ee9", - "sha256:ca109fe9f74da4930590bb589eb8fdf80e5d19f5cd9f337815cac9309bbd0a76", - "sha256:d0260ba68f9bafd8775b2988b5aeace6e69a37593ec256e23e150c808160c05c", - "sha256:d2ce33501149b373118fcfec88a292a87ef0b333fb30c7c6aac72fe64700bdf6", - "sha256:d582ea8496e2a0e124e927a67dca55c8833f0dbfbc2c84aaf0e5949a2dd30c51", - "sha256:d68b9ab0a900582a345fb279675b0ad4fac07d6a8c2678f12910d55083b7240d", - "sha256:dbf1fa571db6006907aeaf6473580aaa76041f4f3cd1ff8a0039fd0f40b83f6d", - "sha256:e032437a7d2b89dab880c79379d88059cee8019da0ff475d924c4ccab52db88f", - "sha256:e0f5798f3ad60695465a093e3d002f609c41fef3dcb97fcefae355d24d3274cf", - "sha256:e756355704a2cf91a7f4a649aa0bbf3bbd263018b9ed08f60198c262f4ee24b6", - "sha256:e824b4b87bd88cbeb25c8babeadbbaaaf06f02bbb95a93462b7c6193a064974e", - "sha256:ea1171470b52487152ed8bf27713cc2480dc8b0cd58e282a1bff742541efbfb8", - "sha256:fa19aef44d5ed8f798a8136ff981aedfa508edac3b1bed481eca5dde5f14fd3d", - "sha256:fceb6ae5a149a42766efb8344b0df6cfb21b55c55f360170abaddb11d43af0f1" - ], - "version": "==3.10.0" + "sha256:03dc64a9aa7a5d405aea5c56db95835f6a2fa31b3502c5af1760e0e99210be30", + "sha256:05fcc6f9c60e6efe5219fbb5a30258adb3d3e5cbd317068f3d73c09727f2abb6", + "sha256:076a7f2f7c251635cf6116ac8e45eefac77758ee5a77ab7bd2f63999e957613b", + "sha256:137e6fa718c7eff270dbd2fc4b90d94b1a69c9e9eb3f3de9e850a7fd33c822dc", + "sha256:1f865b1d1c191d785106f54df9abdc7d2f45a946b45fd1ea0a641b4f982a2a77", + "sha256:213c445fe7e654621c6309e874627c35354b46ef3ee807f5a1927dc4b30e1a67", + "sha256:25e617daf47d8dfd4e152c880cd0741cbdb48e51f54b8de9ddbfe74ecd87dd16", + "sha256:3d9bb1ba935a90ec4809a8031efd988bdb13cdba05d9e9a3e9bf151bf759ecde", + "sha256:40696a9a53faa7d85aaa6fd7bef1cae08f7882640bad08c350fb59dee7ad069b", + "sha256:421aa1b92c291c429668bd8d8d8ec2bd00f183483a756928e3afbf2b6f941f00", + "sha256:4437300eb3a5e9cc1a73b07d22c77302f872f339caca97e9bf8cf45eca8fa0d2", + "sha256:455f4deb00158d5ec8b1d3092df6abb681b225774ab8a59b3510293b4c8530e3", + "sha256:475a34a0745c456ceffaec4ce86b7e0983478f1b6140890dff7b161e7bcd895b", + "sha256:4797c0080f41eba90404335e5ded3aa66731d303293a675ff097ce4ea3025bb9", + "sha256:4ae23fbbe9eadf61279a26eba866bbf161a6f7e2ffad14a42cf20e9cb8e94166", + "sha256:4b32744901ee9990aa8cd488ec85634f443526def1e5190a407dc107148249d7", + "sha256:50127b13b38e8e586d5e97d342689405edbd74ad0bd891d97ee126a8c7b6e45f", + "sha256:50531caa7b4be1c4ed5e2d5793a4e51cc9bd62a919a6fd3299ef7c902e206eab", + "sha256:63a5387e496a98170ffe638b435c0832c0f2011a6f4ff7a2880f17669fff8c03", + "sha256:68220b81850de8e966d4667d5c325a96c6ac0d6adb3d18935d6e3d325d441f48", + "sha256:689142dc0c150e9cb7c012d84cac2c346d40beb891323afb6caf18ec4caafae0", + "sha256:6a15e2bee5c4188369a87ed6f02de804651152634a46cca91966a11c8abd2550", + "sha256:7122ffe597b531fb065d3314e704a6fe152b81820ca5f38543e70ffcc95ecfd4", + "sha256:7307024b18266b302f4265da84bb1effb5d18999ef35b30d17592959568d5c0a", + "sha256:7a4a6f5b818988a3917ec4baa91d1143242bdfece8d38305020463955961266a", + "sha256:83c5a3ecd96a9f3f11cfe6dfcbcec7323265340eb24cc996acaecea129865a3a", + "sha256:890b0f1e18dbd898aeb0ab9eae1ab159c6bcbe87f0abb065b0044581d8614062", + "sha256:8deda1f7b4c03242f2a8037706d9584e703f3d8c74d6d9cac5833db36fe16c42", + "sha256:8ea13d0348b4c96b437d944d7068d59ed4a6c98aaa6c40d8537a2981313f1c66", + "sha256:91e96bf85b7c07c827d339a386e8a3cf2e90ef098c42595227f729922d0851df", + "sha256:96782ebb3c9e91e174c333208b272ea144ed2a684413afb1038e3b3342230d72", + "sha256:9755c726aa6788f076114dfdc03b92b03ff8860316cca00902cce88bcdb5fedd", + "sha256:9dbab90c348c512e03f146e93a5e2610acec76df391043ecd46b6b775d5397e6", + "sha256:9ee0eef254e340cc11c379f797af3977992a7f2c176f1a658740c94bf677e13c", + "sha256:9fc17fdac8f1973850d42e51e8ba6149d93b1993ed6768a24f352f926dd3d587", + "sha256:a2787319dc69854acdfd6452e6a8ba8f929aeb20843c7f090e04159fc18e6245", + "sha256:b7c522292407fa04d8195032493aac937e253ad9ae524aab43b9d9d242571f03", + "sha256:bd312794f51e37dcf77f013d40650fe4fbb211dd55ef2863839c37480bd44369", + "sha256:c0d660a186e36c526366edf8a64391874fe53cf8b7039224137aee0163c046df", + "sha256:c4869141e20769b65d2d72686e7a7eb141ce9f3168106bed3e7dcced54eb2422", + "sha256:cc4057f692ac35bbe82a0a908d42ce3a281c9e913290fac37d7fa3bd01307dfb", + "sha256:cccf1e7806f12300e3a3b48f219e111000c2538483e85c869c35c1ae591e6ce9", + "sha256:ce208f80f398522e49d9db789065c8ad2cd37b21bd6b23d30053474b7416af11", + "sha256:d0565481dc196986c484a7fb13214fc6402201f7fb55c65fd215b3324962fe6c", + "sha256:d1b3366329c45a474b3bbc9b9c95d4c686e03f35da7fd12bc144626d1f2a7c04", + "sha256:d226e0d4b9192d95079a9a29c04dd81816b1ce8903b8c174a39224fe978547cb", + "sha256:d38b35f6eef4237b1d0d8e845fc1546dad85c55eba447e28c211da8c7ef9697c", + "sha256:d64c98277ea80e4484f1332ab107e8dfd173a7dcf1bdbf10a9cccc97aaab145f", + "sha256:d9de8427a5601799784eb0e7fa1b031aa64086ce04de29df775a8ca37eedac41", + "sha256:e6a15cf8f887d9f578dd49c6fb3a99d53e1d922fdd67a245a67488d77bf56eb2", + "sha256:e8c446882cbb3774cd78c738c9f58220606b702b7c1655f1423357dc51674054", + "sha256:e8d188ee39bd0ffe76603da887706e4e7b471f613625899ddf1e27867dc6a0d3", + "sha256:ef76535776c0708a85258f6dc51d36a2df12633c735f6d197ed7dfcaa7449b99", + "sha256:f6efca006a81e1197b925a7d7b16b8f61980697bb6746587aad8842865233218" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==3.11.0" }, "python-dateutil": { "hashes": [ @@ -240,18 +255,19 @@ }, "python-dotenv": { "hashes": [ - "sha256:debd928b49dbc2bf68040566f55cdb3252458036464806f4094487244e2a4093", - "sha256:f157d71d5fec9d4bd5f51c82746b6344dffa680ee85217c123f4a0c8117c4544" + "sha256:8c10c99a1b25d9a68058a1ad6f90381a62ba68230ca93966882a4dbc3bc9c33d", + "sha256:c10863aee750ad720f4f43436565e4c1698798d763b63234fb5021b6c616e423" ], "index": "pypi", - "version": "==0.10.3" + "version": "==0.14.0" }, "six": { "hashes": [ - "sha256:1f1b7d42e254082a9db6279deae68afb421ceba6158efa6131de7b3003ee93fd", - "sha256:30f610279e8b2578cab6db20741130331735c781b56053c59c4076da27f06b66" + "sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259", + "sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced" ], - "version": "==1.13.0" + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==1.15.0" }, "uvloop": { "hashes": [ @@ -265,94 +281,72 @@ "sha256:e7514d7a48c063226b7d06617cbb12a14278d4323a065a8d46a7962686ce2e95", "sha256:f07909cd9fc08c52d294b1570bba92186181ca01fe3dc9ffba68955273dd7362" ], - "index": "pypi", "markers": "sys_platform != 'win32'", "version": "==0.14.0" }, "virtualenv": { "hashes": [ - "sha256:116655188441670978117d0ebb6451eb6a7526f9ae0796cc0dee6bd7356909b0", - "sha256:b57776b44f91511866594e477dd10e76a6eb44439cdd7f06dcd30ba4c5bd854f" + "sha256:b0011228208944ce71052987437d3843e05690b2f23d1c7da4263fde104c97a2", + "sha256:b8d6110f493af256a40d65e29846c69340a947669eec8ce784fcf3dd3af28380" ], - "version": "==16.7.8" + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==20.1.0" }, "virtualenv-clone": { "hashes": [ - "sha256:532f789a5c88adf339506e3ca03326f20ee82fd08ee5586b44dc859b5b4468c5", - "sha256:c88ae171a11b087ea2513f260cdac9232461d8e9369bcd1dc143fc399d220557" - ], - "version": "==0.5.3" - }, - "websockets": { - "hashes": [ - "sha256:0e2f7d6567838369af074f0ef4d0b802d19fa1fee135d864acc656ceefa33136", - "sha256:2a16dac282b2fdae75178d0ed3d5b9bc3258dabfae50196cbb30578d84b6f6a6", - "sha256:5a1fa6072405648cb5b3688e9ed3b94be683ce4a4e5723e6f5d34859dee495c1", - "sha256:5c1f55a1274df9d6a37553fef8cff2958515438c58920897675c9bc70f5a0538", - "sha256:669d1e46f165e0ad152ed8197f7edead22854a6c90419f544e0f234cc9dac6c4", - "sha256:695e34c4dbea18d09ab2c258994a8bf6a09564e762655408241f6a14592d2908", - "sha256:6b2e03d69afa8d20253455e67b64de1a82ff8612db105113cccec35d3f8429f0", - "sha256:79ca7cdda7ad4e3663ea3c43bfa8637fc5d5604c7737f19a8964781abbd1148d", - "sha256:7fd2dd9a856f72e6ed06f82facfce01d119b88457cd4b47b7ae501e8e11eba9c", - "sha256:82c0354ac39379d836719a77ee360ef865377aa6fdead87909d50248d0f05f4d", - "sha256:8f3b956d11c5b301206382726210dc1d3bee1a9ccf7aadf895aaf31f71c3716c", - "sha256:91ec98640220ae05b34b79ee88abf27f97ef7c61cf525eec57ea8fcea9f7dddb", - "sha256:952be9540d83dba815569d5cb5f31708801e0bbfc3a8c5aef1890b57ed7e58bf", - "sha256:99ac266af38ba1b1fe13975aea01ac0e14bb5f3a3200d2c69f05385768b8568e", - "sha256:9fa122e7adb24232247f8a89f2d9070bf64b7869daf93ac5e19546b409e47e96", - "sha256:a0873eadc4b8ca93e2e848d490809e0123eea154aa44ecd0109c4d0171869584", - "sha256:cb998bd4d93af46b8b49ecf5a72c0a98e5cc6d57fdca6527ba78ad89d6606484", - "sha256:e02e57346f6a68523e3c43bbdf35dde5c440318d1f827208ae455f6a2ace446d", - "sha256:e79a5a896bcee7fff24a788d72e5c69f13e61369d055f28113e71945a7eb1559", - "sha256:ee55eb6bcf23ecc975e6b47c127c201b913598f38b6a300075f84eeef2d3baff", - "sha256:f1414e6cbcea8d22843e7eafdfdfae3dd1aba41d1945f6ca66e4806c07c4f454" - ], - "version": "==6.0" + "sha256:07e74418b7cc64f4fda987bf5bc71ebd59af27a7bc9e8a8ee9fd54b1f2390a27", + "sha256:665e48dd54c84b98b71a657acb49104c54e7652bce9c1c4f6c6976ed4c827a29" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==0.5.4" }, "yarl": { "hashes": [ - "sha256:0c2ab325d33f1b824734b3ef51d4d54a54e0e7a23d13b86974507602334c2cce", - "sha256:0ca2f395591bbd85ddd50a82eb1fde9c1066fafe888c5c7cc1d810cf03fd3cc6", - "sha256:2098a4b4b9d75ee352807a95cdf5f10180db903bc5b7270715c6bbe2551f64ce", - "sha256:25e66e5e2007c7a39541ca13b559cd8ebc2ad8fe00ea94a2aad28a9b1e44e5ae", - "sha256:26d7c90cb04dee1665282a5d1a998defc1a9e012fdca0f33396f81508f49696d", - "sha256:308b98b0c8cd1dfef1a0311dc5e38ae8f9b58349226aa0533f15a16717ad702f", - "sha256:3ce3d4f7c6b69c4e4f0704b32eca8123b9c58ae91af740481aa57d7857b5e41b", - "sha256:58cd9c469eced558cd81aa3f484b2924e8897049e06889e8ff2510435b7ef74b", - "sha256:5b10eb0e7f044cf0b035112446b26a3a2946bca9d7d7edb5e54a2ad2f6652abb", - "sha256:6faa19d3824c21bcbfdfce5171e193c8b4ddafdf0ac3f129ccf0cdfcb083e462", - "sha256:944494be42fa630134bf907714d40207e646fd5a94423c90d5b514f7b0713fea", - "sha256:a161de7e50224e8e3de6e184707476b5a989037dcb24292b391a3d66ff158e70", - "sha256:a4844ebb2be14768f7994f2017f70aca39d658a96c786211be5ddbe1c68794c1", - "sha256:c2b509ac3d4b988ae8769901c66345425e361d518aecbe4acbfc2567e416626a", - "sha256:c9959d49a77b0e07559e579f38b2f3711c2b8716b8410b320bf9713013215a1b", - "sha256:d8cdee92bc930d8b09d8bd2043cedd544d9c8bd7436a77678dd602467a993080", - "sha256:e15199cdb423316e15f108f51249e44eb156ae5dba232cb73be555324a1d49c2" - ], - "version": "==1.4.2" + "sha256:040b237f58ff7d800e6e0fd89c8439b841f777dd99b4a9cca04d6935564b9409", + "sha256:17668ec6722b1b7a3a05cc0167659f6c95b436d25a36c2d52db0eca7d3f72593", + "sha256:3a584b28086bc93c888a6c2aa5c92ed1ae20932f078c46509a66dce9ea5533f2", + "sha256:4439be27e4eee76c7632c2427ca5e73703151b22cae23e64adb243a9c2f565d8", + "sha256:48e918b05850fffb070a496d2b5f97fc31d15d94ca33d3d08a4f86e26d4e7c5d", + "sha256:9102b59e8337f9874638fcfc9ac3734a0cfadb100e47d55c20d0dc6087fb4692", + "sha256:9b930776c0ae0c691776f4d2891ebc5362af86f152dd0da463a6614074cb1b02", + "sha256:b3b9ad80f8b68519cc3372a6ca85ae02cc5a8807723ac366b53c0f089db19e4a", + "sha256:bc2f976c0e918659f723401c4f834deb8a8e7798a71be4382e024bcc3f7e23a8", + "sha256:c22c75b5f394f3d47105045ea551e08a3e804dc7e01b37800ca35b58f856c3d6", + "sha256:c52ce2883dc193824989a9b97a76ca86ecd1fa7955b14f87bf367a61b6232511", + "sha256:ce584af5de8830d8701b8979b18fcf450cef9a382b1a3c8ef189bedc408faf1e", + "sha256:da456eeec17fa8aa4594d9a9f27c0b1060b6a75f2419fe0c00609587b2695f4a", + "sha256:db6db0f45d2c63ddb1a9d18d1b9b22f308e52c83638c26b422d520a815c4b3fb", + "sha256:df89642981b94e7db5596818499c4b2219028f2a528c9c37cc1de45bf2fd3a3f", + "sha256:f18d68f2be6bf0e89f1521af2b1bb46e66ab0018faafa81d70f358153170a317", + "sha256:f379b7f83f23fe12823085cd6b906edc49df969eb99757f58ff382349a3303c6" + ], + "markers": "python_version >= '3.5'", + "version": "==1.5.1" } }, "develop": { "appdirs": { "hashes": [ - "sha256:9e5896d1372858f8dd3344faf4e5014d21849c756c8d5701f78f8a103b372d92", - "sha256:d8b24664561d0d34ddfaec54636d502d7cea6e29c3eaf68f3df6180863e2166e" + "sha256:7d5d0167b2b1ba821647616af46a749d1c653740dd0d2415100fe26e27afdf41", + "sha256:a841dacd6b99318a741b166adb07e19ee71a274450e68237b4650ca1055ab128" ], - "version": "==1.4.3" + "version": "==1.4.4" }, "astroid": { "hashes": [ - "sha256:71ea07f44df9568a75d0f354c49143a4575d90645e9fead6dfb52c26a85ed13a", - "sha256:840947ebfa8b58f318d42301cf8c0a20fd794a33b61cc4638e28e9e61ba32f42" + "sha256:2f4078c2a41bf377eea06d71c9d2ba4eb8f6b1af2135bec27bbbb7d8f12bb703", + "sha256:bc58d83eb610252fd8de6363e39d4f1d0619c894b0ed24603b881c02e64c7386" ], - "version": "==2.3.3" + "markers": "python_version >= '3.5'", + "version": "==2.4.2" }, "attrs": { "hashes": [ - "sha256:08a96c641c3a74e44eb59afb61a24f2cb9f4d7188748e76ba4bb5edfa3cb7d1c", - "sha256:f7b7ce16570fe9965acd6d30101a28f62fb4a7f9e926b3bbc9b61f8b04247e72" + "sha256:26b54ddbbb9ee1d34d5d3668dd37d6cf74990ab23c828c2888dccdceee395594", + "sha256:fce7fc47dfc976152e82d53ff92fa0407700c21acd20886a13777a0d20e655dc" ], - "version": "==19.3.0" + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==20.2.0" }, "bandit": { "hashes": [ @@ -364,39 +358,59 @@ }, "black": { "hashes": [ - "sha256:09a9dcb7c46ed496a9850b76e4e825d6049ecd38b611f1224857a79bd985a8cf", - "sha256:68950ffd4d9169716bcb8719a56c07a2f4485354fec061cdd5910aa07369731c" + "sha256:1b30e59be925fafc1ee4565e5e08abef6b03fe455102883820fe5ee2e4734e0b", + "sha256:c2edb73a08e9e0e6f65a0e6af18b059b8b1cdd5bef997d7a0b181df93dc81539" ], "index": "pypi", - "version": "==19.3b0" + "version": "==19.10b0" }, "click": { "hashes": [ - "sha256:2335065e6395b9e67ca716de5f7526736bfa6ceead690adf616d925bdc622b13", - "sha256:5b94b49521f6456670fdb30cd82a4eca9412788a93fa6dd6df72c94d5a8ff2d7" + "sha256:d2b5255c7c6349bc1bd1e59e08cd12acbbd63ce649f2588755783aa94dfb6b1a", + "sha256:dacca89f4bfadd5de3d7489b7c8a566eee0d3676333fbb50030263894c38c0dc" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", + "version": "==7.1.2" + }, + "colorama": { + "hashes": [ + "sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b", + "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2" + ], + "index": "pypi", + "version": "==0.4.4" + }, + "flake8": { + "hashes": [ + "sha256:749dbbd6bfd0cf1318af27bf97a14e28e5ff548ef8e5b1566ccfb25a11e7c839", + "sha256:aadae8761ec651813c24be05c6f7b4680857ef6afaae4651a4eccaef97ce6c3b" ], - "version": "==7.0" + "index": "pypi", + "version": "==3.8.4" }, - "gitdb2": { + "gitdb": { "hashes": [ - "sha256:1b6df1433567a51a4a9c1a5a0de977aa351a405cc56d7d35f3388bad1f630350", - "sha256:96bbb507d765a7f51eb802554a9cfe194a174582f772e0d89f4e87288c288b7b" + "sha256:91f36bfb1ab7949b3b40e23736db18231bf7593edada2ba5c3a174a7b23657ac", + "sha256:c9e1f2d0db7ddb9a704c2a0217be31214e91a4fe1dea1efad19ae42ba0c285c9" ], - "version": "==2.0.6" + "markers": "python_version >= '3.4'", + "version": "==4.0.5" }, "gitpython": { "hashes": [ - "sha256:9c2398ffc3dcb3c40b27324b316f08a4f93ad646d5a6328cafbb871aa79f5e42", - "sha256:c155c6a2653593ccb300462f6ef533583a913e17857cfef8fc617c246b6dc245" + "sha256:6eea89b655917b500437e9668e4a12eabdcf00229a0df1762aabd692ef9b746b", + "sha256:befa4d101f91bad1b632df4308ec64555db684c360bd7d2130b4807d49ce86b8" ], - "version": "==3.0.5" + "markers": "python_version >= '3.4'", + "version": "==3.1.11" }, "isort": { "hashes": [ - "sha256:54da7e92468955c4fceacd0c86bd0ec997b0e1ee80d97f67c35a78b719dccab1", - "sha256:6e811fcb295968434526407adb8796944f1988c5b65e8139058f2014cbe100fd" + "sha256:dcab1d98b469a12a1a624ead220584391648790275560e1a43e54c5dceae65e7", + "sha256:dcaeec1b5f0eca77faea2a35ab790b4f3680ff75590bfcb7145986905aab2f58" ], - "version": "==4.3.21" + "markers": "python_version >= '3.6' and python_version < '4.0'", + "version": "==5.6.4" }, "lazy-object-proxy": { "hashes": [ @@ -422,6 +436,7 @@ "sha256:efa1909120ce98bbb3777e8b6f92237f5d5c8ea6758efea36a473e1d38f7d3e4", "sha256:f3900e8a5de27447acbf900b4750b0ddfd7ec1ea7fbaf11dfa911141bc522af0" ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==1.4.3" }, "mccabe": { @@ -431,96 +446,164 @@ ], "version": "==0.6.1" }, + "pathspec": { + "hashes": [ + "sha256:7d91249d21749788d07a2d0f94147accd8f845507400749ea19c1ec9054a12b0", + "sha256:da45173eb3a6f2a5a487efba21f050af2b41948be6ab52b6a1e3ff22bb8b7061" + ], + "version": "==0.8.0" + }, "pbr": { "hashes": [ - "sha256:139d2625547dbfa5fb0b81daebb39601c478c21956dc57e2e07b74450a8c506b", - "sha256:61aa52a0f18b71c5cc58232d2cf8f8d09cd67fcad60b742a60124cb8d6951488" + "sha256:5fad80b613c402d5b7df7bd84812548b2a61e9977387a80a5fc5c396492b13c9", + "sha256:b236cde0ac9a6aedd5e3c34517b423cd4fd97ef723849da6b0d2231142d89c00" ], - "version": "==5.4.4" + "markers": "python_version >= '2.6'", + "version": "==5.5.1" + }, + "pycodestyle": { + "hashes": [ + "sha256:2295e7b2f6b5bd100585ebcb1f616591b652db8a741695b3d8f5d28bdc934367", + "sha256:c58a7d2815e0e8d7972bf1803331fb0152f867bd89adf8a01dfd55085434192e" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==2.6.0" + }, + "pyflakes": { + "hashes": [ + "sha256:0d94e0e05a19e57a99444b6ddcf9a6eb2e5c68d3ca1e98e90707af8152c90a92", + "sha256:35b2d75ee967ea93b55750aa9edbbf72813e06a66ba54438df2cfac9e3c27fc8" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==2.2.0" }, "pylint": { "hashes": [ - "sha256:3db5468ad013380e987410a8d6956226963aed94ecb5f9d3a28acca6d9ac36cd", - "sha256:886e6afc935ea2590b462664b161ca9a5e40168ea99e5300935f6591ad467df4" + "sha256:bb4a908c9dadbc3aac18860550e870f58e1a02c9f2c204fdf5693d73be061210", + "sha256:bfe68f020f8a0fece830a22dd4d5dddb4ecc6137db04face4c3420a46a52239f" ], "index": "pypi", - "version": "==2.4.4" + "version": "==2.6.0" }, "pyyaml": { "hashes": [ - "sha256:0e7f69397d53155e55d10ff68fdfb2cf630a35e6daf65cf0bdeaf04f127c09dc", - "sha256:2e9f0b7c5914367b0916c3c104a024bb68f269a486b9d04a2e8ac6f6597b7803", - "sha256:35ace9b4147848cafac3db142795ee42deebe9d0dad885ce643928e88daebdcc", - "sha256:38a4f0d114101c58c0f3a88aeaa44d63efd588845c5a2df5290b73db8f246d15", - "sha256:483eb6a33b671408c8529106df3707270bfacb2447bf8ad856a4b4f57f6e3075", - "sha256:4b6be5edb9f6bb73680f5bf4ee08ff25416d1400fbd4535fe0069b2994da07cd", - "sha256:7f38e35c00e160db592091751d385cd7b3046d6d51f578b29943225178257b31", - "sha256:8100c896ecb361794d8bfdb9c11fce618c7cf83d624d73d5ab38aef3bc82d43f", - "sha256:c0ee8eca2c582d29c3c2ec6e2c4f703d1b7f1fb10bc72317355a746057e7346c", - "sha256:e4c015484ff0ff197564917b4b4246ca03f411b9bd7f16e02a2f586eb48b6d04", - "sha256:ebc4ed52dcc93eeebeae5cf5deb2ae4347b3a81c3fa12b0b8c976544829396a4" - ], - "version": "==5.2" + "sha256:06a0d7ba600ce0b2d2fe2e78453a470b5a6e000a985dd4a4e54e436cc36b0e97", + "sha256:240097ff019d7c70a4922b6869d8a86407758333f02203e0fc6ff79c5dcede76", + "sha256:4f4b913ca1a7319b33cfb1369e91e50354d6f07a135f3b901aca02aa95940bd2", + "sha256:69f00dca373f240f842b2931fb2c7e14ddbacd1397d57157a9b005a6a9942648", + "sha256:73f099454b799e05e5ab51423c7bcf361c58d3206fa7b0d555426b1f4d9a3eaf", + "sha256:74809a57b329d6cc0fdccee6318f44b9b8649961fa73144a98735b0aaf029f1f", + "sha256:7739fc0fa8205b3ee8808aea45e968bc90082c10aef6ea95e855e10abf4a37b2", + "sha256:95f71d2af0ff4227885f7a6605c37fd53d3a106fcab511b8860ecca9fcf400ee", + "sha256:b8eac752c5e14d3eca0e6dd9199cd627518cb5ec06add0de9d32baeee6fe645d", + "sha256:cc8955cfbfc7a115fa81d85284ee61147059a753344bc51098f3ccd69b0d7e0c", + "sha256:d13155f591e6fcc1ec3b30685d50bf0711574e2c0dfffd7644babf8b5102ca1a" + ], + "version": "==5.3.1" + }, + "regex": { + "hashes": [ + "sha256:0cb23ed0e327c18fb7eac61ebbb3180ebafed5b9b86ca2e15438201e5903b5dd", + "sha256:1a065e7a6a1b4aa851a0efa1a2579eabc765246b8b3a5fd74000aaa3134b8b4e", + "sha256:1a511470db3aa97432ac8c1bf014fcc6c9fbfd0f4b1313024d342549cf86bcd6", + "sha256:1c447b0d108cddc69036b1b3910fac159f2b51fdeec7f13872e059b7bc932be1", + "sha256:2278453c6a76280b38855a263198961938108ea2333ee145c5168c36b8e2b376", + "sha256:240509721a663836b611fa13ca1843079fc52d0b91ef3f92d9bba8da12e768a0", + "sha256:4e21340c07090ddc8c16deebfd82eb9c9e1ec5e62f57bb86194a2595fd7b46e0", + "sha256:570e916a44a361d4e85f355aacd90e9113319c78ce3c2d098d2ddf9631b34505", + "sha256:59d5c6302d22c16d59611a9fd53556554010db1d47e9df5df37be05007bebe75", + "sha256:6a46eba253cedcbe8a6469f881f014f0a98819d99d341461630885139850e281", + "sha256:6f567df0601e9c7434958143aebea47a9c4b45434ea0ae0286a4ec19e9877169", + "sha256:781906e45ef1d10a0ed9ec8ab83a09b5e0d742de70e627b20d61ccb1b1d3964d", + "sha256:8469377a437dbc31e480993399fd1fd15fe26f382dc04c51c9cb73e42965cc06", + "sha256:8cd0d587aaac74194ad3e68029124c06245acaeddaae14cb45844e5c9bebeea4", + "sha256:97a023f97cddf00831ba04886d1596ef10f59b93df7f855856f037190936e868", + "sha256:a973d5a7a324e2a5230ad7c43f5e1383cac51ef4903bf274936a5634b724b531", + "sha256:af360e62a9790e0a96bc9ac845d87bfa0e4ee0ee68547ae8b5a9c1030517dbef", + "sha256:b706c70070eea03411b1761fff3a2675da28d042a1ab7d0863b3efe1faa125c9", + "sha256:bfd7a9fddd11d116a58b62ee6c502fd24cfe22a4792261f258f886aa41c2a899", + "sha256:c30d8766a055c22e39dd7e1a4f98f6266169f2de05db737efe509c2fb9c8a3c8", + "sha256:c53dc8ee3bb7b7e28ee9feb996a0c999137be6c1d3b02cb6b3c4cba4f9e5ed09", + "sha256:c95d514093b80e5309bdca5dd99e51bcf82c44043b57c34594d9d7556bd04d05", + "sha256:d43cf21df524283daa80ecad551c306b7f52881c8d0fe4e3e76a96b626b6d8d8", + "sha256:d62205f00f461fe8b24ade07499454a3b7adf3def1225e258b994e2215fd15c5", + "sha256:e289a857dca3b35d3615c3a6a438622e20d1bf0abcb82c57d866c8d0be3f44c4", + "sha256:e5f6aa56dda92472e9d6f7b1e6331f4e2d51a67caafff4d4c5121cadac03941e", + "sha256:f4b1c65ee86bfbf7d0c3dfd90592a9e3d6e9ecd36c367c884094c050d4c35d04" + ], + "version": "==2020.10.23" }, "six": { "hashes": [ - "sha256:1f1b7d42e254082a9db6279deae68afb421ceba6158efa6131de7b3003ee93fd", - "sha256:30f610279e8b2578cab6db20741130331735c781b56053c59c4076da27f06b66" + "sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259", + "sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced" ], - "version": "==1.13.0" + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==1.15.0" }, - "smmap2": { + "smmap": { "hashes": [ - "sha256:0555a7bf4df71d1ef4218e4807bbf9b201f910174e6e08af2e138d4e517b4dde", - "sha256:29a9ffa0497e7f2be94ca0ed1ca1aa3cd4cf25a1f6b4f5f87f74b46ed91d609a" + "sha256:54c44c197c819d5ef1991799a7e30b662d1e520f2ac75c9efbeb54a742214cf4", + "sha256:9c98bbd1f9786d22f14b3d4126894d56befb835ec90cef151af566c7e19b5d24" ], - "version": "==2.0.5" + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==3.0.4" }, "stevedore": { "hashes": [ - "sha256:01d9f4beecf0fbd070ddb18e5efb10567801ba7ef3ddab0074f54e3cd4e91730", - "sha256:e0739f9739a681c7a1fda76a102b65295e96a144ccdb552f2ae03c5f0abe8a14" + "sha256:5e1ab03eaae06ef6ce23859402de785f08d97780ed774948ef16c4652c41bc62", + "sha256:f845868b3a3a77a2489d226568abe7328b5c2d4f6a011cc759dfa99144a521f0" ], - "version": "==1.31.0" + "markers": "python_version >= '3.6'", + "version": "==3.2.2" }, "toml": { "hashes": [ - "sha256:229f81c57791a41d65e399fc06bf0848bab550a9dfd5ed66df18ce5f05e73d5c", - "sha256:235682dd292d5899d361a811df37e04a8828a5b1da3115886b73cf81ebc9100e" + "sha256:926b612be1e5ce0634a2ca03470f95169cf16f939018233a670519cb4ac58b0f", + "sha256:bda89d5935c2eac546d648028b9901107a595863cb36bae0c73ac804a9b4ce88" ], - "version": "==0.10.0" + "version": "==0.10.1" }, "typed-ast": { "hashes": [ - "sha256:1170afa46a3799e18b4c977777ce137bb53c7485379d9706af8a59f2ea1aa161", - "sha256:18511a0b3e7922276346bcb47e2ef9f38fb90fd31cb9223eed42c85d1312344e", - "sha256:262c247a82d005e43b5b7f69aff746370538e176131c32dda9cb0f324d27141e", - "sha256:2b907eb046d049bcd9892e3076c7a6456c93a25bebfe554e931620c90e6a25b0", - "sha256:354c16e5babd09f5cb0ee000d54cfa38401d8b8891eefa878ac772f827181a3c", - "sha256:48e5b1e71f25cfdef98b013263a88d7145879fbb2d5185f2a0c79fa7ebbeae47", - "sha256:4e0b70c6fc4d010f8107726af5fd37921b666f5b31d9331f0bd24ad9a088e631", - "sha256:630968c5cdee51a11c05a30453f8cd65e0cc1d2ad0d9192819df9978984529f4", - "sha256:66480f95b8167c9c5c5c87f32cf437d585937970f3fc24386f313a4c97b44e34", - "sha256:71211d26ffd12d63a83e079ff258ac9d56a1376a25bc80b1cdcdf601b855b90b", - "sha256:7954560051331d003b4e2b3eb822d9dd2e376fa4f6d98fee32f452f52dd6ebb2", - "sha256:838997f4310012cf2e1ad3803bce2f3402e9ffb71ded61b5ee22617b3a7f6b6e", - "sha256:95bd11af7eafc16e829af2d3df510cecfd4387f6453355188342c3e79a2ec87a", - "sha256:bc6c7d3fa1325a0c6613512a093bc2a2a15aeec350451cbdf9e1d4bffe3e3233", - "sha256:cc34a6f5b426748a507dd5d1de4c1978f2eb5626d51326e43280941206c209e1", - "sha256:d755f03c1e4a51e9b24d899561fec4ccaf51f210d52abdf8c07ee2849b212a36", - "sha256:d7c45933b1bdfaf9f36c579671fec15d25b06c8398f113dab64c18ed1adda01d", - "sha256:d896919306dd0aa22d0132f62a1b78d11aaf4c9fc5b3410d3c666b818191630a", - "sha256:fdc1c9bbf79510b76408840e009ed65958feba92a88833cdceecff93ae8fff66", - "sha256:ffde2fbfad571af120fcbfbbc61c72469e72f550d676c3342492a9dfdefb8f12" - ], - "markers": "implementation_name == 'cpython' and python_version < '3.8'", - "version": "==1.4.0" + "sha256:0666aa36131496aed8f7be0410ff974562ab7eeac11ef351def9ea6fa28f6355", + "sha256:0c2c07682d61a629b68433afb159376e24e5b2fd4641d35424e462169c0a7919", + "sha256:0d8110d78a5736e16e26213114a38ca35cb15b6515d535413b090bd50951556d", + "sha256:249862707802d40f7f29f6e1aad8d84b5aa9e44552d2cc17384b209f091276aa", + "sha256:24995c843eb0ad11a4527b026b4dde3da70e1f2d8806c99b7b4a7cf491612652", + "sha256:269151951236b0f9a6f04015a9004084a5ab0d5f19b57de779f908621e7d8b75", + "sha256:3742b32cf1c6ef124d57f95be609c473d7ec4c14d0090e5a5e05a15269fb4d0c", + "sha256:4083861b0aa07990b619bd7ddc365eb7fa4b817e99cf5f8d9cf21a42780f6e01", + "sha256:498b0f36cc7054c1fead3d7fc59d2150f4d5c6c56ba7fb150c013fbc683a8d2d", + "sha256:4e3e5da80ccbebfff202a67bf900d081906c358ccc3d5e3c8aea42fdfdfd51c1", + "sha256:6daac9731f172c2a22ade6ed0c00197ee7cc1221aa84cfdf9c31defeb059a907", + "sha256:715ff2f2df46121071622063fc7543d9b1fd19ebfc4f5c8895af64a77a8c852c", + "sha256:73d785a950fc82dd2a25897d525d003f6378d1cb23ab305578394694202a58c3", + "sha256:7e4c9d7658aaa1fc80018593abdf8598bf91325af6af5cce4ce7c73bc45ea53d", + "sha256:8c8aaad94455178e3187ab22c8b01a3837f8ee50e09cf31f1ba129eb293ec30b", + "sha256:8ce678dbaf790dbdb3eba24056d5364fb45944f33553dd5869b7580cdbb83614", + "sha256:92c325624e304ebf0e025d1224b77dd4e6393f18aab8d829b5b7e04afe9b7a2c", + "sha256:aaee9905aee35ba5905cfb3c62f3e83b3bec7b39413f0a7f19be4e547ea01ebb", + "sha256:b52ccf7cfe4ce2a1064b18594381bccf4179c2ecf7f513134ec2f993dd4ab395", + "sha256:bcd3b13b56ea479b3650b82cabd6b5343a625b0ced5429e4ccad28a8973f301b", + "sha256:c9e348e02e4d2b4a8b2eedb48210430658df6951fa484e59de33ff773fbd4b41", + "sha256:d205b1b46085271b4e15f670058ce182bd1199e56b317bf2ec004b6a44f911f6", + "sha256:d43943ef777f9a1c42bf4e552ba23ac77a6351de620aa9acf64ad54933ad4d34", + "sha256:d5d33e9e7af3b34a40dc05f498939f0ebf187f07c385fd58d591c533ad8562fe", + "sha256:d648b8e3bf2fe648745c8ffcee3db3ff903d0817a01a12dd6a6ea7a8f4889072", + "sha256:f208eb7aff048f6bea9586e61af041ddf7f9ade7caed625742af423f6bae3298", + "sha256:fac11badff8313e23717f3dada86a15389d0708275bddf766cca67a84ead3e91", + "sha256:fc0fea399acb12edbf8a628ba8d2312f583bdbdb3335635db062fa98cf71fca4", + "sha256:fcf135e17cc74dbfbc05894ebca928ffeb23d9790b3167a674921db19082401f", + "sha256:fe460b922ec15dd205595c9b5b99e2f056fd98ae8f9f56b888e7a17dc2b757e7" + ], + "version": "==1.4.1" }, "wrapt": { "hashes": [ - "sha256:565a021fd19419476b9362b05eeaa094178de64f8361e44468f9e9d7843901e1" + "sha256:b62ffa81fb85f4332a4f609cab4ac40709470da05643a082ec1eb88e6d9b97d7" ], - "version": "==1.11.2" + "version": "==1.12.1" } } } diff --git a/Procfile b/Procfile index 5ae4640def..29cff6d9d1 100644 --- a/Procfile +++ b/Procfile @@ -1 +1 @@ -worker: pipenv run bot +worker: python bot.py diff --git a/README.md b/README.md index 52e4c4abd8..6173484788 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@
- +
@@ -31,10 +31,6 @@ Made with Python 3.7 - - - - @@ -50,25 +46,25 @@ ## What is Modmail? -Modmail is similar to Reddit's Modmail both in functionality and purpose. It serves as a shared inbox for server staff to communicate with their users in a seamless way. +Modmail is similar to Reddit's Modmail, both in functionality and purpose. It serves as a shared inbox for server staff to communicate with their users in a seamless way. This bot is free for everyone and always will be. If you like this project and would like to show your appreciation, you can support us on **[Patreon](https://www.patreon.com/kyber)**, cool benefits included! ## How does it work? -When a member sends a direct message to the bot, Modmail will create a channel or "thread" within an isolated category. All further DM messages will automatically relay to that channel, for any available staff can respond within the channel. +When a member sends a direct message to the bot, Modmail will create a channel or "thread" into a designated category. All further DM messages will automatically relay to that channel; any available staff can respond within the channel. -All threads are logged and you can view previous threads through their corresponding log link. Here is an [**example**](https://logs.logviewer.tech/example). +Our Logviewer will save the threads so you can view previous threads through their corresponding log link. Here is an [**example**](https://logs.logviewer.tech/example). ## Features * **Highly Customisable:** * Bot activity, prefix, category, log channel, etc. * Command permission system. - * Interface elements (color, responses, reactions, etc). + * Interface elements (color, responses, reactions, etc.). * Snippets and *command aliases*. * Minimum duration for accounts to be created before allowed to contact Modmail (`account_age`). - * Minimum duration for members to be in the guild before allowed to contact Modmail (`guild_age`). + * Minimum length for members to be in the guild before allowed to contact Modmail (`guild_age`). * **Advanced Logging Functionality:** * When you close a thread, Modmail will generate a [log link](https://logs.logviewer.tech/example) and post it to your log channel. @@ -90,11 +86,11 @@ This list is ever-growing thanks to active development and our exceptional contr Where can I find the Modmail bot invite link? -Unfortunately, due to how this bot functions, it cannot be invited. This is to ensure the individuality to your server and grant you full control over your bot and data. Nonetheless, you can easily obtain a free copy of Modmail for your server by following one of the methods listed below (roughly takes 15 minutes of your time)... +Unfortunately, due to how this bot functions, it cannot be invited. The lack of an invite link is to ensure an individuality to your server and grant you full control over your bot and data. Nonetheless, you can quickly obtain a free copy of Modmail for your server by following one of the methods listed below (roughly takes 15 minutes of your time). ### Heroku -This bot can be hosted on Heroku. +You can host this bot on Heroku. Installation via Heroku is possible with your web browser alone. The [**installation guide**](https://github.com/kyb3r/modmail/wiki/Installation) (which includes a video tutorial!) will guide you through the entire installation process. If you run into any problems, join our [Modmail Discord Server](https://discord.gg/etJNHCQ) for help and support. @@ -108,11 +104,11 @@ To configure automatic updates: ### Hosting for Patreons -If you don't want to go through the trouble of setting up your very own Modmail bot, and/or want to support this project, we offer the all inclusive installation, hosting and maintenance of your Modmail with [**Patreon**](https://patreon.com/kyber). Join our [Modmail Discord Server](https://discord.gg/etJNHCQ) for more info! +If you don't want to go through the trouble of setting up your very own Modmail bot or wish to support this project, we got a solution for you! We offer the complete installation, hosting, and maintenance of your Modmail with [**Patreon**](https://patreon.com/kyber). Join our [Modmail Discord Server](https://discord.gg/etJNHCQ) for more info! ### Locally -Local hosting of Modmail is also possible, first you will need [`Python 3.7`](https://www.python.org/downloads/). +Local hosting of Modmail is also possible. First, you will need [`Python 3.7`](https://www.python.org/downloads/release/python-376/). Follow the [**installation guide**](https://github.com/kyb3r/modmail/wiki/Installation) and disregard deploying the Heroku bot application. If you run into any problems, join our [Modmail Discord Server](https://discord.gg/etJNHCQ) for help and support. @@ -147,7 +143,7 @@ You can build your own Docker image: $ docker build . --tag=modmail ``` -or run directly from a pre-built version from https://hub.docker.com/. Currently there are two community release of Modmail: +Or run directly from a pre-built version from https://hub.docker.com/. - Kyber's: @@ -155,22 +151,12 @@ or run directly from a pre-built version from https://hub.docker.com/. Currently $ docker pull kyb3rr/modmail ``` -- Taku's: - -```console -$ docker pull taaku18/modmail -# You can also choose one of the following: -$ docker pull taaku18/modmail:dev -$ docker pull taaku18/modmail: ( ex: 3.2.0, 3.2, etc.) -``` - And to run your docker image: ```console -$ docker run --env-file .env user/modmail +$ docker run --env-file .env kyb3rr/modmail ``` -- Replace `user/modmail` with `kyb3rr/modmail`, `taaku18/modmail`, `taaku18/modmail:3.2`, etc as above. -- `.env` should be the path to your env file, you can also supply a path: `/path/to/.env`. +- `.env` should be the path to your env file; you can also supply a path: `/path/to/.env`. ## Sponsors @@ -180,18 +166,14 @@ Special thanks to our sponsors for supporting the project. - - - - Become a sponsor on [Patreon](https://patreon.com/kyber). ## Plugins Modmail supports the use of third-party plugins to extend or add functionalities to the bot. -This allows niche features as well as anything else outside of the scope of the core functionality of Modmail. +Plugins allow niche features as well as anything else outside of the scope of the core functionality of Modmail. -A list of third-party plugins can be found using the `?plugins registry` command or visit the [Unofficial List of Plugins](https://github.com/kyb3r/modmail/wiki/Unofficial-List-of-Plugins) for a list of plugins contributed by the community. +You can find a list of third-party plugins using the `?plugins registry` command or visit the [Unofficial List of Plugins](https://github.com/kyb3r/modmail/wiki/Unofficial-List-of-Plugins) for a list of plugins contributed by the community. To develop your own, check out the [plugins documentation](https://github.com/kyb3r/modmail/wiki/Plugins). @@ -199,6 +181,6 @@ Plugins requests and support is available in our [Modmail Plugins Server](https: ## Contributing -Contributions to Modmail are always welcome, whether it be improvements to the documentation or new functionality, please feel free to make the change. Check out our contribution [guidelines](https://github.com/kyb3r/modmail/blob/master/CONTRIBUTING.md) before you get started. +Contributions to Modmail are always welcome, whether it be improvements to the documentation or new functionality, please feel free to make the change. Check out our [contributing guidelines](https://github.com/kyb3r/modmail/blob/master/CONTRIBUTING.md) before you get started. If you like this project and would like to show your appreciation, support us on **[Patreon](https://www.patreon.com/kyber)**! diff --git a/SPONSORS.json b/SPONSORS.json index d30134e015..ceed37be23 100644 --- a/SPONSORS.json +++ b/SPONSORS.json @@ -26,42 +26,5 @@ } ] } - }, - { - "embed": { - "title": "Hey there!", - "description": "Nice to see you here! You can support us by subscribing on youtube -> [Youtube](https://www.youtube.com/user/RoomieOfficial) <- and also join our [Discord](https://discord.gg/zaeVCaV)!", - "url": "https://discord.gg/zaeVCaV", - "color": 13003681, - "footer": { - "icon_url": "https://imgur.com/Mrc9pLd.gif", - "text": "everyone is a clown" - }, - "thumbnail": { - "url": "https://imgur.com/Mrc9pLd.gif" - }, - "image": { - "url": "https://imgur.com/ZUFiL6b.gif" - }, - "author": { - "name": "Roomieofficial", - "url": "https://discord.gg/zaeVCaV", - "icon_url": "https://imgur.com/6hBkt7Z.png" - }, - "fields": [ - { - "name": "What is all this about πŸ€”", - "value": "We are mainly focused on everything that has anything to do with music or singing! " - }, - { - "name": "Youtube πŸ™„", - "value": "U will get great content if you follow our [Youtube](https://www.youtube.com/user/RoomieOfficial) with weekly uploads." - }, - { - "name": "Discord 😁", - "value": "Make sure to join our [Discord](https://discord.gg/zaeVCaV) We have weekly events, 24/7 chats and more!" - } - ] - } } ] diff --git a/app.json b/app.json index 76fe320a30..41697cf064 100644 --- a/app.json +++ b/app.json @@ -1,27 +1,35 @@ { - "name": "Modmail", - "description": "An easy to install Modmail bot for Discord - DM to contact mods!", - "repository": "https://github.com/kyb3r/modmail", - "env": { - "TOKEN": { - "description": "Your discord bot's token.", - "required": true - }, - "GUILD_ID": { - "description": "The id for the server you are hosting this bot for.", - "required": true - }, - "OWNERS": { - "description": "Comma separated user IDs of people that are allowed to use owner only commands. (eval).", - "required": true - }, - "MONGO_URI": { - "description": "Mongo DB connection URI for self-hosting your data.", - "required": true - }, - "LOG_URL": { - "description": "The url of the log viewer app for viewing self-hosted logs.", - "required": true + "name": "Modmail", + "description": "An easy to install Modmail bot for Discord - DM to contact mods!", + "repository": "https://github.com/kyb3r/modmail", + "env": { + "TOKEN": { + "description": "Your discord bot's token.", + "required": true + }, + "GUILD_ID": { + "description": "The id for the server you are hosting this bot for.", + "required": true + }, + "MODMAIL_GUILD_ID": { + "description": "The ID of the discord server where the threads channels should be created (receiving server). Default to GUILD_ID.", + "required": false + }, + "OWNERS": { + "description": "Comma separated user IDs of people that are allowed to use owner only commands. (eval).", + "required": true + }, + "CONNECTION_URI": { + "description": "The connection URI for your database.", + "required": true + }, + "DATABASE_TYPE": { + "description": "The type of your database. There is only one supported database at the moment - MongoDB (default).", + "required": false + }, + "LOG_URL": { + "description": "The url of the log viewer app for viewing self-hosted logs.", + "required": true + } } - } -} +} \ No newline at end of file diff --git a/bot.py b/bot.py index a379567b4d..ab5ded6c53 100644 --- a/bot.py +++ b/bot.py @@ -1,4 +1,4 @@ -__version__ = "3.5.0-dev0" +__version__ = "3.6.3-dev0" import asyncio @@ -18,8 +18,6 @@ from aiohttp import ClientSession from emoji import UNICODE_EMOJI -from motor.motor_asyncio import AsyncIOMotorClient -from pymongo.errors import ConfigurationError from pkg_resources import parse_version @@ -31,8 +29,8 @@ except ImportError: pass -from core import checks, translations -from core.clients import ApiClient, PluginDatabaseClient +from core import checks +from core.clients import ApiClient, PluginDatabaseClient, MongoDBClient from core.config import ConfigManager from core.utils import human_join, normalize_alias from core.models import PermissionLevel, SafeFormatter, getLogger, configure_logging @@ -46,10 +44,17 @@ if not os.path.exists(temp_dir): os.mkdir(temp_dir) +if sys.platform == "win32": + try: + asyncio.set_event_loop_policy(asyncio.WindowsProactorEventLoopPolicy()) + except AttributeError: + logger.error("Failed to use WindowsProactorEventLoopPolicy.", exc_info=True) + class ModmailBot(commands.Bot): def __init__(self): - super().__init__(command_prefix=None) # implemented in `get_prefix` + intents = discord.Intents.all() + super().__init__(command_prefix=None, intents=intents) # implemented in `get_prefix` self._session = None self._api = None self.metadata_loop = None @@ -66,22 +71,7 @@ def __init__(self): self.log_file_name = os.path.join(temp_dir, f"{self.token.split('.')[0]}.log") self._configure_logging() - mongo_uri = self.config["mongo_uri"] - if mongo_uri is None: - logger.critical("A Mongo URI is necessary for the bot to function.") - raise RuntimeError - - try: - self.db = AsyncIOMotorClient(mongo_uri).modmail_bot - except ConfigurationError as e: - logger.critical( - "Your MONGO_URI might be copied wrong, try re-copying from the source again. " - "Otherwise noted in the following message:" - ) - logger.critical(e) - sys.exit(0) - - self.plugin_db = PluginDatabaseClient(self) + self.plugin_db = PluginDatabaseClient(self) # Deprecated self.startup() @property @@ -100,9 +90,12 @@ def uptime(self) -> str: def startup(self): logger.line() - logger.info("β”Œβ”¬β”β”Œβ”€β”β”Œβ”¬β”β”Œβ”¬β”β”Œβ”€β”β”¬β”¬") - logger.info("β”‚β”‚β”‚β”‚ β”‚ β”‚β”‚β”‚β”‚β”‚β”œβ”€β”€β”‚β”‚") - logger.info("β”΄ β”΄β””β”€β”˜β”€β”΄β”˜β”΄ β”΄β”΄ β”΄β”΄β”΄β”€β”˜") + if os.name != "nt": + logger.info("β”Œβ”¬β”β”Œβ”€β”β”Œβ”¬β”β”Œβ”¬β”β”Œβ”€β”β”¬β”¬") + logger.info("β”‚β”‚β”‚β”‚ β”‚ β”‚β”‚β”‚β”‚β”‚β”œβ”€β”€β”‚β”‚") + logger.info("β”΄ β”΄β””β”€β”˜β”€β”΄β”˜β”΄ β”΄β”΄ β”΄β”΄β”΄β”€β”˜") + else: + logger.info("MODMAIL") logger.info("v%s", __version__) logger.info("Authors: kyb3r, fourjr, Taaku18") logger.line() @@ -152,9 +145,18 @@ def session(self) -> ClientSession: @property def api(self) -> ApiClient: if self._api is None: - self._api = ApiClient(self) + if self.config["database_type"].lower() == "mongodb": + self._api = MongoDBClient(self) + else: + logger.critical("Invalid database type.") + raise RuntimeError return self._api + @property + def db(self): + # deprecated + return self.api.db + async def get_prefix(self, message=None): return [self.prefix, f"<@{self.user.id}> ", f"<@!{self.user.id}> "] @@ -165,6 +167,10 @@ def run(self, *args, **kwargs): pass except discord.LoginFailure: logger.critical("Invalid token") + except discord.PrivilegedIntentsRequired: + logger.critical( + "Privileged intents are not explicitly granted in the discord developers dashboard." + ) except Exception: logger.critical("Fatal exception", exc_info=True) finally: @@ -180,7 +186,7 @@ def run(self, *args, **kwargs): logger.error(" - Shutting down bot - ") @property - def owner_ids(self): + def bot_owner_ids(self): owner_ids = self.config["owners"] if owner_ids is not None: owner_ids = set(map(int, str(owner_ids).split(","))) @@ -192,7 +198,7 @@ def owner_ids(self): return owner_ids async def is_owner(self, user: discord.User) -> bool: - if user.id in self.owner_ids: + if user.id in self.bot_owner_ids: return True return await super().is_owner(user) @@ -370,37 +376,16 @@ def command_perm(self, command_name: str) -> PermissionLevel: async def on_connect(self): try: - await self.validate_database_connection() + await self.api.validate_database_connection() except Exception: logger.debug("Logging out due to failed database connection.") return await self.logout() logger.debug("Connected to gateway.") await self.config.refresh() - await self.setup_indexes() + await self.api.setup_indexes() self._connected.set() - async def setup_indexes(self): - """Setup text indexes so we can use the $search operator""" - coll = self.db.logs - index_name = "messages.content_text_messages.author.name_text_key_text" - - index_info = await coll.index_information() - - # Backwards compatibility - old_index = "messages.content_text_messages.author.name_text" - if old_index in index_info: - logger.info("Dropping old index: %s", old_index) - await coll.drop_index(old_index) - - if index_name not in index_info: - logger.info('Creating "text" index for logs collection.') - logger.info("Name: %s", index_name) - await coll.create_index( - [("messages.content", "text"), ("messages.author.name", "text"), ("key", "text")] - ) - logger.debug("Successfully configured and verified database indexes.") - async def on_ready(self): """Bot startup, sets uptime.""" @@ -416,7 +401,8 @@ async def on_ready(self): logger.info("Logged in as: %s", self.user) logger.info("Bot ID: %s", self.user.id) owners = ", ".join( - getattr(self.get_user(owner_id), "name", str(owner_id)) for owner_id in self.owner_ids + getattr(self.get_user(owner_id), "name", str(owner_id)) + for owner_id in self.bot_owner_ids ) logger.info("Owners: %s", owners) logger.info("Prefix: %s", self.prefix) @@ -469,7 +455,7 @@ async def on_ready(self): { "open": False, "closed_at": str(datetime.utcnow()), - "close_message": _("Channel has been deleted, no closer found."), + "close_message": "Channel has been deleted, no closer found.", "closer": { "id": str(self.user.id), "name": self.user.name, @@ -498,6 +484,17 @@ async def on_ready(self): self.metadata_loop.before_loop(self.before_post_metadata) self.metadata_loop.start() + other_guilds = [ + guild for guild in self.guilds if guild not in {self.guild, self.modmail_guild} + ] + if any(other_guilds): + logger.warning( + "The bot is in more servers other than the main and staff server." + "This may cause data compromise (%s).", + ", ".join(guild.name for guild in other_guilds), + ) + logger.warning("If the external servers are valid, you may ignore this message.") + async def convert_emoji(self, name: str) -> str: ctx = SimpleNamespace(bot=self, guild=self.modmail_guild) converter = commands.EmojiConverter() @@ -549,9 +546,7 @@ def check_account_age(self, author: discord.Member) -> bool: logger.debug("Blocked due to account age, user %s.", author.name) if str(author.id) not in self.blocked_users: - new_reason = _("System Message: New Account. Required to wait for {time}.").format( - time=delta - ) + new_reason = f"System Message: New Account. Required to wait for {delta}." self.blocked_users[str(author.id)] = new_reason return False @@ -617,7 +612,7 @@ def check_manual_blocked(self, author: discord.Member) -> bool: return False async def _process_blocked(self, message): - x, blocked_emoji = await self.retrieve_emoji() + _, blocked_emoji = await self.retrieve_emoji() if await self.is_blocked(message.author, channel=message.channel, send_message=True): await self.add_reaction(message, blocked_emoji) return True @@ -937,7 +932,7 @@ async def on_typing(self, channel, user, _): return await thread.recipient.trigger_typing() - async def handle_reaction_events(self, payload, *, add): + async def handle_reaction_events(self, payload): user = self.get_user(payload.user_id) if user.bot: return @@ -963,7 +958,7 @@ async def handle_reaction_events(self, payload, *, add): if not thread: return if ( - add + payload.event_type == "REACTION_ADD" and message.embeds and str(reaction) == str(close_emoji) and self.config.get("recipient_thread_close") @@ -987,14 +982,14 @@ async def handle_reaction_events(self, payload, *, add): if not thread: return try: - x, linked_message = await thread.find_linked_messages( + _, linked_message = await thread.find_linked_messages( message.id, either_direction=True ) except ValueError as e: logger.warning("Failed to find linked message for reactions: %s", e) return - if add: + if payload.event_type == "REACTION_ADD": if await self.add_reaction(linked_message, reaction): await self.add_reaction(message, reaction) else: @@ -1005,28 +1000,15 @@ async def handle_reaction_events(self, payload, *, add): logger.warning("Failed to remove reaction: %s", e) async def on_raw_reaction_add(self, payload): - await self.handle_reaction_events(payload, add=True) + await self.handle_reaction_events(payload) async def on_raw_reaction_remove(self, payload): - await self.handle_reaction_events(payload, add=False) + await self.handle_reaction_events(payload) async def on_guild_channel_delete(self, channel): if channel.guild != self.modmail_guild: return - try: - audit_logs = self.modmail_guild.audit_logs() - entry = await audit_logs.find(lambda a: a.target == channel) - mod = entry.user - except AttributeError as e: - # discord.py broken implementation with discord API - # TODO: waiting for dpy - logger.warning("Failed to retrieve audit log: %s.", e) - return - - if mod == self.user: - return - if isinstance(channel, discord.CategoryChannel): if self.main_category == channel: logger.debug("Main category was deleted.") @@ -1043,6 +1025,19 @@ async def on_guild_channel_delete(self, channel): await self.config.update() return + audit_logs = self.modmail_guild.audit_logs( + limit=10, action=discord.AuditLogAction.channel_delete + ) + entry = await audit_logs.find(lambda a: int(a.target.id) == channel.id) + + if entry is None: + logger.debug("Cannot find the audit log entry for channel delete of %d.", channel.id) + return + + mod = entry.user + if mod == self.user: + return + thread = await self.threads.find(channel=channel) if thread and thread.channel == channel: logger.debug("Manually closed channel %s.", channel.name) @@ -1070,20 +1065,60 @@ async def on_member_join(self, member): async def on_message_delete(self, message): """Support for deleting linked messages""" - # TODO: use audit log to check if modmail deleted the message - if message.embeds and not isinstance(message.channel, discord.DMChannel): - thread = await self.threads.find(channel=message.channel) + + if message.is_system(): + return + + if isinstance(message.channel, discord.DMChannel): + if message.author == self.user: + return + thread = await self.threads.find(recipient=message.author) + if not thread: + return try: - await thread.delete_message(message) + message = await thread.find_linked_message_from_dm(message) except ValueError as e: - if str(e) not in {"DM message not found.", " Malformed thread message."}: - logger.warning("Failed to find linked message to delete: %s", e) - else: - thread = await self.threads.find(recipient=message.author) - message = await thread.find_linked_message_from_dm(message) + if str(e) != "Thread channel message not found.": + logger.debug("Failed to find linked message to delete: %s", e) + return embed = message.embeds[0] embed.set_footer(text=f"{embed.footer.text} (deleted)", icon_url=embed.footer.icon_url) await message.edit(embed=embed) + return + + if message.author != self.user: + return + + thread = await self.threads.find(channel=message.channel) + if not thread: + return + + audit_logs = self.modmail_guild.audit_logs( + limit=10, action=discord.AuditLogAction.message_delete + ) + + entry = await audit_logs.find(lambda a: a.target == self.user) + + if entry is None: + return + + try: + await thread.delete_message(message, note=False) + embed = discord.Embed( + description="Successfully deleted message.", color=self.main_color + ) + except ValueError as e: + if str(e) not in {"DM message not found.", "Malformed thread message."}: + logger.debug("Failed to find linked message to delete: %s", e) + embed = discord.Embed( + description="Failed to delete message.", color=self.error_color + ) + else: + return + except discord.NotFound: + return + embed.set_footer(text=f"Message ID: {message.id} from {message.author}.") + return await message.channel.send(embed=embed) async def on_bulk_message_delete(self, messages): await discord.utils.async_all(self.on_message_delete(msg) for msg in messages) @@ -1096,10 +1131,13 @@ async def on_message_edit(self, before, after): if isinstance(after.channel, discord.DMChannel): thread = await self.threads.find(recipient=before.author) + if not thread: + return + try: await thread.edit_dm_message(after, after.content) except ValueError: - x, blocked_emoji = await self.retrieve_emoji() + _, blocked_emoji = await self.retrieve_emoji() await self.add_reaction(after, blocked_emoji) else: embed = discord.Embed( @@ -1147,45 +1185,17 @@ async def on_command_error(self, context, exception): corrected_permission_level.name, ) logger.warning("CheckFailure: %s", exception) + elif isinstance(exception, commands.DisabledCommand): + logger.info( + "DisabledCommand: %s is trying to run eval but it's disabled", context.author.name + ) else: logger.error("Unexpected exception:", exc_info=exception) - async def validate_database_connection(self): - try: - await self.db.command("buildinfo") - except Exception as exc: - logger.critical("Something went wrong while connecting to the database.") - message = f"{type(exc).__name__}: {str(exc)}" - logger.critical(message) - - if "ServerSelectionTimeoutError" in message: - logger.critical( - "This may have been caused by not whitelisting " - "IPs correctly. Make sure to whitelist all " - "IPs (0.0.0.0/0) https://i.imgur.com/mILuQ5U.png" - ) - - if "OperationFailure" in message: - logger.critical( - "This is due to having invalid credentials in your MONGO_URI. " - "Remember you need to substitute `` with your actual password." - ) - logger.critical( - "Be sure to URL encode your username and password (not the entire URL!!), " - "https://www.urlencoder.io/, if this issue persists, try changing your username and password " - "to only include alphanumeric characters, no symbols." - "" - ) - raise - else: - logger.debug("Successfully connected to the database.") - logger.line("debug") - async def post_metadata(self): - owner = (await self.application_info()).owner + info = await self.application_info() + data = { - "owner_name": str(owner), - "owner_id": owner.id, "bot_id": self.user.id, "bot_name": str(self.user), "avatar_url": str(self.user.avatar_url), @@ -1199,7 +1209,20 @@ async def post_metadata(self): "last_updated": str(datetime.utcnow()), } - async with self.session.post("https://api.logviewer.tech/metadata", json=data): + if info.team is not None: + data.update( + { + "owner_name": info.team.owner.name + if info.team.owner is not None + else "No Owner", + "owner_id": info.team.owner_id, + "team": True, + } + ) + else: + data.update({"owner_name": info.owner.name, "owner_id": info.owner.id, "team": False}) + + async with self.session.post("https://api.modmail.dev/metadata", json=data): logger.debug("Uploading metadata to Modmail server.") async def before_post_metadata(self): @@ -1220,7 +1243,6 @@ def main(): except ImportError: pass - translations.init() bot = ModmailBot() bot.run() diff --git a/cogs/modmail.py b/cogs/modmail.py index 76a28b3c9c..44f0e9dcde 100644 --- a/cogs/modmail.py +++ b/cogs/modmail.py @@ -41,23 +41,17 @@ async def setup(self, ctx): if ctx.guild != self.bot.modmail_guild: return await ctx.send( - _( - "You can only setup in the Modmail guild: {guild_name}.".format( - guild_name=self.bot.modmail_guild - ) - ) + f"You can only setup in the Modmail guild: {self.bot.modmail_guild}." ) if self.bot.main_category is not None: logger.debug("Can't re-setup server, main_category is found.") - return await ctx.send( - _("{guild_name} is already set up.").format(guild_name=self.bot.modmail_guild) - ) + return await ctx.send(f"{self.bot.modmail_guild} is already set up.") if self.bot.modmail_guild is None: embed = discord.Embed( - title=_("Error"), - description=_("Modmail functioning guild not found."), + title="Error", + description="Modmail functioning guild not found.", color=self.bot.error_color, ) return await ctx.send(embed=embed) @@ -94,29 +88,21 @@ async def setup(self, ctx): ) embed = discord.Embed( - title=_("Friendly Reminder"), - description=_( - "You may use the `{prefix}config set log_channel_id " - "` command to set up a custom log channel, then you can delete this default " - "{log_channel} log channel." - ).format(prefix=self.bot.prefix, log_channel=log_channel.mention), + title="Friendly Reminder", + description=f"You may use the `{self.bot.prefix}config set log_channel_id " + "` command to set up a custom log channel, then you can delete this default " + f"{log_channel.mention} log channel.", color=self.bot.main_color, ) embed.add_field( - name=_("Thanks for using the bot!"), - value=_( - "If you like what you see, consider giving the " - "[repo a star](https://github.com/kyb3r/modmail) :star: or if you are " - "feeling generous, check us out on [Patreon](https://patreon.com/kyber)!" - ), + name="Thanks for using our bot!", + value="If you like what you see, consider giving the " + "[repo a star](https://github.com/kyb3r/modmail) :star: and if you are " + "feeling extra generous, buy us coffee on [Patreon](https://patreon.com/kyber) :heart:!", ) - embed.set_footer( - text=_('Type "{prefix}help" for a complete list of commands.').format( - prefix=self.bot.prefix - ) - ) + embed.set_footer(text=f'Type "{self.bot.prefix}help" for a complete list of commands.') await log_channel.send(embed=embed) self.bot.config["main_category_id"] = category.id @@ -124,20 +110,18 @@ async def setup(self, ctx): await self.bot.config.update() await ctx.send( - _( - "**Successfully set up server.**\n" - "Consider setting permission levels " - "to give access to roles or users the ability to use Modmail.\n\n" - "Type:\n- `{prefix}permissions` and `{prefix}permissions add` " - "for more info on setting permissions.\n" - "- `{prefix}config help` for a list of available customizations." - ).format(prefix=self.bot.prefix) + "**Successfully set up server.**\n" + "Consider setting permission levels to give access to roles " + "or users the ability to use Modmail.\n\n" + f"Type:\n- `{self.bot.prefix}permissions` and `{self.bot.prefix}permissions add` " + "for more info on setting permissions.\n" + f"- `{self.bot.prefix}config help` for a list of available customizations." ) if not self.bot.config["command_permissions"] and not self.bot.config["level_permissions"]: await self.bot.update_perms(PermissionLevel.REGULAR, -1) - for owner_ids in self.bot.owner_ids: - await self.bot.update_perms(PermissionLevel.OWNER, owner_ids) + for owner_id in self.bot.bot_owner_ids: + await self.bot.update_perms(PermissionLevel.OWNER, owner_id) @commands.group(aliases=["snippets"], invoke_without_command=True) @checks.has_permissions(PermissionLevel.SUPPORTER) @@ -186,7 +170,7 @@ async def snippet(self, ctx, *, name: str.lower = None): for i, names in enumerate(zip_longest(*(iter(sorted(self.bot.snippets)),) * 15)): description = format_description(i, names) embed = discord.Embed(color=self.bot.main_color, description=description) - embed.set_author(name=_("Snippets"), icon_url=ctx.guild.icon_url) + embed.set_author(name="Snippets", icon_url=ctx.guild.icon_url) embeds.append(embed) session = EmbedPaginatorSession(ctx, *embeds) @@ -228,27 +212,25 @@ async def snippet_add(self, ctx, name: str.lower, *, value: commands.clean_conte """ if name in self.bot.snippets: embed = discord.Embed( - title=_("Error"), + title="Error", color=self.bot.error_color, - description=_("Snippet `{name}` already exists.").format(name=name), + description=f"Snippet `{name}` already exists.", ) return await ctx.send(embed=embed) if name in self.bot.aliases: embed = discord.Embed( - title=_("Error"), + title="Error", color=self.bot.error_color, - description=_("An alias with the same name already exists: `{name}`.").format( - name=name - ), + description=f"An alias that shares the same name exists: `{name}`.", ) return await ctx.send(embed=embed) if len(name) > 120: embed = discord.Embed( - title=_("Error"), + title="Error", color=self.bot.error_color, - description=_("Snippet names cannot be longer than 120 characters."), + description="Snippet names cannot be longer than 120 characters.", ) return await ctx.send(embed=embed) @@ -269,9 +251,9 @@ async def snippet_remove(self, ctx, *, name: str.lower): if name in self.bot.snippets: embed = discord.Embed( - title=_("Removed snippet"), + title="Removed snippet", color=self.bot.main_color, - description=_("Snippet `{name}` is now deleted.").format(name=name), + description=f"Snippet `{name}` is now deleted.", ) self.bot.snippets.pop(name) await self.bot.config.update() @@ -294,42 +276,63 @@ async def snippet_edit(self, ctx, name: str.lower, *, value): await self.bot.config.update() embed = discord.Embed( - title=_("Edited snippet"), + title="Edited snippet", color=self.bot.main_color, - description=f_('`{name}` will now send "{value}".'), + description=f'`{name}` will now send "{value}".', ) else: embed = create_not_found_embed(name, self.bot.snippets.keys(), "Snippet") await ctx.send(embed=embed) - @commands.command() + @commands.command(usage=" [options]") @checks.has_permissions(PermissionLevel.MODERATOR) @checks.thread_only() - async def move(self, ctx, category: discord.CategoryChannel, *, specifics: str = None): + async def move(self, ctx, *, arguments): """ Move a thread to another category. `category` may be a category ID, mention, or name. - `specifics` is a string which takes in arguments on how to perform the move. Ex: "silently" + `options` is a string which takes in arguments on how to perform the move. Ex: "silently" """ + split_args = arguments.strip('"').split(" ") + + # manually parse arguments, consumes as much of args as possible for category + for i in range(len(split_args)): + try: + if i == 0: + fmt = arguments + else: + fmt = " ".join(split_args[:-i]) + + category = await commands.CategoryChannelConverter().convert(ctx, fmt) + except commands.BadArgument: + if i == len(split_args) - 1: + # last one + raise + pass + else: + break + + options = " ".join(arguments.split(" ")[-i:]) + thread = ctx.thread silent = False - if specifics: + if options: silent_words = ["silent", "silently"] - silent = any(word in silent_words for word in specifics.split()) + silent = any(word in silent_words for word in options.split()) await thread.channel.edit(category=category, sync_permissions=True) if self.bot.config["thread_move_notify"] and not silent: embed = discord.Embed( - title=_("Thread Moved"), + title=self.bot.config["thread_move_title"], description=self.bot.config["thread_move_response"], color=self.bot.main_color, ) await thread.recipient.send(embed=embed) - sent_emoji, x = await self.bot.retrieve_emoji() + sent_emoji, _ = await self.bot.retrieve_emoji() await self.bot.add_reaction(ctx.message, sent_emoji) async def send_scheduled_close_message(self, ctx, after, silent=False): @@ -338,17 +341,15 @@ async def send_scheduled_close_message(self, ctx, after, silent=False): silent = "*silently* " if silent else "" embed = discord.Embed( - title=_("Scheduled close"), - description=_("This thread will close {silent}in {time}.").format( - silent=silent, time=human_delta - ), + title="Scheduled close", + description=f"This thread will close {silent}in {human_delta}.", color=self.bot.error_color, ) if after.arg and not silent: - embed.add_field(name=_("Message"), value=after.arg) + embed.add_field(name="Message", value=after.arg) - embed.set_footer(text=_("Closing will be cancelled if a thread message is sent.")) + embed.set_footer(text="Closing will be cancelled if a thread message is sent.") embed.timestamp = after.dt await ctx.send(embed=embed) @@ -382,21 +383,20 @@ async def close(self, ctx, *, after: UserFriendlyTime = None): close_after = (after.dt - now).total_seconds() if after else 0 message = after.arg if after else None - silent = str(message).lower() in {_("silent"), _("silently")} - cancel = str(message).lower() == _("cancel") + silent = str(message).lower() in {"silent", "silently"} + cancel = str(message).lower() == "cancel" if cancel: if thread.close_task is not None or thread.auto_close_task is not None: await thread.cancel_closure(all=True) embed = discord.Embed( - color=self.bot.error_color, - description=_("Scheduled close has been cancelled."), + color=self.bot.error_color, description="Scheduled close has been cancelled." ) else: embed = discord.Embed( color=self.bot.error_color, - description=_("This thread has not already been scheduled to close."), + description="This thread has not already been scheduled to close.", ) return await ctx.send(embed=embed) @@ -446,18 +446,14 @@ async def notify( if mention in mentions: embed = discord.Embed( color=self.bot.error_color, - description=_("{mention} is already going to be mentioned.").format( - mention=mention - ), + description=f"{mention} is already going to be mentioned.", ) else: mentions.append(mention) await self.bot.config.update() embed = discord.Embed( color=self.bot.main_color, - description=_( - "{mention} will be mentioned " "on the next message received." - ).format(mention=mention), + description=f"{mention} will be mentioned on the next message received.", ) return await ctx.send(embed=embed) @@ -488,16 +484,13 @@ async def unnotify( if mention not in mentions: embed = discord.Embed( color=self.bot.error_color, - description=_("{mention} does not have a pending notification.").format( - mention=mention - ), + description=f"{mention} does not have a pending notification.", ) else: mentions.remove(mention) await self.bot.config.update() embed = discord.Embed( - color=self.bot.main_color, - description=_("{mention} will no longer be notified.").format(mention=mention), + color=self.bot.main_color, description=f"{mention} will no longer be notified." ) return await ctx.send(embed=embed) @@ -530,18 +523,14 @@ async def subscribe( if mention in mentions: embed = discord.Embed( color=self.bot.error_color, - description=_("{mention} is already subscribed to this thread.").format( - mention=mention - ), + description=f"{mention} is not subscribed to this thread.", ) else: mentions.append(mention) await self.bot.config.update() embed = discord.Embed( color=self.bot.main_color, - description=_( - "{mention} will now be " "notified of all messages received." - ).format(mention=mention), + description=f"{mention} will now be notified of all messages received.", ) return await ctx.send(embed=embed) @@ -572,18 +561,14 @@ async def unsubscribe( if mention not in mentions: embed = discord.Embed( color=self.bot.error_color, - description=_("{mention} is not already subscribed to this thread.").format( - mention=mention - ), + description=f"{mention} is not subscribed to this thread.", ) else: mentions.remove(mention) await self.bot.config.update() embed = discord.Embed( color=self.bot.main_color, - description=_("{mention} is now unsubscribed to this thread.").format( - mention=mention - ), + description=f"{mention} is now unsubscribed from this thread.", ) return await ctx.send(embed=embed) @@ -593,7 +578,7 @@ async def unsubscribe( async def nsfw(self, ctx): """Flags a Modmail thread as NSFW (not safe for work).""" await ctx.channel.edit(nsfw=True) - sent_emoji, x = await self.bot.retrieve_emoji() + sent_emoji, _ = await self.bot.retrieve_emoji() await self.bot.add_reaction(ctx.message, sent_emoji) @commands.command() @@ -602,7 +587,7 @@ async def nsfw(self, ctx): async def sfw(self, ctx): """Flags a Modmail thread as SFW (safe for work).""" await ctx.channel.edit(nsfw=False) - sent_emoji, x = await self.bot.retrieve_emoji() + sent_emoji, _ = await self.bot.retrieve_emoji() await self.bot.add_reaction(ctx.message, sent_emoji) @commands.command() @@ -635,26 +620,24 @@ def format_log_embeds(self, logs, avatar_url): embed.add_field(name="Created", value=duration(created_at, now=datetime.utcnow())) closer = entry.get("closer") if closer is None: - closer_msg = _("Unknown") + closer_msg = "Unknown" else: closer_msg = f"<@{closer['id']}>" - embed.add_field(name=_("Closed By"), value=closer_msg) + embed.add_field(name="Closed By", value=closer_msg) if entry["recipient"]["id"] != entry["creator"]["id"]: - embed.add_field(name=_("Created by"), value=f"<@{entry['creator']['id']}>") + embed.add_field(name="Created by", value=f"<@{entry['creator']['id']}>") - embed.add_field( - name=_("Preview"), value=format_preview(entry["messages"]), inline=False - ) + embed.add_field(name="Preview", value=format_preview(entry["messages"]), inline=False) if closer is not None: # BUG: Currently, logviewer can't display logs without a closer. - embed.add_field(name=_("Link"), value=log_url) + embed.add_field(name="Link", value=log_url) else: logger.debug("Invalid log entry: no closer.") - embed.add_field(name=_("Log Key"), value=f"`{entry['key']}`") + embed.add_field(name="Log Key", value=f"`{entry['key']}`") - embed.set_footer(text=_("Recipient ID") + ": " + str(entry["recipient"]["id"])) + embed.set_footer(text="Recipient ID: " + str(entry["recipient"]["id"])) embeds.append(embed) return embeds @@ -685,7 +668,7 @@ async def logs(self, ctx, *, user: User = None): if not any(not log["open"] for log in logs): embed = discord.Embed( color=self.bot.error_color, - description=_("This user does not have any previous logs."), + description="This user does not have any previous logs.", ) return await ctx.send(embed=embed) @@ -707,18 +690,13 @@ async def logs_closed_by(self, ctx, *, user: User = None): """ user = user if user is not None else ctx.author - query = {"guild_id": str(self.bot.guild_id), "open": False, "closer.id": str(user.id)} - - projection = {"messages": {"$slice": 5}} - - entries = await self.bot.db.logs.find(query, projection).to_list(None) - + entries = await self.bot.api.search_closed_by(user.id) embeds = self.format_log_embeds(entries, avatar_url=self.bot.guild.icon_url) if not embeds: embed = discord.Embed( color=self.bot.error_color, - description=_("No log entries have been found for that query"), + description="No log entries have been found for that query.", ) return await ctx.send(embed=embed) @@ -737,14 +715,14 @@ async def logs_delete(self, ctx, key_or_link: str): if not success: embed = discord.Embed( - title=_("Error"), - description=_("Log entry `{key}` not found.").format(key=key), + title="Error", + description=f"Log entry `{key}` not found.", color=self.bot.error_color, ) else: embed = discord.Embed( title="Success", - description=_("Log entry `{key}` successfully deleted.").format(key=key), + description=f"Log entry `{key}` successfully deleted.", color=self.bot.main_color, ) @@ -768,9 +746,7 @@ async def logs_responded(self, ctx, *, user: User = None): if not embeds: embed = discord.Embed( color=self.bot.error_color, - description=_("{mention} has not responded to any threads.").format( - mention=getattr(user, "mention", user.id) - ), + description=f"{getattr(user, 'mention', user.id)} has not responded to any threads.", ) return await ctx.send(embed=embed) @@ -788,22 +764,14 @@ async def logs_search(self, ctx, limit: Optional[int] = None, *, query): await ctx.trigger_typing() - query = { - "guild_id": str(self.bot.guild_id), - "open": False, - "$text": {"$search": f'"{query}"'}, - } - - projection = {"messages": {"$slice": 5}} - - entries = await self.bot.db.logs.find(query, projection).to_list(limit) + entries = await self.bot.api.search_by_text(query, limit) embeds = self.format_log_embeds(entries, avatar_url=self.bot.guild.icon_url) if not embeds: embed = discord.Embed( color=self.bot.error_color, - description=_("No log entries have been found for that query."), + description="No log entries have been found for that query.", ) return await ctx.send(embed=embed) @@ -896,13 +864,13 @@ async def edit(self, ctx, message_id: Optional[int] = None, *, message: str): except ValueError: return await ctx.send( embed=discord.Embed( - title=_("Failed"), - description=_("Cannot find a message to edit."), + title="Failed", + description="Cannot find a message to edit.", color=self.bot.error_color, ) ) - sent_emoji, x = await self.bot.retrieve_emoji() + sent_emoji, _ = await self.bot.retrieve_emoji() await self.bot.add_reaction(ctx.message, sent_emoji) @commands.command() @@ -926,7 +894,7 @@ async def contact( if user.bot: embed = discord.Embed( - color=self.bot.error_color, description=_("Cannot start a thread with a bot."), + color=self.bot.error_color, description="Cannot start a thread with a bot." ) return await ctx.send(embed=embed) @@ -934,9 +902,8 @@ async def contact( if exists: embed = discord.Embed( color=self.bot.error_color, - description=_("A thread for this user already " "exists in {mention}.").format( - mention=exists.channel.mention - ), + description="A thread for this user already " + f"exists in {exists.channel.mention}.", ) await ctx.channel.send(embed=embed) @@ -946,15 +913,13 @@ async def contact( logger.info("Contacting user %s when Modmail DM is disabled.", user) embed = discord.Embed( - title=_("Created Thread"), - description=_("Thread started by {author_mention} " "for {user_mention}.").format( - author_mention=ctx.author.mention, user_mention=user.mention - ), + title="Created Thread", + description=f"Thread started by {ctx.author.mention} for {user.mention}.", color=self.bot.main_color, ) await thread.wait_until_ready() await thread.channel.send(embed=embed) - sent_emoji, x = await self.bot.retrieve_emoji() + sent_emoji, _ = await self.bot.retrieve_emoji() await self.bot.add_reaction(ctx.message, sent_emoji) await asyncio.sleep(3) await ctx.message.delete() @@ -965,9 +930,7 @@ async def contact( async def blocked(self, ctx): """Retrieve a list of blocked users.""" - embeds = [ - discord.Embed(title=_("Blocked Users"), color=self.bot.main_color, description="") - ] + embeds = [discord.Embed(title="Blocked Users", color=self.bot.main_color, description="")] users = [] @@ -986,10 +949,10 @@ async def blocked(self, ctx): embed = embeds[0] for mention, reason in users: - line = mention + f" - {reason or _('No Reason Provided')}\n" + line = mention + f" - {reason or 'No Reason Provided'}\n" if len(embed.description) + len(line) > 2048: embed = discord.Embed( - title=_("Blocked Users") + " " + _("(Continued)"), + title="Blocked Users (Continued)", color=self.bot.main_color, description=line, ) @@ -997,7 +960,7 @@ async def blocked(self, ctx): else: embed.description += line else: - embeds[0].description = _("Currently there are no blocked users.") + embeds[0].description = "Currently there are no blocked users." session = EmbedPaginatorSession(ctx, *embeds) await session.run() @@ -1023,8 +986,8 @@ async def blocked_whitelist(self, ctx, *, user: User = None): if str(user.id) in self.bot.blocked_whitelisted_users: embed = discord.Embed( - title=_("Success"), - description=_("{mention} is no longer whitelisted.").format(mention=mention), + title="Success", + description=f"{mention} is no longer whitelisted.", color=self.bot.main_color, ) self.bot.blocked_whitelisted_users.remove(str(user.id)) @@ -1044,10 +1007,8 @@ async def blocked_whitelist(self, ctx, *, user: User = None): reason = msg[16:].strip().rstrip(".") embed = discord.Embed( title="Success", - description=_( - "{mention} was previously blocked internally for " - '"{reason}". {mention} is now whitelisted.' - ).format(mention=mention, reason=reason), + description=f"{mention} was previously blocked internally for " + f'"{reason}". {mention} is now whitelisted.', color=self.bot.main_color, ) else: @@ -1087,10 +1048,8 @@ async def block(self, ctx, user: Optional[User] = None, *, after: UserFriendlyTi if str(user.id) in self.bot.blocked_whitelisted_users: embed = discord.Embed( - title=_("Error"), - description=_("Cannot block {mention}, user is whitelisted.").format( - mention=mention - ), + title="Error", + description=f"Cannot block {mention}, user is whitelisted.", color=self.bot.error_color, ) return await ctx.send(embed=embed) @@ -1114,20 +1073,16 @@ async def block(self, ctx, user: Optional[User] = None, *, after: UserFriendlyTi if str(user.id) in self.bot.blocked_users and msg: old_reason = msg.strip().rstrip(".") embed = discord.Embed( - title=_("Success"), - description=_( - "{mention} was previously blocked {old_reason}.\n" - "{mention} is now blocked {reason}" - ).format(mention=mention, old_reason=old_reason, reason=reason), + title="Success", + description=f"{mention} was previously blocked {old_reason}.\n" + f"{mention} is now blocked {reason}", color=self.bot.main_color, ) else: embed = discord.Embed( - title=_("Success"), + title="Success", color=self.bot.main_color, - description=_("{mention} is now blocked {reason}").format( - mention=mention, reason=reason - ), + description=f"{mention} is now blocked {reason}", ) self.bot.blocked_users[str(user.id)] = reason await self.bot.config.update() @@ -1166,30 +1121,24 @@ async def unblock(self, ctx, *, user: User = None): reason = msg[16:].strip().rstrip(".") or "no reason" embed = discord.Embed( title="Success", - description=_( - "{mention} was previously blocked internally " - "{reason}.\n{mention} is no longer blocked." - ).format(mention=mention, reason=reason), + description=f"{mention} was previously blocked internally {reason}.\n" + f"{mention} is no longer blocked.", color=self.bot.main_color, ) embed.set_footer( - text=_( - "However, if the original system block reason still applies, " - "{name} will be automatically blocked again. Use " - '"{self.bot.prefix}blocked whitelist {user.id}" to whitelist the user.' - ).format(name=name, prefix=self.bot.prefix, user_id=user.id) + text="However, if the original system block reason still applies, " + f"{name} will be automatically blocked again. " + f'Use "{self.bot.prefix}blocked whitelist {user.id}" to whitelist the user.' ) else: embed = discord.Embed( - title=_("Success"), + title="Success", color=self.bot.main_color, - description=_("{mention} is no longer blocked.").format(mention=mention), + description=f"{mention} is no longer blocked.", ) else: embed = discord.Embed( - title=_("Error"), - description=_("{mention} is not blocked.").format(mention=mention), - color=self.bot.error_color, + title="Error", description=f"{mention} is not blocked.", color=self.bot.error_color ) return await ctx.send(embed=embed) @@ -1209,17 +1158,18 @@ async def delete(self, ctx, message_id: int = None): thread = ctx.thread try: - await thread.delete_message(message_id) - except ValueError: + await thread.delete_message(message_id, note=True) + except ValueError as e: + logger.warning("Failed to delete message: %s.", e) return await ctx.send( embed=discord.Embed( - title=_("Failed"), - description=_("Cannot find a message to delete."), + title="Failed", + description="Cannot find a message to delete.", color=self.bot.error_color, ) ) - sent_emoji, x = await self.bot.retrieve_emoji() + sent_emoji, _ = await self.bot.retrieve_emoji() await self.bot.add_reaction(ctx.message, sent_emoji) @commands.command() @@ -1341,20 +1291,15 @@ async def enable(self, ctx): Undo's the `{prefix}disable` command, all DM will be relayed after running this command. """ + embed = discord.Embed( + title="Success", + description="Modmail will now accept all DM messages.", + color=self.bot.main_color, + ) if self.bot.config["dm_disabled"] != 0: - embed = discord.Embed( - title=_("Success"), - description=_("Modmail will now accept **all** DM messages."), - color=self.bot.main_color, - ) self.bot.config["dm_disabled"] = 0 await self.bot.config.update() - else: - embed = discord.Embed( - description=_("Modmail is already accepting all DM messages."), - color=self.bot.error_color, - ) return await ctx.send(embed=embed) @@ -1378,27 +1323,12 @@ async def disable_new(self, ctx): No new threads can be created through DM. """ + embed = discord.Embed( + title="Success", + description="Modmail will not create any new threads.", + color=self.bot.main_color, + ) if self.bot.config["dm_disabled"] < 1: - embed = discord.Embed( - title=_("Success"), - description=_("Modmail will not create any **new** threads."), - color=self.bot.main_color, - ) - self.bot.config["dm_disabled"] = 1 - await self.bot.config.update() - elif self.bot.config["dm_disabled"] == 1: - embed = discord.Embed( - description=_("Modmail is already not creating any new threads."), - color=self.bot.error_color, - ) - else: - embed = discord.Embed( - title=_("Success"), - description=_( - "Modmail will not create **new** threads, but existing threads will now be functioning." - ), - color=self.bot.main_color, - ) self.bot.config["dm_disabled"] = 1 await self.bot.config.update() @@ -1412,20 +1342,15 @@ async def disable_all(self, ctx): No new threads can be created through DM nor no further DM messages will be relayed. """ + embed = discord.Embed( + title="Success", + description="Modmail will not accept any DM messages.", + color=self.bot.main_color, + ) - if self.bot.config["dm_disabled"] < 2: - embed = discord.Embed( - title=_("Success"), - description=_("Modmail will not accept **any** DM messages."), - color=self.bot.main_color, - ) + if self.bot.config["dm_disabled"] != 2: self.bot.config["dm_disabled"] = 2 await self.bot.config.update() - else: - embed = discord.Embed( - description=_("Modmail is already not accepting any DM messages."), - color=self.bot.error_color, - ) return await ctx.send(embed=embed) @@ -1438,22 +1363,20 @@ async def isenable(self, ctx): if self.bot.config["dm_disabled"] == 1: embed = discord.Embed( - title=_("New Threads Disabled"), - description=_("Modmail is not creating new threads."), + title="New Threads Disabled", + description="Modmail is not creating new threads.", color=self.bot.error_color, ) elif self.bot.config["dm_disabled"] == 2: embed = discord.Embed( - title=_("All DM Disabled"), - description=_( - "Modmail is not accepting any DM messages for new and existing threads." - ), + title="All DM Disabled", + description="Modmail is not accepting any DM messages for new and existing threads.", color=self.bot.error_color, ) else: embed = discord.Embed( - title=_("Enabled"), - description=_("Modmail is accepting all DM messages."), + title="Enabled", + description="Modmail now is accepting all DM messages.", color=self.bot.main_color, ) diff --git a/cogs/plugins.py b/cogs/plugins.py index 4d335607e1..227cfc8270 100644 --- a/cogs/plugins.py +++ b/cogs/plugins.py @@ -104,7 +104,7 @@ def __init__(self, bot): self.bot.loop.create_task(self.populate_registry()) - if getattr(self.bot, "config", None) and self.bot.config.get("enable_plugins"): + if self.bot.config.get("enable_plugins"): self.bot.loop.create_task(self.initial_load_plugins()) else: logger.info("Plugins not loaded since ENABLE_PLUGINS=false.") @@ -136,7 +136,12 @@ async def initial_load_plugins(self): await self.download_plugin(plugin) await self.load_plugin(plugin) except Exception: - logger.error("Error when loading plugin %s.", plugin, exc_info=True) + self.bot.config["plugins"].remove(plugin_name) + logger.error( + "Error when loading plugin %s. Plugin removed from config.", + plugin, + exc_info=True, + ) continue logger.debug("Finished loading all plugins.") @@ -152,11 +157,26 @@ async def download_plugin(self, plugin, force=False): if plugin.cache_path.exists() and not force: plugin_io = plugin.cache_path.open("rb") logger.debug("Loading cached %s.", plugin.cache_path) - else: - async with self.bot.session.get(plugin.url) as resp: + headers = {} + github_token = self.bot.config["github_token"] + if github_token is not None: + headers["Authorization"] = f"token {github_token}" + + async with self.bot.session.get(plugin.url, headers=headers) as resp: logger.debug("Downloading %s.", plugin.url) raw = await resp.read() + + try: + raw = await resp.text() + except UnicodeDecodeError: + pass + else: + if raw == 'Not Found': + raise InvalidPluginError('Plugin not found') + else: + raise InvalidPluginError('Invalid download recieved, non-bytes object') + plugin_io = io.BytesIO(raw) if not plugin.cache_path.parent.exists(): plugin.cache_path.parent.mkdir(parents=True) @@ -187,10 +207,10 @@ async def load_plugin(self, plugin): if req_txt.exists(): # Install PIP requirements - venv = hasattr(sys, "real_prefix") # in a virtual env + venv = hasattr(sys, "real_prefix") or hasattr(sys, "base_prefix") # in a virtual env user_install = " --user" if not venv else "" proc = await asyncio.create_subprocess_shell( - f"{sys.executable} -m pip install --upgrade{user_install} -r {req_txt} -q -q", + f'"{sys.executable}" -m pip install --upgrade{user_install} -r {req_txt} -q -q', stderr=PIPE, stdout=PIPE, ) @@ -227,7 +247,7 @@ async def parse_user_input(self, ctx, plugin_name, check_version=False): if not self._ready_event.is_set(): embed = discord.Embed( - description=_("Plugins are still loading, please try again later."), + description="Plugins are still loading, please try again later.", color=self.bot.main_color, ) await ctx.send(embed=embed) @@ -243,10 +263,8 @@ async def parse_user_input(self, ctx, plugin_name, check_version=False): if required_version and self.bot.version < parse_version(required_version): embed = discord.Embed( - description=_( - "Your bot's version is too low. " - "This plugin requires version `{required_version}`." - ).format(required_version=required_version), + description="Your bot's version is too low. " + f"This plugin requires version `{required_version}`.", color=self.bot.error_color, ) await ctx.send(embed=embed) @@ -259,11 +277,9 @@ async def parse_user_input(self, ctx, plugin_name, check_version=False): plugin = Plugin.from_string(plugin_name) except InvalidPluginError: embed = discord.Embed( - description=_( - "Invalid plugin name, double check the plugin name " - "or use one of the following formats: " - "username/repo/plugin, username/repo/plugin@branch." - ), + description="Invalid plugin name, double check the plugin name " + "or use one of the following formats: " + "username/repo/plugin, username/repo/plugin@branch.", color=self.bot.error_color, ) await ctx.send(embed=embed) @@ -296,33 +312,31 @@ async def plugins_add(self, ctx, *, plugin_name: str): if str(plugin) in self.bot.config["plugins"]: embed = discord.Embed( - description=_("This plugin is already installed."), color=self.bot.error_color, + description="This plugin is already installed.", color=self.bot.error_color ) return await ctx.send(embed=embed) if plugin.name in self.bot.cogs: # another class with the same name embed = discord.Embed( - description=_("Cannot install this plugin (dupe cog name)."), + description="Cannot install this plugin (dupe cog name).", color=self.bot.error_color, ) return await ctx.send(embed=embed) embed = discord.Embed( - description=_("Starting to download plugin from {plugin_link}...").format( - plugin_link=plugin.link - ), + description=f"Starting to download plugin from {plugin.link}...", color=self.bot.main_color, ) msg = await ctx.send(embed=embed) try: await self.download_plugin(plugin, force=True) - except Exception: + except Exception as e: logger.warning("Unable to download plugin %s.", plugin, exc_info=True) embed = discord.Embed( - description=_("Failed to download plugin, check logs for error."), + description=f"Failed to download plugin, check logs for error.\n{type(e)}: {e}", color=self.bot.error_color, ) @@ -337,35 +351,28 @@ async def plugins_add(self, ctx, *, plugin_name: str): try: await self.load_plugin(plugin) - except Exception: + except Exception as e: logger.warning("Unable to load plugin %s.", plugin, exc_info=True) embed = discord.Embed( - description=_("Failed to download plugin, check logs for error."), + description=f"Failed to download plugin, check logs for error.\n{type(e)}: {e}", color=self.bot.error_color, ) else: embed = discord.Embed( - description=_( - "Successfully installed plugin.\n" - "*Friendly reminder, plugins have absolute control over your bot. " - "Please only install plugins from developers you trust.*" - ), + description="Successfully installed plugin.\n" + "*Friendly reminder, plugins have absolute control over your bot. " + "Please only install plugins from developers you trust.*", color=self.bot.main_color, ) else: embed = discord.Embed( - description=_( - "Successfully installed plugin.\n" - "*Friendly reminder, plugins have absolute control over your bot. " - "Please only install plugins from developers you trust.*" - ) - + "\n\n" - + _( - "This plugin is currently not enabled due to `ENABLE_PLUGINS=false`, " - "to re-enable plugins, remove or change `ENABLE_PLUGINS=true` and restart your bot." - ), + description="Successfully installed plugin.\n" + "*Friendly reminder, plugins have absolute control over your bot. " + "Please only install plugins from developers you trust.*\n\n" + "This plugin is currently not enabled due to `ENABLE_PLUGINS=false`, " + "to re-enable plugins, remove or change `ENABLE_PLUGINS=true` and restart your bot.", color=self.bot.main_color, ) return await msg.edit(embed=embed) @@ -385,7 +392,7 @@ async def plugins_remove(self, ctx, *, plugin_name: str): if str(plugin) not in self.bot.config["plugins"]: embed = discord.Embed( - description=_("Plugin is not installed."), color=self.bot.error_color + description="Plugin is not installed.", color=self.bot.error_color ) return await ctx.send(embed=embed) @@ -411,7 +418,7 @@ async def plugins_remove(self, ctx, *, plugin_name: str): pass # dir not empty embed = discord.Embed( - description=_("The plugin is successfully uninstalled."), color=self.bot.main_color, + description="The plugin is successfully uninstalled.", color=self.bot.main_color ) await ctx.send(embed=embed) @@ -423,25 +430,33 @@ async def update_plugin(self, ctx, plugin_name): if str(plugin) not in self.bot.config["plugins"]: embed = discord.Embed( - description=_("Plugin is not installed."), color=self.bot.error_color + description="Plugin is not installed.", color=self.bot.error_color ) return await ctx.send(embed=embed) async with ctx.typing(): + embed = discord.Embed( + description=f"Successfully updated {plugin.name}.", color=self.bot.main_color + ) await self.download_plugin(plugin, force=True) if self.bot.config.get("enable_plugins"): try: self.bot.unload_extension(plugin.ext_string) except commands.ExtensionError: logger.warning("Plugin unload fail.", exc_info=True) - await self.load_plugin(plugin) - logger.debug("Updated %s.", plugin_name) - embed = discord.Embed( - description=_("Successfully updated {plugin_name}.").format( - plugin_name=plugin.name - ), - color=self.bot.main_color, - ) + try: + await self.load_plugin(plugin) + except Exception: + embed = discord.Embed( + description=f"Failed to update {plugin.name}. This plugin will now be removed from your bot.", + color=self.bot.error_color, + ) + self.bot.config["plugins"].remove(plugin_name) + logger.debug("Failed to update %s. Removed plugin from config.", plugin_name) + else: + logger.debug("Updated %s.", plugin_name) + else: + logger.debug("Updated %s.", plugin_name) return await ctx.send(embed=embed) @plugins.command(name="update") @@ -458,11 +473,45 @@ async def plugins_update(self, ctx, *, plugin_name: str = None): if plugin_name is None: # pylint: disable=redefined-argument-from-local - for plugin_name in self.bot.config["plugins"]: + for plugin_name in list(self.bot.config["plugins"]): await self.update_plugin(ctx, plugin_name) else: await self.update_plugin(ctx, plugin_name) + @plugins.command(name="reset") + @checks.has_permissions(PermissionLevel.OWNER) + async def plugins_reset(self, ctx): + """ + Reset all plugins for the bot. + + Deletes all cache and plugins from config and unloads from the bot. + """ + logger.warning("Purging plugins.") + for ext in list(self.bot.extensions): + if not ext.startswith("plugins."): + continue + try: + logger.error("Unloading plugin: %s.", ext) + self.bot.unload_extension(ext) + except Exception: + logger.error("Failed to unload plugin: %s.", ext) + self.bot.config["plugins"].clear() + + cache_path = Path(__file__).absolute().parent.parent / "temp" / "plugins-cache" + if cache_path.exists(): + logger.warning("Removing cache path.") + shutil.rmtree(cache_path) + + for entry in os.scandir(Path(__file__).absolute().parent.parent / "plugins"): + if entry.is_dir(): + shutil.rmtree(entry.path) + logger.warning("Removing %s.", entry.name) + + embed = discord.Embed( + description="Successfully purged all plugins from the bot.", color=self.bot.main_color + ) + return await ctx.send(embed=embed) + @plugins.command(name="loaded", aliases=["enabled", "installed"]) @checks.has_permissions(PermissionLevel.OWNER) async def plugins_loaded(self, ctx): @@ -472,25 +521,22 @@ async def plugins_loaded(self, ctx): if not self.bot.config.get("enable_plugins"): embed = discord.Embed( - description=_( - "No plugins are loaded due to `ENABLE_PLUGINS=false`, " - "to re-enable plugins, remove or set `ENABLE_PLUGINS=true` and restart your bot." - ), + description="No plugins are loaded due to `ENABLE_PLUGINS=false`, " + "to re-enable plugins, remove or set `ENABLE_PLUGINS=true` and restart your bot.", color=self.bot.error_color, ) return await ctx.send(embed=embed) if not self._ready_event.is_set(): embed = discord.Embed( - description=_("Plugins are still loading, please try again later."), + description="Plugins are still loading, please try again later.", color=self.bot.main_color, ) return await ctx.send(embed=embed) if not self.loaded_plugins: embed = discord.Embed( - description=_("There are no plugins currently loaded."), - color=self.bot.error_color, + description="There are no plugins currently loaded.", color=self.bot.error_color ) return await ctx.send(embed=embed) @@ -510,7 +556,7 @@ async def plugins_loaded(self, ctx): embeds = [] for page in pages: embed = discord.Embed( - title=_("Loaded plugins:"), description=page, color=self.bot.main_color + title="Loaded plugins:", description=page, color=self.bot.main_color ) embeds.append(embed) paginator = EmbedPaginatorSession(ctx, *embeds) @@ -546,16 +592,14 @@ async def plugins_registry(self, ctx, *, plugin_name: typing.Union[int, str] = N if not index and plugin_name is not None: embed = discord.Embed( color=self.bot.error_color, - description=_( - 'Could not find a plugin with name "{plugin_name}" within the registry.' - ).format(plugin_name=plugin_name), + description=f'Could not find a plugin with name "{plugin_name}" within the registry.', ) matches = get_close_matches(plugin_name, self.registry.keys()) if matches: embed.add_field( - name=_("Perhaps you meant:"), value="\n".join(f"`{m}`" for m in matches), + name="Perhaps you meant:", value="\n".join(f"`{m}`" for m in matches) ) return await ctx.send(embed=embed) @@ -575,7 +619,7 @@ async def plugins_registry(self, ctx, *, plugin_name: typing.Union[int, str] = N ) embed.add_field( - name=_("Installation"), value=f"```{self.bot.prefix}plugins add {plugin_name}```", + name="Installation", value=f"```{self.bot.prefix}plugins add {name}```" ) embed.set_author( @@ -594,13 +638,11 @@ async def plugins_registry(self, ctx, *, plugin_name: typing.Union[int, str] = N required_version = details.get("bot_version", False) if required_version and self.bot.version < parse_version(required_version): embed.set_footer( - text=_( - "Your bot is unable to install this plugin, " - "minimum required version is v{required_version}." - ).format(required_version=required_version) + text="Your bot is unable to install this plugin, " + f"minimum required version is v{required_version}." ) else: - embed.set_footer(text=_("Your bot is able to install this plugin.")) + embed.set_footer(text="Your bot is able to install this plugin.") embeds.append(embed) @@ -655,7 +697,7 @@ async def plugins_registry_compact(self, ctx): for page in pages: embed = discord.Embed(color=self.bot.main_color, description=page) - embed.set_author(name=_("Plugin Registry"), icon_url=self.bot.user.avatar_url) + embed.set_author(name="Plugin Registry", icon_url=self.bot.user.avatar_url) embeds.append(embed) paginator = EmbedPaginatorSession(ctx, *embeds) diff --git a/cogs/utility.py b/cogs/utility.py index 9974deb803..b0d96ea094 100644 --- a/cogs/utility.py +++ b/cogs/utility.py @@ -47,7 +47,7 @@ async def format_cog_help(self, cog, *, no_cog=False): else: format_ = f"`[{perm_level}] {prefix + cmd.qualified_name}` " - format_ += f"- {_(cmd.short_doc)}\n" + format_ += f"- {cmd.short_doc}\n" if cmd.short_doc else "- *No description.*\n" if not format_.strip(): continue if len(format_) + len(formats[-1]) >= 1024: @@ -58,26 +58,21 @@ async def format_cog_help(self, cog, *, no_cog=False): embeds = [] for format_ in formats: description = ( - _(cog.description) or _("No description.") + cog.description or "No description." if not no_cog - else _("Miscellaneous commands without a category.") + else "Miscellaneous commands without a category." ) embed = discord.Embed(description=f"*{description}*", color=bot.main_color) - embed.add_field(name=_("Commands"), value=format_ or _("No commands.")) + embed.add_field(name="Commands", value=format_ or "No commands.") - continued = " " + _("(Continued)") if embeds else "" - name = ( - cog.qualified_name + " - " + _("Help") - if not no_cog - else _("Miscellaneous Commands") - ) + continued = " (Continued)" if embeds else "" + name = cog.qualified_name + " - Help" if not no_cog else "Miscellaneous Commands" embed.set_author(name=name + continued, icon_url=bot.user.avatar_url) embed.set_footer( - text=_( - 'Type "{prefix}{command} command" ' "for more info on a specific command." - ).format(prefix=prefix, command=self.command_attrs["name"]) + text=f'Type "{prefix}{self.command_attrs["name"]} command" ' + "for more info on a specific command." ) embeds.append(embed) return embeds @@ -117,19 +112,19 @@ async def _get_help_embed(self, topic): if perm_level is not PermissionLevel.INVALID: perm_level = f"{perm_level.name} [{perm_level}]" else: - perm_level = _("NONE") + perm_level = "NONE" embed = discord.Embed( title=f"`{self.get_command_signature(topic)}`", color=self.context.bot.main_color, - description=self.process_help_msg(_(topic.help)), + description=self.process_help_msg(topic.help), ) return embed, perm_level async def send_command_help(self, command): topic = await self._get_help_embed(command) if topic is not None: - topic[0].set_footer(text=_("Permission level: {level}").format(level=topic[1])) + topic[0].set_footer(text=f"Permission level: {topic[1]}") await self.get_destination().send(embed=topic[0]) async def send_group_help(self, group): @@ -137,7 +132,7 @@ async def send_group_help(self, group): if topic is None: return embed = topic[0] - embed.add_field(name=_("Permission Level"), value=topic[1], inline=False) + embed.add_field(name="Permission Level", value=topic[1], inline=False) format_ = "" length = len(group.commands) @@ -151,13 +146,12 @@ async def send_group_help(self, group): branch = "└─" else: branch = "β”œβ”€" - format_ += f"`{branch} {command.name}` - {_(command.short_doc)}\n" + format_ += f"`{branch} {command.name}` - {command.short_doc}\n" - embed.add_field(name=_("Sub Command(s)"), value=format_[:1024], inline=False) + embed.add_field(name="Sub Command(s)", value=format_[:1024], inline=False) embed.set_footer( - text=_('Type "{prefix}{command} command" ' "for more info on a command.").format( - prefix=self.clean_prefix, command=self.command_attrs["name"] - ) + text=f'Type "{self.clean_prefix}{self.command_attrs["name"]} command" ' + "for more info on a command." ) await self.get_destination().send(embed=embed) @@ -167,8 +161,7 @@ async def send_error_message(self, error): val = self.context.bot.snippets.get(command) if val is not None: embed = discord.Embed( - title=_("{command} is a snippet.").format(command=command), - color=self.context.bot.main_color, + title=f"{command} is a snippet.", color=self.context.bot.main_color ) embed.add_field(name=f"`{command}` will send:", value=val) return await self.get_destination().send(embed=embed) @@ -179,12 +172,10 @@ async def send_error_message(self, error): if not values: embed = discord.Embed( - title=_("Error"), + title="Error", color=self.context.bot.error_color, - description=_( - "Alias `{command}` is invalid, this alias will now be deleted." - "This alias will now be deleted." - ).format(command=command), + description=f"Alias `{command}` is invalid, this alias will now be deleted." + "This alias will now be deleted.", ) embed.add_field(name=f"{command}` used to be:", value=val) self.context.bot.aliases.pop(command) @@ -192,34 +183,28 @@ async def send_error_message(self, error): else: if len(values) == 1: embed = discord.Embed( - title=_("{command} is an alias.").format(command=command), - color=self.context.bot.main_color, - ) - embed.add_field( - name=_("`{command}` points to:").format(command=command), value=values[0] + title=f"{command} is an alias.", color=self.context.bot.main_color ) + embed.add_field(name=f"`{command}` points to:", value=values[0]) else: embed = discord.Embed( - title=_("{command} is an alias.").format(command=command), + title=f"{command} is an alias.", color=self.context.bot.main_color, - description=_("**`{command}` points to the following steps:**").format( - command=command - ), + description=f"**`{command}` points to the following steps:**", ) for i, val in enumerate(values, start=1): - embed.add_field(name=_("Step") + f" {i}:", value=val) + embed.add_field(name=f"Step {i}:", value=val) embed.set_footer( - text=_('Type "{prefix}{command} alias" ' "for more details on aliases.").format( - prefix=self.clean_prefix, command=self.command_attrs["name"] - ) + text=f'Type "{self.clean_prefix}{self.command_attrs["name"]} alias" ' + "for more details on aliases." ) return await self.get_destination().send(embed=embed) logger.warning("CommandNotFound: %s", error) embed = discord.Embed(color=self.context.bot.error_color) - embed.set_footer(text=_('Command/Category "{command}" not found.').format(command=command)) + embed.set_footer(text=f'Command/Category "{command}" not found.') choices = set() @@ -229,15 +214,12 @@ async def send_error_message(self, error): closest = get_close_matches(command, choices) if closest: - embed.add_field( - name=_("Perhaps you meant:"), value="\n".join(f"`{x}`" for x in closest) - ) + embed.add_field(name="Perhaps you meant:", value="\n".join(f"`{x}`" for x in closest)) else: - embed.title = _("Cannot find command or category") + embed.title = "Cannot find command or category" embed.set_footer( - text=_('Type "{prefix}{command}" ' "for a list of all available commands.").format( - prefix=self.clean_prefix, command=self.command_attrs["name"] - ) + text=f'Type "{self.clean_prefix}{self.command_attrs["name"]}" ' + "for a list of all available commands." ) await self.get_destination().send(embed=embed) @@ -251,12 +233,15 @@ def __init__(self, bot): self.bot.help_command = ModmailHelpCommand( verify_checks=False, command_attrs={ - "help": _("Shows this help message."), + "help": "Shows this help message.", "checks": [checks.has_permissions_predicate(PermissionLevel.REGULAR)], }, ) self.bot.help_command.cog = self self.loop_presence.start() # pylint: disable=no-member + if not self.bot.config.get("enable_eval"): + self.eval_.enabled = False + logger.info("Eval disabled. enable_eval=False") def cog_unload(self): self.bot.help_command = self._original_help_command @@ -275,9 +260,7 @@ async def changelog(self, ctx, version: str.lower = ""): return await ctx.send( embed=discord.Embed( color=self.bot.error_color, - description=_("The specified version `{version}` could not be found.").format( - version=version - ), + description=f"The specified version `{version}` could not be found.", ) ) @@ -293,9 +276,7 @@ async def changelog(self, ctx, version: str.lower = ""): finally: logger.warning("Failed to display changelog.", exc_info=True) await ctx.send( - _("View the changelog here: {url}").format( - url=f"{changelog.latest_version.changelog_url}#v{version[::2]}" - ) + f"View the changelog here: {changelog.latest_version.changelog_url}#v{version[::2]}" ) @commands.command(aliases=["info"]) @@ -305,23 +286,21 @@ async def about(self, ctx): """Shows information about this bot.""" embed = discord.Embed(color=self.bot.main_color, timestamp=datetime.utcnow()) embed.set_author( - name=_("Modmail - About"), + name="Modmail - About", icon_url=self.bot.user.avatar_url, url="https://discord.gg/F34cRU8", ) embed.set_thumbnail(url=self.bot.user.avatar_url) - desc = _( - "This is an open source Discord bot that serves as a means for " - "members to easily communicate with server administrators in " - "an organised manner." - ) + desc = "This is an open source Discord bot that serves as a means for " + desc += "members to easily communicate with server administrators in " + desc += "an organised manner." embed.description = desc - embed.add_field(name=_("Uptime"), value=self.bot.uptime) - embed.add_field(name=_("Latency"), value=f"{self.bot.latency * 1000:.2f} ms") - embed.add_field(name=_("Version"), value=f"`{self.bot.version}`") - embed.add_field(name=_("Authors"), value="`kyb3r`, `Taki`, `fourjr`") + embed.add_field(name="Uptime", value=self.bot.uptime) + embed.add_field(name="Latency", value=f"{self.bot.latency * 1000:.2f} ms") + embed.add_field(name="Version", value=f"`{self.bot.version}`") + embed.add_field(name="Authors", value="`kyb3r`, `Taki`, `fourjr`") changelog = await Changelog.from_url(self.bot) latest = changelog.latest_version @@ -330,30 +309,26 @@ async def about(self, ctx): stable = next( filter(lambda v: not parse_version(v.version).is_prerelease, changelog.versions) ) - footer = _( - "You are on the prerelease version β€’ the latest version is v{version}." - ).format(version=stable.version) + footer = ( + f"You are on the prerelease version β€’ the latest version is v{stable.version}." + ) elif self.bot.version < parse_version(latest.version): - footer = _("A newer version is available v{version}.").format(version=latest.version) + footer = f"A newer version is available v{latest.version}." else: - footer = _("You are up to date with the latest version.") + footer = "You are up to date with the latest version." embed.add_field( - name=_("Want Modmail in Your Server?"), - value=_( - "Follow the installation guide on [GitHub](https://github.com/kyb3r/modmail/) " - "and join our [Discord server](https://discord.gg/F34cRU8/)!" - ), + name="Want Modmail in Your Server?", + value="Follow the installation guide on [GitHub](https://github.com/kyb3r/modmail/) " + "and join our [Discord server](https://discord.gg/F34cRU8/)!", inline=False, ) embed.add_field( - name=_("Support the Developers"), - value=_( - "This bot is completely free for everyone. We rely on kind individuals " - "like you to support us on [`Patreon`](https://patreon.com/kyber) (perks included) " - "to keep this bot free forever!" - ), + name="Support the Developers", + value="This bot is completely free for everyone. We rely on kind individuals " + "like you to support us on [`Patreon`](https://patreon.com/kyber) (perks included) " + "to keep this bot free forever!", inline=False, ) @@ -400,10 +375,10 @@ async def debug(self, ctx): if not logs: embed = discord.Embed( color=self.bot.main_color, - title=_("Debug Logs:"), - description=_("You don't have any logs at the moment."), + title="Debug Logs:", + description="You don't have any logs at the moment.", ) - embed.set_footer(text=_("Go to Heroku to see your logs.")) + embed.set_footer(text="Go to Heroku to see your logs.") return await ctx.send(embed=embed) messages = [] @@ -429,7 +404,7 @@ async def debug(self, ctx): messages.append(msg) embed = discord.Embed(color=self.bot.main_color) - embed.set_footer(text=_("Debug logs - Navigate using the reactions below.")) + embed.set_footer(text="Debug logs - Navigate using the reactions below.") session = MessagePaginatorSession(ctx, *messages, embed=embed) session.current = len(messages) - 1 @@ -461,7 +436,7 @@ async def debug_hastebin(self, ctx): logger.error(data["message"]) raise embed = discord.Embed( - title=_("Debug Logs"), + title="Debug Logs", color=self.bot.main_color, description=f"{haste_url}/" + key, ) @@ -469,9 +444,9 @@ async def debug_hastebin(self, ctx): embed = discord.Embed( title="Debug Logs", color=self.bot.main_color, - description=_("Something's wrong. We're unable to upload your logs to hastebin."), + description="Something's wrong. We're unable to upload your logs to hastebin.", ) - embed.set_footer(text=_("Go to Heroku to see your logs.")) + embed.set_footer(text="Go to Heroku to see your logs.") await ctx.send(embed=embed) @debug.command(name="clear", aliases=["wipe"]) @@ -491,7 +466,7 @@ async def debug_clear(self, ctx): pass await ctx.send( embed=discord.Embed( - color=self.bot.main_color, description=_("Cached logs are now cleared.") + color=self.bot.main_color, description="Cached logs are now cleared." ) ) @@ -522,7 +497,7 @@ async def activity(self, ctx, activity_type: str.lower, *, message: str = ""): self.bot.config.remove("activity_message") await self.bot.config.update() await self.set_presence() - embed = discord.Embed(title=_("Activity Removed"), color=self.bot.main_color) + embed = discord.Embed(title="Activity Removed", color=self.bot.main_color) return await ctx.send(embed=embed) if not message: @@ -533,7 +508,7 @@ async def activity(self, ctx, activity_type: str.lower, *, message: str = ""): except KeyError: raise commands.MissingRequiredArgument(SimpleNamespace(name="activity")) - activity, x = await self.set_presence( + activity, _ = await self.set_presence( activity_type=activity_type, activity_message=message ) @@ -541,15 +516,13 @@ async def activity(self, ctx, activity_type: str.lower, *, message: str = ""): self.bot.config["activity_message"] = activity.name await self.bot.config.update() - msg = _("Activity set to: {name} ").format(name=activity.type.name.capitalize()) + msg = f"Activity set to: {activity.type.name.capitalize()} " if activity.type == ActivityType.listening: - msg += _("to {name}.").format(name=activity.name) + msg += f"to {activity.name}." else: msg += f"{activity.name}." - embed = discord.Embed( - title=_("Activity Changed"), description=msg, color=self.bot.main_color - ) + embed = discord.Embed(title="Activity Changed", description=msg, color=self.bot.main_color) return await ctx.send(embed=embed) @commands.command() @@ -571,7 +544,7 @@ async def status(self, ctx, *, status_type: str.lower): self.bot.config.remove("status") await self.bot.config.update() await self.set_presence() - embed = discord.Embed(title=_("Status Removed"), color=self.bot.main_color) + embed = discord.Embed(title="Status Removed", color=self.bot.main_color) return await ctx.send(embed=embed) status_type = status_type.replace(" ", "_") @@ -580,15 +553,13 @@ async def status(self, ctx, *, status_type: str.lower): except KeyError: raise commands.MissingRequiredArgument(SimpleNamespace(name="status")) - x, status = await self.set_presence(status=status) + _, status = await self.set_presence(status=status) self.bot.config["status"] = status.value await self.bot.config.update() - msg = _("Status set to: {value}.").format(value=status.value) - embed = discord.Embed( - title=_("Status Changed"), description=msg, color=self.bot.main_color - ) + msg = f"Status set to: {status.value}." + embed = discord.Embed(title="Status Changed", description=msg, color=self.bot.main_color) return await ctx.send(embed=embed) async def set_presence(self, *, status=None, activity_type=None, activity_message=None): @@ -659,7 +630,7 @@ async def before_loop_presence(self): async def ping(self, ctx): """Pong! Returns your websocket latency.""" embed = discord.Embed( - title=_("Pong! Websocket Latency:"), + title="Pong! Websocket Latency:", description=f"{self.bot.ws.latency * 1000:.4f} ms", color=self.bot.main_color, ) @@ -667,7 +638,7 @@ async def ping(self, ctx): @commands.command() @checks.has_permissions(PermissionLevel.ADMINISTRATOR) - async def mention(self, ctx, *, mention: str = None): + async def mention(self, ctx, *mention: Union[discord.Role, discord.Member]): """ Change what the bot mentions at the start of each thread. @@ -676,16 +647,15 @@ async def mention(self, ctx, *, mention: str = None): # TODO: ability to disable mention. current = self.bot.config["mention"] - if mention is None: + if not mention: embed = discord.Embed( - title=_("Current mention:"), color=self.bot.main_color, description=str(current), + title="Current mention:", color=self.bot.main_color, description=str(current) ) else: + mention = " ".join(i.mention for i in mention) embed = discord.Embed( - title=_("Changed mention!"), - description=_('On thread creation the bot now says "{mention}".').format( - mention=mention - ), + title="Changed mention!", + description=f'On thread creation the bot now says "{mention}".', color=self.bot.main_color, ) self.bot.config["mention"] = mention @@ -704,14 +674,14 @@ async def prefix(self, ctx, *, prefix=None): current = self.bot.prefix embed = discord.Embed( - title=_("Current prefix"), color=self.bot.main_color, description=f"{current}" + title="Current prefix", color=self.bot.main_color, description=f"{current}" ) if prefix is None: await ctx.send(embed=embed) else: - embed.title = _("Changed prefix!") - embed.description = _("Set prefix to `{prefix}`").format(prefix=prefix) + embed.title = "Changed prefix!" + embed.description = f"Set prefix to `{prefix}`" self.bot.config["prefix"] = prefix await self.bot.config.update() await ctx.send(embed=embed) @@ -746,7 +716,7 @@ async def config_options(self, ctx): f"`{name}`" for name in takewhile(lambda x: x is not None, names) ) embed = discord.Embed( - title=_("Available configuration keys:"), + title="Available configuration keys:", color=self.bot.main_color, description=description, ) @@ -767,22 +737,18 @@ async def config_set(self, ctx, key: str.lower, *, value: str): self.bot.config.set(key, value) await self.bot.config.update() embed = discord.Embed( - title=_("Success"), + title="Success", color=self.bot.main_color, - description=_("Set `{key}` to `{value}`.").format( - key=key, value=self.bot.config[key] - ), + description=f"Set `{key}` to `{self.bot.config[key]}`.", ) except InvalidConfigError as exc: embed = exc.embed else: embed = discord.Embed( - title=_("Error"), - color=self.bot.error_color, - description=_("{key} is an invalid key.").format(key=key), + title="Error", color=self.bot.error_color, description=f"{key} is an invalid key." ) valid_keys = [f"`{k}`" for k in sorted(keys)] - embed.add_field(name=_("Valid keys"), value=", ".join(valid_keys)) + embed.add_field(name="Valid keys", value=", ".join(valid_keys)) return await ctx.send(embed=embed) @@ -795,18 +761,16 @@ async def config_remove(self, ctx, *, key: str.lower): self.bot.config.remove(key) await self.bot.config.update() embed = discord.Embed( - title=_("Success"), + title="Success", color=self.bot.main_color, - description=_("`{key}` had been reset to default.").format(key=key), + description=f"`{key}` had been reset to default.", ) else: embed = discord.Embed( - title=_("Error"), - color=self.bot.error_color, - description=_("{key} is an invalid key.").format(key=key), + title="Error", color=self.bot.error_color, description=f"{key} is an invalid key." ) valid_keys = [f"`{k}`" for k in sorted(keys)] - embed.add_field(name=_("Valid keys"), value=", ".join(valid_keys)) + embed.add_field(name="Valid keys", value=", ".join(valid_keys)) return await ctx.send(embed=embed) @@ -822,28 +786,25 @@ async def config_get(self, ctx, *, key: str.lower = None): if key: if key in keys: - desc = _("`{key}` is set to `{value}`").format(key=key, value=self.bot.config[key]) + desc = f"`{key}` is set to `{self.bot.config[key]}`" embed = discord.Embed(color=self.bot.main_color, description=desc) - embed.set_author(name=_("Config variable"), icon_url=self.bot.user.avatar_url) + embed.set_author(name="Config variable", icon_url=self.bot.user.avatar_url) else: embed = discord.Embed( - title=_("Error"), + title="Error", color=self.bot.error_color, - description=_("`{key}` is an invalid key.").format(key=key), + description=f"`{key}` is an invalid key.", ) embed.set_footer( - text=_('Type "{prefix}config options" for a list of config variables.').format( - prefix=self.bot.prefix - ) + text=f'Type "{self.bot.prefix}config options" for a list of config variables.' ) else: embed = discord.Embed( color=self.bot.main_color, - description=_("Here is a list of currently " "set configuration variable(s)."), + description="Here is a list of currently set configuration variable(s).", ) - embed.set_author(name=_("Current config(s):"), icon_url=self.bot.user.avatar_url) embed.set_author(name="Current config(s):", icon_url=self.bot.user.avatar_url) config = self.bot.config.filter_default(self.bot.config) @@ -866,9 +827,9 @@ async def config_help(self, ctx, key: str.lower = None): key, {**self.bot.config.public_keys, **self.bot.config.protected_keys} ) embed = discord.Embed( - title=_("Error"), + title="Error", color=self.bot.error_color, - description=_("`{key}` is an invalid key.").format(key=key), + description=f"`{key}` is an invalid key.", ) if closest: embed.add_field( @@ -880,9 +841,9 @@ async def config_help(self, ctx, key: str.lower = None): if key is not None and key not in config_help: embed = discord.Embed( - title=_("Error"), + title="Error", color=self.bot.error_color, - description=_("No help details found for `{key}`.").format(key=key), + description=f"No help details found for `{key}`.", ) return await ctx.send(embed=embed) @@ -895,24 +856,21 @@ def fmt(val): if current_key == key: index = i embed = discord.Embed( - title=_("Configuration description on {current_key}:").format( - current_key=current_key - ), - color=self.bot.main_color, + title=f"Configuration description on {current_key}:", color=self.bot.main_color ) embed.add_field(name="Default:", value=fmt(info["default"]), inline=False) - embed.add_field(name=_("Information:"), value=fmt(info["description"]), inline=False) + embed.add_field(name="Information:", value=fmt(info["description"]), inline=False) if info["examples"]: example_text = "" for example in info["examples"]: example_text += f"- {fmt(example)}\n" - embed.add_field(name=_("Example(s):"), value=example_text, inline=False) + embed.add_field(name="Example(s):", value=example_text, inline=False) note_text = "" for note in info["notes"]: note_text += f"- {fmt(note)}\n" if note_text: - embed.add_field(name=_("Note(s):"), value=note_text, inline=False) + embed.add_field(name="Note(s):", value=note_text, inline=False) if info.get("image") is not None: embed.set_image(url=fmt(info["image"])) @@ -957,12 +915,10 @@ async def alias(self, ctx, *, name: str.lower = None): if not values: embed = discord.Embed( - title=_("Error"), + title="Error", color=self.bot.error_color, - description=_( - "Alias `{name}` is invalid, it used to be `{value}`. " - "This alias will now be deleted." - ).format(name=name, value=escape_markdown(val)), + description=f"Alias `{name}` is invalid, this alias will now be deleted." + "This alias will now be deleted.", ) embed.add_field(name=f"{name}` used to be:", value=utils.truncate(val, 1024)) self.bot.aliases.pop(name) @@ -971,9 +927,7 @@ async def alias(self, ctx, *, name: str.lower = None): if len(values) == 1: embed = discord.Embed( - title=_("Alias") + f' - "{name}":', - description=values[0], - color=self.bot.main_color, + title=f'Alias - "{name}":', description=values[0], color=self.bot.main_color ) return await ctx.send(embed=embed) @@ -991,13 +945,10 @@ async def alias(self, ctx, *, name: str.lower = None): if not self.bot.aliases: embed = discord.Embed( - color=self.bot.error_color, - description=_("You dont have any aliases at the moment."), - ) - embed.set_footer( - text=_("Do {prefix}help alias for more commands.").format(prefix=self.bot.prefix) + color=self.bot.error_color, description="You dont have any aliases at the moment." ) - embed.set_author(name=_("Aliases"), icon_url=ctx.guild.icon_url) + embed.set_footer(text=f'Do "{self.bot.prefix}help alias" for more commands.') + embed.set_author(name="Aliases", icon_url=ctx.guild.icon_url) return await ctx.send(embed=embed) embeds = [] @@ -1005,7 +956,7 @@ async def alias(self, ctx, *, name: str.lower = None): for i, names in enumerate(zip_longest(*(iter(sorted(self.bot.aliases)),) * 15)): description = utils.format_description(i, names) embed = discord.Embed(color=self.bot.main_color, description=description) - embed.set_author(name=_("Command Aliases"), icon_url=ctx.guild.icon_url) + embed.set_author(name="Command Aliases", icon_url=ctx.guild.icon_url) embeds.append(embed) session = EmbedPaginatorSession(ctx, *embeds) @@ -1033,9 +984,9 @@ async def make_alias(self, name, value, action): values = utils.parse_alias(value) if not values: embed = discord.Embed( - title=_("Error"), + title="Error", color=self.bot.error_color, - description=_("Invalid multi-step alias, try wrapping each steps in quotes."), + description="Invalid multi-step alias, try wrapping each steps in quotes.", ) embed.set_footer(text=f'See "{self.bot.prefix}alias add" for more details.') return embed @@ -1050,15 +1001,12 @@ async def make_alias(self, name, value, action): multiple_alias = len(values) > 1 - embed = discord.Embed(title=_("{action} alias").format(action), color=self.bot.main_color) + embed = discord.Embed(title=f"{action} alias", color=self.bot.main_color) if not multiple_alias: - embed.add_field( - name=_("`{name}` points to:").format(name=name), - value=utils.truncate(values[0], 1024), - ) + embed.add_field(name=f"`{name}` points to:", value=utils.truncate(values[0], 1024)) else: - embed.description = _("`{name}` now points to the following steps:").format(name=name) + embed.description = f"`{name}` now points to the following steps:" for i, val in enumerate(values, start=1): view = StringView(val) @@ -1073,21 +1021,21 @@ async def make_alias(self, name, value, action): embed = discord.Embed(title="Error", color=self.bot.error_color) if multiple_alias: - embed.description = _( + embed.description = ( "The command you are attempting to point " - "to does not exist: `{command}`." - ).format(command=linked_command) + f"to does not exist: `{linked_command}`." + ) else: - embed.description = _( + embed.description = ( "The command you are attempting to point " - "to on step {number} does not exist: `{command}`." - ).format(number=i, command=linked_command) + f"to on step {i} does not exist: `{linked_command}`." + ) return embed else: save_aliases.append(val) if multiple_alias: - embed.add_field(name=_("Step") + f" {i}:", value=utils.truncate(val, 1024)) + embed.add_field(name=f"Step {i}:", value=utils.truncate(val, 1024)) self.bot.aliases[name] = " && ".join(f'"{a}"' for a in save_aliases) await self.bot.config.update() @@ -1112,36 +1060,30 @@ async def alias_add(self, ctx, name: str.lower, *, value): embed = None if self.bot.get_command(name): embed = discord.Embed( - title=_("Error"), + title="Error", color=self.bot.error_color, - description=_("A command with the same name already exists: `{name}`.").format( - name=name - ), + description=f"A command with the same name already exists: `{name}`.", ) elif name in self.bot.aliases: embed = discord.Embed( - title=_("Error"), + title="Error", color=self.bot.error_color, - description=_("Another alias with the same name already exists: `{name}`.").format( - name=name - ), + description=f"Another alias with the same name already exists: `{name}`.", ) elif name in self.bot.snippets: embed = discord.Embed( - title=_("Error"), + title="Error", color=self.bot.error_color, - description=_("A snippet with the same name already exists: `{name}`.").format( - name=name - ), + description=f"A snippet with the same name already exists: `{name}`.", ) elif len(name) > 120: embed = discord.Embed( - title=_("Error"), + title="Error", color=self.bot.error_color, - description=_("Alias names cannot be longer than 120 characters."), + description="Alias names cannot be longer than 120 characters.", ) if embed is None: @@ -1158,9 +1100,9 @@ async def alias_remove(self, ctx, *, name: str.lower): await self.bot.config.update() embed = discord.Embed( - title=_("Removed alias"), + title="Removed alias", color=self.bot.main_color, - description=_("Successfully deleted `{name}`.").format(name=name), + description=f"Successfully deleted `{name}`.", ) else: embed = utils.create_not_found_embed(name, self.bot.aliases.keys(), "Alias") @@ -1264,11 +1206,9 @@ async def permissions_override(self, ctx, command_name: str.lower, *, level_name level = self._parse_level(level_name) if level is PermissionLevel.INVALID: embed = discord.Embed( - title=_("Error"), + title="Error", color=self.bot.error_color, - description=_("The referenced level does not exist: `{level}`.").format( - level=level_name - ), + description=f"The referenced level does not exist: `{level_name}`.", ) else: logger.info( @@ -1280,11 +1220,10 @@ async def permissions_override(self, ctx, command_name: str.lower, *, level_name await self.bot.config.update() embed = discord.Embed( - title=_("Success"), + title="Success", color=self.bot.main_color, - description=_( - "Successfully set command permission level for " "`{command}` to `{level}`." - ).format(command=command.qualified_name, level=level.name), + description="Successfully set command permission level for " + f"`{command.qualified_name}` to `{level.name}`.", ) return await ctx.send(embed=embed) @@ -1327,11 +1266,9 @@ async def permissions_add( if not check: embed = discord.Embed( - title=_("Error"), + title="Error", color=self.bot.error_color, - description=_("The referenced {type} does not exist: `{name}`.").format( - type=type_, name=name - ), + description=f"The referenced {type_} does not exist: `{name}`.", ) return await ctx.send(embed=embed) @@ -1354,9 +1291,9 @@ async def permissions_add( await self.bot.main_category.set_permissions(key, read_messages=True) embed = discord.Embed( - title=_("Success"), + title="Success", color=self.bot.main_color, - description=_("Permission for `{name}` was successfully updated.").format(name=name), + description=f"Permission for `{name}` was successfully updated.", ) return await ctx.send(embed=embed) @@ -1405,12 +1342,10 @@ async def permissions_remove( if level is None: perm = self.bot.command_perm(name) embed = discord.Embed( - title=_("Error"), + title="Error", color=self.bot.error_color, - description=_( - "The command permission level was never overridden: `{name}`, " - "current permission level is {perm_name}." - ).format(name=name, perm_name=perm.name), + description=f"The command permission level was never overridden: `{name}`, " + f"current permission level is {perm.name}.", ) else: logger.info("Restored command permission level for `%s`.", name) @@ -1418,11 +1353,9 @@ async def permissions_remove( await self.bot.config.update() perm = self.bot.command_perm(name) embed = discord.Embed( - title=_("Success"), + title="Success", color=self.bot.main_color, - description=_( - "Command permission level for `{name}` was successfully restored to {perm_name}." - ).format(name=name, perm_name=perm.name), + description=f"Command permission level for `{name}` was successfully restored to {perm.name}.", ) return await ctx.send(embed=embed) @@ -1434,11 +1367,9 @@ async def permissions_remove( level = self._parse_level(name) if level is PermissionLevel.INVALID: embed = discord.Embed( - title=_("Error"), + title="Error", color=self.bot.error_color, - description=_("The referenced level does not exist: `{name}`.").format( - name=name - ), + description=f"The referenced level does not exist: `{name}`.", ) return await ctx.send(embed=embed) name = level.name @@ -1463,9 +1394,9 @@ async def permissions_remove( await self.bot.main_category.set_permissions(member, overwrite=None) embed = discord.Embed( - title=_("Success"), + title="Success", color=self.bot.main_color, - description=_("Permission for `{name}` was successfully updated.").format(name=name), + description=f"Permission for `{name}` was successfully updated.", ) return await ctx.send(embed=embed) @@ -1476,8 +1407,8 @@ def _get_perm(self, ctx, name, type_): permissions = self.bot.config["level_permissions"].get(name, []) if not permissions: embed = discord.Embed( - title=_("Permission entries for {type} `{name}`:").format(type=type_, name=name), - description=_("No permission entries found."), + title=f"Permission entries for {type_} `{name}`:", + description="No permission entries found.", color=self.bot.main_color, ) else: @@ -1501,7 +1432,7 @@ def _get_perm(self, ctx, name, type_): values.append(str(perm)) embed = discord.Embed( - title=_("Permission entries for {type} `{name}`:").format(type=type_, name=name), + title=f"Permission entries for {type_} `{name}`:", description=", ".join(values), color=self.bot.main_color, ) @@ -1564,26 +1495,22 @@ async def permissions_get( desc_cmd = ( ", ".join(map(lambda x: f"`{x}`", cmds)) if cmds - else _("No permission entries found.") + else "No permission entries found." ) desc_level = ( ", ".join(map(lambda x: f"`{x}`", levels)) if levels - else _("No permission entries found.") + else "No permission entries found." ) embeds = [ discord.Embed( - title=_("{mention} has permission with the following commands:").format( - mention=mention - ), + title=f"{mention} has permission with the following commands:", description=desc_cmd, color=self.bot.main_color, ), discord.Embed( - title=_( - "{mention} has permission with the following permission levels:" - ).format(mention=mention), + title=f"{mention} has permission with the following permission levels:", description=desc_level, color=self.bot.main_color, ), @@ -1608,10 +1535,8 @@ async def permissions_get( if not overrides: embeds.append( discord.Embed( - title=_("Permission Overrides"), - description=_( - "You don't have any command level overrides at the moment." - ), + title="Permission Overrides", + description="You don't have any command level overrides at the moment.", color=self.bot.error_color, ) ) @@ -1625,7 +1550,7 @@ async def permissions_get( color=self.bot.main_color, description=description ) embed.set_author( - name=_("Permission Overrides"), icon_url=ctx.guild.icon_url + name="Permission Overrides", icon_url=ctx.guild.icon_url ) embeds.append(embed) @@ -1638,20 +1563,16 @@ async def permissions_get( perm = self.bot.command_perm(name) if level is None: embed = discord.Embed( - title=_("Error"), + title="Error", color=self.bot.error_color, - description=_( - "The command permission level was never overridden: `{name}`, " - "current permission level is {perm_name}." - ).format(name=name, perm_name=perm.name), + description=f"The command permission level was never overridden: `{name}`, " + f"current permission level is {perm.name}.", ) else: embed = discord.Embed( - title=_("Success"), + title="Success", color=self.bot.main_color, - description=_( - 'Permission override for command "{name}" is "{perm_name}".' - ).format(name=name, perm_name=perm.name), + description=f'Permission override for command "{name}" is "{perm.name}".', ) return await ctx.send(embed=embed) @@ -1727,19 +1648,14 @@ async def oauth_whitelist(self, ctx, target: Union[discord.Role, utils.User]): await self.bot.config.update() embed = discord.Embed(color=self.bot.main_color) - embed.title = _("Success") + embed.title = "Success" if not hasattr(target, "mention"): target = self.bot.get_user(target.id) or self.bot.modmail_guild.get_role(target.id) - if removed: - embed.description = _("Un-whitelisted {target_mention} to view logs.").format( - target_mention=target.mention - ) - else: - embed.description = _("Whitelisted {target_mention} to view logs.").format( - target_mention=target.mention - ) + embed.description = ( + f"{'Un-w' if removed else 'W'}hitelisted {target.mention} to view logs." + ) await ctx.send(embed=embed) @@ -1761,9 +1677,9 @@ async def oauth_show(self, ctx): roles.append(role) embed = discord.Embed(color=self.bot.main_color) - embed.title = _("Oauth Whitelist") + embed.title = "Oauth Whitelist" - embed.add_field(name=_("Users"), value=" ".join(u.mention for u in users) or _("None")) + embed.add_field(name="Users", value=" ".join(u.mention for u in users) or "None") embed.add_field(name="Roles", value=" ".join(r.mention for r in roles) or "None") await ctx.send(embed=embed) diff --git a/core/clients.py b/core/clients.py index 54cc28c9be..03921d5a17 100644 --- a/core/clients.py +++ b/core/clients.py @@ -1,18 +1,21 @@ import secrets +import sys from datetime import datetime from json import JSONDecodeError -from typing import Union +from typing import Union, Optional from discord import Member, DMChannel, TextChannel, Message from aiohttp import ClientResponseError, ClientResponse +from motor.motor_asyncio import AsyncIOMotorClient +from pymongo.errors import ConfigurationError from core.models import getLogger logger = getLogger(__name__) -class RequestClient: +class ApiClient: """ This class represents the general request class for all type of clients. @@ -29,8 +32,9 @@ class RequestClient: The bot's current running `ClientSession`. """ - def __init__(self, bot): + def __init__(self, bot, db): self.bot = bot + self.db = db self.session = bot.session async def request( @@ -74,16 +78,152 @@ async def request( except (JSONDecodeError, ClientResponseError): return await resp.text() - -class ApiClient(RequestClient): - @property - def db(self): - return self.bot.db - @property def logs(self): return self.db.logs + async def setup_indexes(self): + return NotImplemented + + async def validate_database_connection(self): + return NotImplemented + + async def get_user_logs(self, user_id: Union[str, int]) -> list: + return NotImplemented + + async def get_latest_user_logs(self, user_id: Union[str, int]): + return NotImplemented + + async def get_responded_logs(self, user_id: Union[str, int]) -> list: + return NotImplemented + + async def get_open_logs(self) -> list: + return NotImplemented + + async def get_log(self, channel_id: Union[str, int]) -> dict: + return NotImplemented + + async def get_log_link(self, channel_id: Union[str, int]) -> str: + return NotImplemented + + async def create_log_entry( + self, recipient: Member, channel: TextChannel, creator: Member + ) -> str: + return NotImplemented + + async def delete_log_entry(self, key: str) -> bool: + return NotImplemented + + async def get_config(self) -> dict: + return NotImplemented + + async def update_config(self, data: dict): + return NotImplemented + + async def edit_message(self, message_id: Union[int, str], new_content: str) -> None: + return NotImplemented + + async def append_log( + self, + message: Message, + *, + message_id: str = "", + channel_id: str = "", + type_: str = "thread_message", + ) -> dict: + return NotImplemented + + async def post_log(self, channel_id: Union[int, str], data: dict) -> dict: + return NotImplemented + + async def search_closed_by(self, user_id: Union[int, str]): + return NotImplemented + + async def search_by_text(self, text: str, limit: Optional[int]): + return NotImplemented + + def get_plugin_partition(self, cog): + return NotImplemented + + +class MongoDBClient(ApiClient): + def __init__(self, bot): + mongo_uri = bot.config["connection_uri"] + if mongo_uri is None: + mongo_uri = bot.config["mongo_uri"] + if mongo_uri is not None: + logger.warning( + "You're using the old config MONGO_URI, " + "consider switching to the new CONNECTION_URI config." + ) + else: + logger.critical("A Mongo URI is necessary for the bot to function.") + raise RuntimeError + + try: + db = AsyncIOMotorClient(mongo_uri).modmail_bot + except ConfigurationError as e: + logger.critical( + "Your MongoDB CONNECTION_URI might be copied wrong, try re-copying from the source again. " + "Otherwise noted in the following message:" + ) + logger.critical(e) + sys.exit(0) + + super().__init__(bot, db) + + async def setup_indexes(self): + """Setup text indexes so we can use the $search operator""" + coll = self.db.logs + index_name = "messages.content_text_messages.author.name_text_key_text" + + index_info = await coll.index_information() + + # Backwards compatibility + old_index = "messages.content_text_messages.author.name_text" + if old_index in index_info: + logger.info("Dropping old index: %s", old_index) + await coll.drop_index(old_index) + + if index_name not in index_info: + logger.info('Creating "text" index for logs collection.') + logger.info("Name: %s", index_name) + await coll.create_index( + [("messages.content", "text"), ("messages.author.name", "text"), ("key", "text")] + ) + logger.debug("Successfully configured and verified database indexes.") + + async def validate_database_connection(self): + try: + await self.db.command("buildinfo") + except Exception as exc: + logger.critical("Something went wrong while connecting to the database.") + message = f"{type(exc).__name__}: {str(exc)}" + logger.critical(message) + + if "ServerSelectionTimeoutError" in message: + logger.critical( + "This may have been caused by not whitelisting " + "IPs correctly. Make sure to whitelist all " + "IPs (0.0.0.0/0) https://i.imgur.com/mILuQ5U.png" + ) + + if "OperationFailure" in message: + logger.critical( + "This is due to having invalid credentials in your MongoDB CONNECTION_URI. " + "Remember you need to substitute `` with your actual password." + ) + logger.critical( + "Be sure to URL encode your username and password (not the entire URL!!), " + "https://www.urlencoder.io/, if this issue persists, try changing your username and password " + "to only include alphanumeric characters, no symbols." + "" + ) + raise + else: + logger.debug("Successfully connected to the database.") + logger.line("debug") + async def get_user_logs(self, user_id: Union[str, int]) -> list: query = {"recipient.id": str(user_id), "guild_id": str(self.bot.guild_id)} projection = {"messages": {"$slice": 5}} @@ -245,6 +385,26 @@ async def post_log(self, channel_id: Union[int, str], data: dict) -> dict: {"channel_id": str(channel_id)}, {"$set": data}, return_document=True ) + async def search_closed_by(self, user_id: Union[int, str]): + return await self.logs.find( + {"guild_id": str(self.bot.guild_id), "open": False, "closer.id": str(user_id)}, + {"messages": {"$slice": 5}}, + ).to_list(None) + + async def search_by_text(self, text: str, limit: Optional[int]): + return await self.bot.db.logs.find( + { + "guild_id": str(self.bot.guild_id), + "open": False, + "$text": {"$search": f'"{text}"'}, + }, + {"messages": {"$slice": 5}}, + ).to_list(limit) + + def get_plugin_partition(self, cog): + cls_name = cog.__class__.__name__ + return self.db.plugins[cls_name] + class PluginDatabaseClient: def __init__(self, bot): @@ -252,4 +412,4 @@ def __init__(self, bot): def get_partition(self, cog): cls_name = cog.__class__.__name__ - return self.bot.db.plugins[cls_name] + return self.bot.api.db.plugins[cls_name] diff --git a/core/config.py b/core/config.py index a0eda4a2dc..f2bb54a5c8 100644 --- a/core/config.py +++ b/core/config.py @@ -57,6 +57,7 @@ class ConfigManager: "thread_close_title": "Thread Closed", "thread_close_response": "{closer.mention} has closed this Modmail thread.", "thread_self_close_response": "You have closed this Modmail thread.", + "thread_move_title": "Thread Moved", "thread_move_notify": False, "thread_move_response": "This thread has been moved.", "disabled_new_thread_title": "Not Delivered", @@ -81,7 +82,7 @@ class ConfigManager: "activity_type": None, "status": None, # dm_disabled 0 = none, 1 = new threads, 2 = all threads - # TODO: use emum + # TODO: use enum "dm_disabled": 0, "oauth_whitelist": [], # moderation @@ -107,12 +108,17 @@ class ConfigManager: "log_url": "https://example.com/", "log_url_prefix": "/logs", "mongo_uri": None, + "database_type": "mongodb", + "connection_uri": None, # replace mongo uri in the future "owners": None, # bot "token": None, + "enable_plugins": True, + "enable_eval": True, + # github access token for private repositories + "github_token": None, # Logging "log_level": "INFO", - "enable_plugins": True, } colors = {"mod_color", "recipient_color", "main_color", "error_color"} @@ -128,6 +134,7 @@ class ConfigManager: "thread_auto_close_silently", "thread_move_notify", "enable_plugins", + "enable_eval", } special_types = {"status", "activity_type"} diff --git a/core/config_help.json b/core/config_help.json index db9218d2b3..9778f49f8c 100644 --- a/core/config_help.json +++ b/core/config_help.json @@ -350,6 +350,16 @@ "See also: `thread_close_title`, `thread_close_footer`, `thread_close_response`." ] }, + "thread_move_title": { + "default": "Thread Moved", + "description": "The title of the message embed when a thread is moved.", + "examples": [ + "`{prefix}config set thread_move_title Thread transferred to another channel!" + ], + "notes": [ + "See also: `thread_move_notify`, `thread_move_response`." + ] + }, "thread_move_notify": { "default": "No", "description": "Notify the recipient if the thread was moved.", @@ -358,7 +368,7 @@ "`{prefix}config set thread_move_notify no`" ], "notes": [ - "See also: `thread_move_response`." + "See also: `thread_move_title`, `thread_move_response`." ] }, "thread_move_response": { @@ -369,7 +379,7 @@ ], "notes": [ "Only has an effect when `thread_move_notify` is on.", - "See also: `thread_move_notify`." + "See also: `thread_move_title`, `thread_move_notify`." ] }, "disabled_new_thread_title": { diff --git a/core/paginator.py b/core/paginator.py index 45c059f844..7ba1c98b60 100644 --- a/core/paginator.py +++ b/core/paginator.py @@ -176,7 +176,7 @@ async def close(self, delete: bool = True) -> typing.Optional[Message]: """ self.running = False - sent_emoji, x = await self.ctx.bot.retrieve_emoji() + sent_emoji, _ = await self.ctx.bot.retrieve_emoji() await self.ctx.bot.add_reaction(self.ctx.message, sent_emoji) if delete: diff --git a/core/thread.py b/core/thread.py index 0926a0acd0..9c388389e0 100644 --- a/core/thread.py +++ b/core/thread.py @@ -72,15 +72,13 @@ def ready(self) -> bool: def ready(self, flag: bool): if flag: self._ready_event.set() - self.bot.dispatch("thread_ready", self) + self.bot.dispatch("thread_create", self) else: self._ready_event.clear() async def setup(self, *, creator=None, category=None): """Create the thread channel and other io related initialisation tasks""" - - self.bot.dispatch("thread_create", self) - + self.bot.dispatch("thread_initiate", self) recipient = self.recipient # in case it creates a channel outside of category @@ -175,6 +173,7 @@ async def send_recipient_genesis_message(): await self.bot.add_reaction(msg, close_emoji) await asyncio.gather(send_genesis_message(), send_recipient_genesis_message()) + self.bot.dispatch("thread_ready", self) def _format_info_embed(self, user, log_url, log_count, color): """Get information about a member of a server @@ -317,6 +316,7 @@ async def _close( { "open": False, "closed_at": str(datetime.utcnow()), + "nsfw": self.channel.nsfw, "close_message": message if not silent else None, "closer": { "id": str(closer.id), @@ -340,7 +340,12 @@ async def _close( else: sneak_peak = "No content" - desc = f"[`{log_data['key']}`]({log_url}): " + if self.channel.nsfw: + _nsfw = "NSFW-" + else: + _nsfw = "" + + desc = f"[`{_nsfw}{log_data['key']}`]({log_url}): " desc += truncate(sneak_peak, max=75 - 13) else: desc = "Could not resolve log url." @@ -362,7 +367,7 @@ async def _close( event = "Thread Closed as Scheduled" if scheduled else "Thread Closed" # embed.set_author(name=f"Event: {event}", url=log_url) - embed.set_footer(text=f"{event} by {_closer}") + embed.set_footer(text=f"{event} by {_closer}", icon_url=closer.avatar_url) embed.timestamp = datetime.utcnow() tasks = [self.bot.config.update()] @@ -457,9 +462,14 @@ async def find_linked_messages( message_id: typing.Optional[int] = None, either_direction: bool = False, message1: discord.Message = None, + note: bool = True, ) -> typing.Tuple[discord.Message, typing.Optional[discord.Message]]: if message1 is not None: - if not message1.embeds or not message1.embeds[0].author.url: + if ( + not message1.embeds + or not message1.embeds[0].author.url + or message1.author != self.bot.user + ): raise ValueError("Malformed thread message.") elif message_id is not None: @@ -469,13 +479,18 @@ async def find_linked_messages( raise ValueError("Thread message not found.") if not ( - message1.embeds and message1.embeds[0].author.url and message1.embeds[0].color + message1.embeds + and message1.embeds[0].author.url + and message1.embeds[0].color + and message1.author == self.bot.user ): raise ValueError("Thread message not found.") if message1.embeds[0].color.value == self.bot.main_color and message1.embeds[ 0 ].author.name.startswith("Note"): + if not note: + raise ValueError("Thread message not found.") return message1, None if message1.embeds[0].color.value != self.bot.mod_color and not ( @@ -495,6 +510,8 @@ async def find_linked_messages( and message1.embeds[0].color.value == self.bot.recipient_color ) ) + and message1.embeds[0].author.url.split("#")[-1].isdigit() + and message1.author == self.bot.user ): break else: @@ -516,7 +533,7 @@ async def find_linked_messages( if int(msg.embeds[0].author.url.split("#")[-1]) == joint_id: return message1, msg except ValueError: - raise ValueError("DM message not found.") + continue raise ValueError("DM message not found.") async def edit_message(self, message_id: typing.Optional[int], message: str) -> None: @@ -537,16 +554,13 @@ async def edit_message(self, message_id: typing.Optional[int], message: str) -> await asyncio.gather(*tasks) - async def delete_message(self, message: typing.Union[int, discord.Message] = None) -> None: - try: - if isinstance(message, discord.Message): - message1, message2 = await self.find_linked_messages(message1=message) - else: - message1, message2 = await self.find_linked_messages(message) - except ValueError as e: - logger.warning("Failed to delete message: %s.", e) - raise - + async def delete_message( + self, message: typing.Union[int, discord.Message] = None, note: bool = True + ) -> None: + if isinstance(message, discord.Message): + message1, message2 = await self.find_linked_messages(message1=message, note=note) + else: + message1, message2 = await self.find_linked_messages(message, note=note) tasks = [] if not isinstance(message, discord.Message): tasks += [message1.delete()] @@ -571,12 +585,12 @@ async def find_linked_message_from_dm(self, message, either_direction=False): return linked_message msg_id = url.split("#")[-1] - try: - if int(msg_id) == message.id: - return linked_message - except ValueError: - raise ValueError("Malformed dm channel message.") - raise ValueError("DM channel message not found.") + if not msg_id.isdigit(): + continue + msg_id = int(msg_id) + if int(msg_id) == message.id: + return linked_message + raise ValueError("Thread channel message not found.") async def edit_dm_message(self, message: discord.Message, content: str) -> None: try: diff --git a/core/time.py b/core/time.py index cc30d84f30..331e26349f 100644 --- a/core/time.py +++ b/core/time.py @@ -134,7 +134,7 @@ def convert(self, ctx, argument): # foo date time # first the first two cases: - dt, status, begin, end, x = elements[0] + dt, status, begin, end, _ = elements[0] if not status.hasDateOrTime: return self.check_constraints(self.now, argument) diff --git a/core/translations.py b/core/translations.py deleted file mode 100644 index 98a3327386..0000000000 --- a/core/translations.py +++ /dev/null @@ -1,25 +0,0 @@ -import builtins -import csv -import os - - -class Translator: - def __init__(self): - self.language = os.getenv("language", "en") - self.texts = {} - self.generate_texts() - - def generate_texts(self): - with open(f"languages/{self.language}.csv", encoding="utf8") as f: - reader = csv.reader(f, dialect="unix") - - for n, row in enumerate(reader): - if n != 0: - self.texts[row[0]] = row[1] - - def translate(self, identifier): - return self.texts.get(identifier, identifier) - - -def init(): - builtins._ = Translator().translate diff --git a/languages/en.csv b/languages/en.csv deleted file mode 100644 index 388091bab5..0000000000 --- a/languages/en.csv +++ /dev/null @@ -1,1172 +0,0 @@ -"Identifier","English","Context" -"You can only setup in the Modmail guild: {guild_name}.","You can only setup in the Modmail guild: {guild_name}.","File: cogs\modmail.py/L44" -"{guild_name} is already set up.","{guild_name} is already set up.","File: cogs\modmail.py/L49" -"Error","Error","File: cogs\modmail.py/L53/L217/L225/L233/L712/L1059/L1151 cogs\utility.py/L168/L748/L774/L802/L822/L892/L979/L1045/L1085/L1092/L1099/L1125/L1245/L1316/L1378/L1559" -"Modmail functioning guild not found.","Modmail functioning guild not found.","File: cogs\modmail.py/L54" -"Friendly Reminder","Friendly Reminder","File: cogs\modmail.py/L91" -"You may use the `{prefix}config set log_channel_id ` command to set up a custom log channel, then you can delete this default {log_channel} log channel.","You may use the `{prefix}config set log_channel_id ` command to set up a custom log channel, then you can delete this default {log_channel} log channel.","File: cogs\modmail.py/L92" -"Thanks for using the bot!","Thanks for using the bot!","File: cogs\modmail.py/L99" -"If you like what you see, consider giving the [repo a star](https://github.com/kyb3r/modmail) :star: or if you are feeling generous, check us out on [Patreon](https://patreon.com/kyber)!","If you like what you see, consider giving the [repo a star](https://github.com/kyb3r/modmail) :star: or if you are feeling generous, check us out on [Patreon](https://patreon.com/kyber)!","File: cogs\modmail.py/L100" -"Type ""{prefix}help"" for a complete list of commands.","Type ""{prefix}help"" for a complete list of commands.","File: cogs\modmail.py/L106" -"**Successfully set up server.**\nConsider setting permission levels to give access to roles or users the ability to use Modmail.\n\nType:\n- `{prefix}permissions` and `{prefix}permissions add` for more info on setting permissions.\n- `{prefix}config help` for a list of available customizations.","**Successfully set up server.**\nConsider setting permission levels to give access to roles or users the ability to use Modmail.\n\nType:\n- `{prefix}permissions` and `{prefix}permissions add` for more info on setting permissions.\n- `{prefix}config help` for a list of available customizations.","File: cogs\modmail.py/L115" -"Snippets","Snippets","File: cogs\modmail.py/L175" -"Snippet `{name}` already exists.","Snippet `{name}` already exists.","File: cogs\modmail.py/L219" -"An alias with the same name already exists: `{name}`.","An alias with the same name already exists: `{name}`.","File: cogs\modmail.py/L227" -"Snippet names cannot be longer than 120 characters.","Snippet names cannot be longer than 120 characters.","File: cogs\modmail.py/L235" -"Removed snippet","Removed snippet","File: cogs\modmail.py/L256" -"Snippet `{name}` is now deleted.","Snippet `{name}` is now deleted.","File: cogs\modmail.py/L258" -"Edited snippet","Edited snippet","File: cogs\modmail.py/L281" -"Thread Moved","Thread Moved","File: cogs\modmail.py/L310" -"Scheduled close","Scheduled close","File: cogs\modmail.py/L325" -"This thread will close {silent}in {time}.","This thread will close {silent}in {time}.","File: cogs\modmail.py/L326" -"Message","Message","File: cogs\modmail.py/L331" -"Closing will be cancelled if a thread message is sent.","Closing will be cancelled if a thread message is sent.","File: cogs\modmail.py/L334" -"silent","silent","File: cogs\modmail.py/L369" -"silently","silently","File: cogs\modmail.py/L369" -"cancel","cancel","File: cogs\modmail.py/L370" -"Scheduled close has been cancelled.","Scheduled close has been cancelled.","File: cogs\modmail.py/L378" -"This thread has not already been scheduled to close.","This thread has not already been scheduled to close.","File: cogs\modmail.py/L383" -"{mention} is already going to be mentioned.","{mention} is already going to be mentioned.","File: cogs\modmail.py/L433" -"{mention} will be mentioned on the next message received.","{mention} will be mentioned on the next message received.","File: cogs\modmail.py/L440" -"{mention} does not have a pending notification.","{mention} does not have a pending notification.","File: cogs\modmail.py/L472" -"{mention} will no longer be notified.","{mention} will no longer be notified.","File: cogs\modmail.py/L479" -"{mention} is already subscribed to this thread.","{mention} is already subscribed to this thread.","File: cogs\modmail.py/L512" -"{mention} will now be notified of all messages received.","{mention} will now be notified of all messages received.","File: cogs\modmail.py/L519" -"{mention} is not already subscribed to this thread.","{mention} is not already subscribed to this thread.","File: cogs\modmail.py/L551" -"{mention} is now unsubscribed to this thread.","{mention} is now unsubscribed to this thread.","File: cogs\modmail.py/L558" -"Unknown","Unknown","File: cogs\modmail.py/L610" -"Closed By","Closed By","File: cogs\modmail.py/L613" -"Created by","Created by","File: cogs\modmail.py/L616" -"Preview","Preview","File: cogs\modmail.py/L619" -"Link","Link","File: cogs\modmail.py/L624" -"Log Key","Log Key","File: cogs\modmail.py/L627" -"Recipient ID","Recipient ID","File: cogs\modmail.py/L629" -"This user does not have any previous logs.","This user does not have any previous logs.","File: cogs\modmail.py/L660" -"No log entries have been found for that query","No log entries have been found for that query","File: cogs\modmail.py/L693" -"Log entry `{key}` not found.","Log entry `{key}` not found.","File: cogs\modmail.py/L713" -"Log entry `{key}` successfully deleted.","Log entry `{key}` successfully deleted.","File: cogs\modmail.py/L719" -"{mention} has not responded to any threads.","{mention} has not responded to any threads.","File: cogs\modmail.py/L743" -"No log entries have been found for that query.","No log entries have been found for that query.","File: cogs\modmail.py/L776" -"Failed","Failed","File: cogs\modmail.py/L869/L1177" -"Cannot find a message to edit.","Cannot find a message to edit.","File: cogs\modmail.py/L870" -"Cannot start a thread with a bot.","Cannot start a thread with a bot.","File: cogs\modmail.py/L900" -"A thread for this user already exists in {mention}.","A thread for this user already exists in {mention}.","File: cogs\modmail.py/L908" -"Created Thread","Created Thread","File: cogs\modmail.py/L919" -"Thread started by {author_mention} for {user_mention}.","Thread started by {author_mention} for {user_mention}.","File: cogs\modmail.py/L920" -"Blocked Users","Blocked Users","File: cogs\modmail.py/L939/L960" -"No Reason Provided","No Reason Provided","File: cogs\modmail.py/L960" -"(Continued)","(Continued)","File: cogs\modmail.py/L963 cogs\utility.py/L67" -"Currently there are no blocked users.","Currently there are no blocked users.","File: cogs\modmail.py/L971" -"Success","Success","File: cogs\modmail.py/L997/L1084/L1091/L1308/L1344/L1357/L1377/L1358 cogs\utility.py/L725/L759/L1231/L1291/L1367/L1392/L1559/L1595" -"{mention} is no longer whitelisted.","{mention} is no longer whitelisted.","File: cogs\modmail.py/L998" -"{mention} was previously blocked internally for ","{mention} was previously blocked internally for ","File: cogs\modmail.py/L1018" -"Cannot block {mention}, user is whitelisted.","Cannot block {mention}, user is whitelisted.","File: cogs\modmail.py/L1060" -"{mention} was previously blocked {old_reason}.\n{mention} is now blocked {reason}","{mention} was previously blocked {old_reason}.\n{mention} is now blocked {reason}","File: cogs\modmail.py/L1085" -"{mention} is now blocked {reason}","{mention} is now blocked {reason}","File: cogs\modmail.py/L1093" -"{mention} was previously blocked internally {reason}.\n{mention} is no longer blocked.","{mention} was previously blocked internally {reason}.\n{mention} is no longer blocked.","File: cogs\modmail.py/L1132" -"However, if the original system block reason still applies, {name} will be automatically blocked again. Use ","However, if the original system block reason still applies, {name} will be automatically blocked again. Use ","File: cogs\modmail.py/L1137" -"{mention} is no longer blocked.","{mention} is no longer blocked.","File: cogs\modmail.py/L1147" -"{mention} is not blocked.","{mention} is not blocked.","File: cogs\modmail.py/L1152" -"Cannot find a message to delete.","Cannot find a message to delete.","File: cogs\modmail.py/L1178" -"Modmail will now accept **all** DM messages.","Modmail will now accept **all** DM messages.","File: cogs\modmail.py/L1309" -"Modmail is already accepting all DM messages.","Modmail is already accepting all DM messages.","File: cogs\modmail.py/L1316" -"Modmail will not create any **new** threads.","Modmail will not create any **new** threads.","File: cogs\modmail.py/L1345" -"Modmail is already not creating any new threads.","Modmail is already not creating any new threads.","File: cogs\modmail.py/L1352" -"Modmail will not create **new** threads, but existing threads will now be functioning.","Modmail will not create **new** threads, but existing threads will now be functioning.","File: cogs\modmail.py/L1358" -"Modmail will not accept **any** DM messages.","Modmail will not accept **any** DM messages.","File: cogs\modmail.py/L1378" -"Modmail is already not accepting any DM messages.","Modmail is already not accepting any DM messages.","File: cogs\modmail.py/L1385" -"New Threads Disabled","New Threads Disabled","File: cogs\modmail.py/L1400" -"Modmail is not creating new threads.","Modmail is not creating new threads.","File: cogs\modmail.py/L1401" -"All DM Disabled","All DM Disabled","File: cogs\modmail.py/L1406" -"Modmail is not accepting any DM messages for new and existing threads.","Modmail is not accepting any DM messages for new and existing threads.","File: cogs\modmail.py/L1407" -"Enabled","Enabled","File: cogs\modmail.py/L1412" -"Modmail is accepting all DM messages.","Modmail is accepting all DM messages.","File: cogs\modmail.py/L1413" -"Plugins are still loading, please try again later.","Plugins are still loading, please try again later.","File: cogs\plugins.py/L230/L470" -"Your bot's version is too low. This plugin requires version `{required_version}`.","Your bot's version is too low. This plugin requires version `{required_version}`.","File: cogs\plugins.py/L246" -"Invalid plugin name, double check the plugin name or use one of the following formats: username/repo/plugin, username/repo/plugin@branch.","Invalid plugin name, double check the plugin name or use one of the following formats: username/repo/plugin, username/repo/plugin@branch.","File: cogs\plugins.py/L260" -"This plugin is already installed.","This plugin is already installed.","File: cogs\plugins.py/L295" -"Cannot install this plugin (dupe cog name).","Cannot install this plugin (dupe cog name).","File: cogs\plugins.py/L303" -"Starting to download plugin from {plugin_link}...","Starting to download plugin from {plugin_link}...","File: cogs\plugins.py/L309" -"Failed to download plugin, check logs for error.","Failed to download plugin, check logs for error.","File: cogs\plugins.py/L320" -"Successfully installed plugin.\n*Friendly reminder, plugins have absolute control over your bot. Please only install plugins from developers you trust.*","Successfully installed plugin.\n*Friendly reminder, plugins have absolute control over your bot. Please only install plugins from developers you trust.*","File: cogs\plugins.py/L345" -"This plugin is currently not enabled due to `ENABLE_PLUGINS=false`, to re-enable plugins, remove or change `ENABLE_PLUGINS=true` and restart your bot.","This plugin is currently not enabled due to `ENABLE_PLUGINS=false`, to re-enable plugins, remove or change `ENABLE_PLUGINS=true` and restart your bot.","File: cogs\plugins.py/L355" -"Plugin is not installed.","Plugin is not installed.","File: cogs\plugins.py/L376/L415" -"The plugin is successfully uninstalled.","The plugin is successfully uninstalled.","File: cogs\plugins.py/L402" -"Successfully updated {plugin_name}.","Successfully updated {plugin_name}.","File: cogs\plugins.py/L429" -"No plugins are loaded due to `ENABLE_PLUGINS=false`, to re-enable plugins, remove or set `ENABLE_PLUGINS=true` and restart your bot.","No plugins are loaded due to `ENABLE_PLUGINS=false`, to re-enable plugins, remove or set `ENABLE_PLUGINS=true` and restart your bot.","File: cogs\plugins.py/L462" -"There are no plugins currently loaded.","There are no plugins currently loaded.","File: cogs\plugins.py/L477" -"Loaded plugins:","Loaded plugins:","File: cogs\plugins.py/L498" -"Could not find a plugin with name ""{plugin_name}"" within the registry.","Could not find a plugin with name ""{plugin_name}"" within the registry.","File: cogs\plugins.py/L534" -"Perhaps you meant:","Perhaps you meant:","File: cogs\plugins.py/L541 cogs\utility.py/L211" -"Installation","Installation","File: cogs\plugins.py/L562" -"Your bot is unable to install this plugin, minimum required version is v{required_version}.","Your bot is unable to install this plugin, minimum required version is v{required_version}.","File: cogs\plugins.py/L582" -"Your bot is able to install this plugin.","Your bot is able to install this plugin.","File: cogs\plugins.py/L586" -"Plugin Registry","Plugin Registry","File: cogs\plugins.py/L641" -" - if not format_.strip(): - continue - if len(format_) + len(formats[-1]) >= 1024: - formats.append(format_) - else: - formats[-1] += format_ - - embeds = [] - for format_ in formats: - description = ( - _(cog.description) or _()"," - if not format_.strip(): - continue - if len(format_) + len(formats[-1]) >= 1024: - formats.append(format_) - else: - formats[-1] += format_ - - embeds = [] - for format_ in formats: - description = ( - _(cog.description) or _()","File: cogs\utility.py/L50" -"Miscellaneous commands without a category.","Miscellaneous commands without a category.","File: cogs\utility.py/L63" -"Commands","Commands","File: cogs\utility.py/L67" -"No commands.","No commands.","File: cogs\utility.py/L67" -"Help","Help","File: cogs\utility.py/L71" -"Miscellaneous Commands","Miscellaneous Commands","File: cogs\utility.py/L73" -"Type ""{prefix}{command} command"" ","Type ""{prefix}{command} command"" ","File: cogs\utility.py/L78/L153" -"NONE","NONE","File: cogs\utility.py/L119" -"","","File: cogs\utility.py/L124" -"Permission Level","Permission Level","File: cogs\utility.py/L139" -" - - embed.add_field(name=_()"," - - embed.add_field(name=_()","File: cogs\utility.py/L153" -"{command} is a snippet.","{command} is a snippet.","File: cogs\utility.py/L168" -"Alias `{command}` is invalid, this alias will now be deleted.This alias will now be deleted.","Alias `{command}` is invalid, this alias will now be deleted.This alias will now be deleted.","File: cogs\utility.py/L181" -"{command} is an alias.","{command} is an alias.","File: cogs\utility.py/L190/L192" -"`{command}` points to:","`{command}` points to:","File: cogs\utility.py/L192" -"**`{command}` points to the following steps:**","**`{command}` points to the following steps:**","File: cogs\utility.py/L197" -"Step","Step","File: cogs\utility.py/L200/L1045" -"Type ""{prefix}{command} alias"" ","Type ""{prefix}{command} alias"" ","File: cogs\utility.py/L203" -"Command/Category ""{command}"" not found.","Command/Category ""{command}"" not found.","File: cogs\utility.py/L211" -"Cannot find command or category","Cannot find command or category","File: cogs\utility.py/L225" -"Type ""{prefix}{command}"" ","Type ""{prefix}{command}"" ","File: cogs\utility.py/L227" -"Shows this help message.","Shows this help message.","File: cogs\utility.py/L242" -"The specified version `{version}` could not be found.","The specified version `{version}` could not be found.","File: cogs\utility.py/L266" -"View the changelog here: {url}","View the changelog here: {url}","File: cogs\utility.py/L282" -"Modmail - About","Modmail - About","File: cogs\utility.py/L292" -"This is an open source Discord bot that serves as a means for members to easily communicate with server administrators in an organised manner.","This is an open source Discord bot that serves as a means for members to easily communicate with server administrators in an organised manner.","File: cogs\utility.py/L298" -"Uptime","Uptime","File: cogs\utility.py/L303" -"Latency","Latency","File: cogs\utility.py/L304" -"Version","Version","File: cogs\utility.py/L305" -"Authors","Authors","File: cogs\utility.py/L306" -"You are on the prerelease version β€’ the latest version is v{version}.","You are on the prerelease version β€’ the latest version is v{version}.","File: cogs\utility.py/L315" -"A newer version is available v{version}.","A newer version is available v{version}.","File: cogs\utility.py/L317" -"You are up to date with the latest version.","You are up to date with the latest version.","File: cogs\utility.py/L319" -"Want Modmail in Your Server?","Want Modmail in Your Server?","File: cogs\utility.py/L322" -"Follow the installation guide on [GitHub](https://github.com/kyb3r/modmail/) and join our [Discord server](https://discord.gg/F34cRU8/)!","Follow the installation guide on [GitHub](https://github.com/kyb3r/modmail/) and join our [Discord server](https://discord.gg/F34cRU8/)!","File: cogs\utility.py/L323" -"Support the Developers","Support the Developers","File: cogs\utility.py/L329" -"This bot is completely free for everyone. We rely on kind individuals like you to support us on [`Patreon`](https://patreon.com/kyber) (perks included) to keep this bot free forever!","This bot is completely free for everyone. We rely on kind individuals like you to support us on [`Patreon`](https://patreon.com/kyber) (perks included) to keep this bot free forever!","File: cogs\utility.py/L330" -"Debug Logs:","Debug Logs:","File: cogs\utility.py/L379" -"You don't have any logs at the moment.","You don't have any logs at the moment.","File: cogs\utility.py/L380" -"Go to Heroku to see your logs.","Go to Heroku to see your logs.","File: cogs\utility.py/L382/L450" -"Debug logs - Navigate using the reactions below.","Debug logs - Navigate using the reactions below.","File: cogs\utility.py/L408" -"Debug Logs","Debug Logs","File: cogs\utility.py/L440" -"Something's wrong. We're unable to upload your logs to hastebin.","Something's wrong. We're unable to upload your logs to hastebin.","File: cogs\utility.py/L448" -"Cached logs are now cleared.","Cached logs are now cleared.","File: cogs\utility.py/L470" -"Activity Removed","Activity Removed","File: cogs\utility.py/L501" -"Activity set to: {name} ","Activity set to: {name} ","File: cogs\utility.py/L520" -"to {name}.","to {name}.","File: cogs\utility.py/L522" -"Activity Changed","Activity Changed","File: cogs\utility.py/L527" -"Status Removed","Status Removed","File: cogs\utility.py/L550" -"Status set to: {value}.","Status set to: {value}.","File: cogs\utility.py/L564" -"Status Changed","Status Changed","File: cogs\utility.py/L566" -"Pong! Websocket Latency:","Pong! Websocket Latency:","File: cogs\utility.py/L638" -"Current mention:","Current mention:","File: cogs\utility.py/L657" -"Changed mention!","Changed mention!","File: cogs\utility.py/L663" -"On thread creation the bot now says ""{mention}"".","On thread creation the bot now says ""{mention}"".","File: cogs\utility.py/L664" -"Current prefix","Current prefix","File: cogs\utility.py/L683" -"Changed prefix!","Changed prefix!","File: cogs\utility.py/L689" -"Set prefix to `{prefix}`","Set prefix to `{prefix}`","File: cogs\utility.py/L690" -"Available configuration keys:","Available configuration keys:","File: cogs\utility.py/L725" -"Set `{key}` to `{value}`.","Set `{key}` to `{value}`.","File: cogs\utility.py/L748" -"{key} is an invalid key.","{key} is an invalid key.","File: cogs\utility.py/L756/L780" -"Valid keys","Valid keys","File: cogs\utility.py/L759/L783" -"`{key}` had been reset to default.","`{key}` had been reset to default.","File: cogs\utility.py/L774" -"`{key}` is set to `{value}`","`{key}` is set to `{value}`","File: cogs\utility.py/L799" -"Config variable","Config variable","File: cogs\utility.py/L802" -"`{key}` is an invalid key.","`{key}` is an invalid key.","File: cogs\utility.py/L809/L822" -"Type ""{prefix}config options"" for a list of config variables.","Type ""{prefix}config options"" for a list of config variables.","File: cogs\utility.py/L812" -"Here is a list of currently set configuration variable(s).","Here is a list of currently set configuration variable(s).","File: cogs\utility.py/L818" -"Current config(s):","Current config(s):","File: cogs\utility.py/L822" -"No help details found for `{key}`.","No help details found for `{key}`.","File: cogs\utility.py/L862" -"Configuration description on {current_key}:","Configuration description on {current_key}:","File: cogs\utility.py/L875" -"Information:","Information:","File: cogs\utility.py/L880" -"Example(s):","Example(s):","File: cogs\utility.py/L886" -"Note(s):","Note(s):","File: cogs\utility.py/L892" -"Alias `{name}` is invalid, it used to be `{value}`. This alias will now be deleted.","Alias `{name}` is invalid, it used to be `{value}`. This alias will now be deleted.","File: cogs\utility.py/L939" -"Alias","Alias","File: cogs\utility.py/L949" -"You dont have any aliases at the moment.","You dont have any aliases at the moment.","File: cogs\utility.py/L968" -"Do {prefix}help alias for more commands.","Do {prefix}help alias for more commands.","File: cogs\utility.py/L970" -"Aliases","Aliases","File: cogs\utility.py/L971" -"Command Aliases","Command Aliases","File: cogs\utility.py/L979" -"Invalid multi-step alias, try wrapping each steps in quotes.","Invalid multi-step alias, try wrapping each steps in quotes.","File: cogs\utility.py/L1009" -"{action} alias","{action} alias","File: cogs\utility.py/L1024" -"`{name}` points to:","`{name}` points to:","File: cogs\utility.py/L1027" -"`{name}` now points to the following steps:","`{name}` now points to the following steps:","File: cogs\utility.py/L1029" -"The command you are attempting to point to does not exist: `{command}`.","The command you are attempting to point to does not exist: `{command}`.","File: cogs\utility.py/L1045" -"The command you are attempting to point to on step {number} does not exist: `{command}`.","The command you are attempting to point to on step {number} does not exist: `{command}`.","File: cogs\utility.py/L1045" -"A command with the same name already exists: `{name}`.","A command with the same name already exists: `{name}`.","File: cogs\utility.py/L1085" -"Another alias with the same name already exists: `{name}`.","Another alias with the same name already exists: `{name}`.","File: cogs\utility.py/L1092" -"A snippet with the same name already exists: `{name}`.","A snippet with the same name already exists: `{name}`.","File: cogs\utility.py/L1099" -"Alias names cannot be longer than 120 characters.","Alias names cannot be longer than 120 characters.","File: cogs\utility.py/L1106" -"Removed alias","Removed alias","File: cogs\utility.py/L1123" -"Successfully deleted `{name}`.","Successfully deleted `{name}`.","File: cogs\utility.py/L1125" -"The referenced level does not exist: `{level}`.","The referenced level does not exist: `{level}`.","File: cogs\utility.py/L1231" -"Successfully set command permission level for `{command}` to `{level}`.","Successfully set command permission level for `{command}` to `{level}`.","File: cogs\utility.py/L1245" -"The referenced {type} does not exist: `{name}`.","The referenced {type} does not exist: `{name}`.","File: cogs\utility.py/L1291" -"Permission for `{name}` was successfully updated.","Permission for `{name}` was successfully updated.","File: cogs\utility.py/L1316/L1419" -"The command permission level was never overridden: `{name}`, current permission level is {perm_name}.","The command permission level was never overridden: `{name}`, current permission level is {perm_name}.","File: cogs\utility.py/L1367/L1559" -"Command permission level for `{name}` was successfully restored to {perm_name}.","Command permission level for `{name}` was successfully restored to {perm_name}.","File: cogs\utility.py/L1378" -"The referenced level does not exist: `{name}`.","The referenced level does not exist: `{name}`.","File: cogs\utility.py/L1392" -"Permission entries for {type} `{name}`:","Permission entries for {type} `{name}`:","File: cogs\utility.py/L1430/L1455" -"No permission entries found.","No permission entries found.","File: cogs\utility.py/L1431/L1523" -"{mention} has permission with the following commands:","{mention} has permission with the following commands:","File: cogs\utility.py/L1528" -"{mention} has permission with the following permission levels:","{mention} has permission with the following permission levels:","File: cogs\utility.py/L1533" -"Permission Overrides","Permission Overrides","File: cogs\utility.py/L1558/L1559" -"You don't have any command level overrides at the moment.","You don't have any command level overrides at the moment.","File: cogs\utility.py/L1559" -"Permission override for command ""{name}"" is ""{perm_name}"".","Permission override for command ""{name}"" is ""{perm_name}"".","File: cogs\utility.py/L1595" -"Un-whitelisted {target_mention} to view logs.","Un-whitelisted {target_mention} to view logs.","File: cogs\utility.py/L1678" -"Whitelisted {target_mention} to view logs.","Whitelisted {target_mention} to view logs.","File: cogs\utility.py/L1682" -"Oauth Whitelist","Oauth Whitelist","File: cogs\utility.py/L1705" -"Users","Users","File: cogs\utility.py/L1708" -"None","None","File: cogs\utility.py/L1708" -"Channel has been deleted, no closer found.","Channel has been deleted, no closer found.","File: bot.py/L472" -"System Message: New Account. Required to wait for {time}.","System Message: New Account. Required to wait for {time}.","File: bot.py/L552" -"Commands directly related to Modmail functionality.","Commands directly related to Modmail functionality.","Cog: Modmail" -"Sets up a server for Modmail.","Sets up a server for Modmail.","Cog: Modmail -Command: setup" -"Sets up a server for Modmail. - -You only need to run this command -once after configuring Modmail.","Sets up a server for Modmail. - -You only need to run this command -once after configuring Modmail.","Cog: Modmail -Command: setup" -"Create pre-defined messages for use in threads.","Create pre-defined messages for use in threads.","Cog: Modmail -Command: snippet" -"Create pre-defined messages for use in threads. - -When `{prefix}snippet` is used by itself, this will retrieve -a list of snippets that are currently set. `{prefix}snippet-name` will show what the -snippet point to. - -To create a snippet: -- `{prefix}snippet add snippet-name A pre-defined text.` - -You can use your snippet in a thread channel -with `{prefix}snippet-name`, the message ""A pre-defined text."" -will be sent to the recipient. - -Currently, there is not a built-in anonymous snippet command; however, a workaround -is available using `{prefix}alias`. Here is how: -- `{prefix}alias add snippet-name anonreply A pre-defined anonymous text.` - -See also `{prefix}alias`.","Create pre-defined messages for use in threads. - -When `{prefix}snippet` is used by itself, this will retrieve -a list of snippets that are currently set. `{prefix}snippet-name` will show what the -snippet point to. - -To create a snippet: -- `{prefix}snippet add snippet-name A pre-defined text.` - -You can use your snippet in a thread channel -with `{prefix}snippet-name`, the message ""A pre-defined text."" -will be sent to the recipient. - -Currently, there is not a built-in anonymous snippet command; however, a workaround -is available using `{prefix}alias`. Here is how: -- `{prefix}alias add snippet-name anonreply A pre-defined anonymous text.` - -See also `{prefix}alias`.","Cog: Modmail -Command: snippet" -"View the raw content of a snippet.","View the raw content of a snippet.","Cog: Modmail -Command: snippet raw" -"View the raw content of a snippet.","View the raw content of a snippet.","Cog: Modmail -Command: snippet raw" -"Add a snippet.","Add a snippet.","Cog: Modmail -Command: snippet add" -"Add a snippet. - -Simply to add a snippet, do: ``` -{prefix}snippet add hey hello there :) -``` -then when you type `{prefix}hey`, ""hello there :)"" will get sent to the recipient. - -To add a multi-word snippet name, use quotes: ``` -{prefix}snippet add ""two word"" this is a two word snippet. -```","Add a snippet. - -Simply to add a snippet, do: ``` -{prefix}snippet add hey hello there :) -``` -then when you type `{prefix}hey`, ""hello there :)"" will get sent to the recipient. - -To add a multi-word snippet name, use quotes: ``` -{prefix}snippet add ""two word"" this is a two word snippet. -```","Cog: Modmail -Command: snippet add" -"Remove a snippet.","Remove a snippet.","Cog: Modmail -Command: snippet remove" -"Remove a snippet.","Remove a snippet.","Cog: Modmail -Command: snippet remove" -"Edit a snippet.","Edit a snippet.","Cog: Modmail -Command: snippet edit" -"Edit a snippet. - -To edit a multi-word snippet name, use quotes: ``` -{prefix}snippet edit ""two word"" this is a new two word snippet. -```","Edit a snippet. - -To edit a multi-word snippet name, use quotes: ``` -{prefix}snippet edit ""two word"" this is a new two word snippet. -```","Cog: Modmail -Command: snippet edit" -"Move a thread to another category.","Move a thread to another category.","Cog: Modmail -Command: move" -"Move a thread to another category. - -`category` may be a category ID, mention, or name. -`specifics` is a string which takes in arguments on how to perform the move. Ex: ""silently""","Move a thread to another category. - -`category` may be a category ID, mention, or name. -`specifics` is a string which takes in arguments on how to perform the move. Ex: ""silently""","Cog: Modmail -Command: move" -"Close the current thread.","Close the current thread.","Cog: Modmail -Command: close" -"Close the current thread. - -Close after a period of time: -- `{prefix}close in 5 hours` -- `{prefix}close 2m30s` - -Custom close messages: -- `{prefix}close 2 hours The issue has been resolved.` -- `{prefix}close We will contact you once we find out more.` - -Silently close a thread (no message) -- `{prefix}close silently` -- `{prefix}close in 10m silently` - -Stop a thread from closing: -- `{prefix}close cancel`","Close the current thread. - -Close after a period of time: -- `{prefix}close in 5 hours` -- `{prefix}close 2m30s` - -Custom close messages: -- `{prefix}close 2 hours The issue has been resolved.` -- `{prefix}close We will contact you once we find out more.` - -Silently close a thread (no message) -- `{prefix}close silently` -- `{prefix}close in 10m silently` - -Stop a thread from closing: -- `{prefix}close cancel`","Cog: Modmail -Command: close" -"Notify a user or role when the next thread message received.","Notify a user or role when the next thread message received.","Cog: Modmail -Command: notify" -"Notify a user or role when the next thread message received. - -Once a thread message is received, `user_or_role` will be pinged once. - -Leave `user_or_role` empty to notify yourself. -`@here` and `@everyone` can be substituted with `here` and `everyone`. -`user_or_role` may be a user ID, mention, name. role ID, mention, name, ""everyone"", or ""here"".","Notify a user or role when the next thread message received. - -Once a thread message is received, `user_or_role` will be pinged once. - -Leave `user_or_role` empty to notify yourself. -`@here` and `@everyone` can be substituted with `here` and `everyone`. -`user_or_role` may be a user ID, mention, name. role ID, mention, name, ""everyone"", or ""here"".","Cog: Modmail -Command: notify" -"Un-notify a user, role, or yourself from a thread.","Un-notify a user, role, or yourself from a thread.","Cog: Modmail -Command: unnotify" -"Un-notify a user, role, or yourself from a thread. - -Leave `user_or_role` empty to un-notify yourself. -`@here` and `@everyone` can be substituted with `here` and `everyone`. -`user_or_role` may be a user ID, mention, name, role ID, mention, name, ""everyone"", or ""here"".","Un-notify a user, role, or yourself from a thread. - -Leave `user_or_role` empty to un-notify yourself. -`@here` and `@everyone` can be substituted with `here` and `everyone`. -`user_or_role` may be a user ID, mention, name, role ID, mention, name, ""everyone"", or ""here"".","Cog: Modmail -Command: unnotify" -"Notify a user, role, or yourself for every thread message received.","Notify a user, role, or yourself for every thread message received.","Cog: Modmail -Command: subscribe" -"Notify a user, role, or yourself for every thread message received. - -You will be pinged for every thread message received until you unsubscribe. - -Leave `user_or_role` empty to subscribe yourself. -`@here` and `@everyone` can be substituted with `here` and `everyone`. -`user_or_role` may be a user ID, mention, name, role ID, mention, name, ""everyone"", or ""here"".","Notify a user, role, or yourself for every thread message received. - -You will be pinged for every thread message received until you unsubscribe. - -Leave `user_or_role` empty to subscribe yourself. -`@here` and `@everyone` can be substituted with `here` and `everyone`. -`user_or_role` may be a user ID, mention, name, role ID, mention, name, ""everyone"", or ""here"".","Cog: Modmail -Command: subscribe" -"Unsubscribe a user, role, or yourself from a thread.","Unsubscribe a user, role, or yourself from a thread.","Cog: Modmail -Command: unsubscribe" -"Unsubscribe a user, role, or yourself from a thread. - -Leave `user_or_role` empty to unsubscribe yourself. -`@here` and `@everyone` can be substituted with `here` and `everyone`. -`user_or_role` may be a user ID, mention, name, role ID, mention, name, ""everyone"", or ""here"".","Unsubscribe a user, role, or yourself from a thread. - -Leave `user_or_role` empty to unsubscribe yourself. -`@here` and `@everyone` can be substituted with `here` and `everyone`. -`user_or_role` may be a user ID, mention, name, role ID, mention, name, ""everyone"", or ""here"".","Cog: Modmail -Command: unsubscribe" -"Flags a Modmail thread as NSFW (not safe for work).","Flags a Modmail thread as NSFW (not safe for work).","Cog: Modmail -Command: nsfw" -"Flags a Modmail thread as NSFW (not safe for work).","Flags a Modmail thread as NSFW (not safe for work).","Cog: Modmail -Command: nsfw" -"Flags a Modmail thread as SFW (safe for work).","Flags a Modmail thread as SFW (safe for work).","Cog: Modmail -Command: sfw" -"Flags a Modmail thread as SFW (safe for work).","Flags a Modmail thread as SFW (safe for work).","Cog: Modmail -Command: sfw" -"Retrieves the link to the current thread's logs.","Retrieves the link to the current thread's logs.","Cog: Modmail -Command: loglink" -"Retrieves the link to the current thread's logs.","Retrieves the link to the current thread's logs.","Cog: Modmail -Command: loglink" -"Get previous Modmail thread logs of a member.","Get previous Modmail thread logs of a member.","Cog: Modmail -Command: logs" -"Get previous Modmail thread logs of a member. - -Leave `user` blank when this command is used within a -thread channel to show logs for the current recipient. -`user` may be a user ID, mention, or name.","Get previous Modmail thread logs of a member. - -Leave `user` blank when this command is used within a -thread channel to show logs for the current recipient. -`user` may be a user ID, mention, or name.","Cog: Modmail -Command: logs" -"Get all logs closed by the specified user.","Get all logs closed by the specified user.","Cog: Modmail -Command: logs closed-by" -"Get all logs closed by the specified user. - -If no `user` is provided, the user will be the person who sent this command. -`user` may be a user ID, mention, or name.","Get all logs closed by the specified user. - -If no `user` is provided, the user will be the person who sent this command. -`user` may be a user ID, mention, or name.","Cog: Modmail -Command: logs closed-by" -"Wipe a log entry from the database.","Wipe a log entry from the database.","Cog: Modmail -Command: logs delete" -"Wipe a log entry from the database.","Wipe a log entry from the database.","Cog: Modmail -Command: logs delete" -"Get all logs where the specified user has responded at least once.","Get all logs where the specified user has responded at least once.","Cog: Modmail -Command: logs responded" -"Get all logs where the specified user has responded at least once. - -If no `user` is provided, the user will be the person who sent this command. -`user` may be a user ID, mention, or name.","Get all logs where the specified user has responded at least once. - -If no `user` is provided, the user will be the person who sent this command. -`user` may be a user ID, mention, or name.","Cog: Modmail -Command: logs responded" -"Retrieve all logs that contain messages with your query.","Retrieve all logs that contain messages with your query.","Cog: Modmail -Command: logs search" -"Retrieve all logs that contain messages with your query. - -Provide a `limit` to specify the maximum number of logs the bot should find.","Retrieve all logs that contain messages with your query. - -Provide a `limit` to specify the maximum number of logs the bot should find.","Cog: Modmail -Command: logs search" -"Reply to a Modmail thread.","Reply to a Modmail thread.","Cog: Modmail -Command: reply" -"Reply to a Modmail thread. - -Supports attachments and images as well as -automatically embedding image URLs.","Reply to a Modmail thread. - -Supports attachments and images as well as -automatically embedding image URLs.","Cog: Modmail -Command: reply" -"Reply to a Modmail thread with variables.","Reply to a Modmail thread with variables.","Cog: Modmail -Command: freply" -"Reply to a Modmail thread with variables. - -Works just like `{prefix}reply`, however with the addition of three variables: - - `{{channel}}` - the `discord.TextChannel` object - - `{{recipient}}` - the `discord.User` object of the recipient - - `{{author}}` - the `discord.User` object of the author - -Supports attachments and images as well as -automatically embedding image URLs.","Reply to a Modmail thread with variables. - -Works just like `{prefix}reply`, however with the addition of three variables: - - `{{channel}}` - the `discord.TextChannel` object - - `{{recipient}}` - the `discord.User` object of the recipient - - `{{author}}` - the `discord.User` object of the author - -Supports attachments and images as well as -automatically embedding image URLs.","Cog: Modmail -Command: freply" -"Reply to a thread anonymously.","Reply to a thread anonymously.","Cog: Modmail -Command: areply" -"Reply to a thread anonymously. - -You can edit the anonymous user's name, -avatar and tag using the config command. - -Edit the `anon_username`, `anon_avatar_url` -and `anon_tag` config variables to do so.","Reply to a thread anonymously. - -You can edit the anonymous user's name, -avatar and tag using the config command. - -Edit the `anon_username`, `anon_avatar_url` -and `anon_tag` config variables to do so.","Cog: Modmail -Command: areply" -"Take a note about the current thread.","Take a note about the current thread.","Cog: Modmail -Command: note" -"Take a note about the current thread. - -Useful for noting context.","Take a note about the current thread. - -Useful for noting context.","Cog: Modmail -Command: note" -"Edit a message that was sent using the reply or anonreply command.","Edit a message that was sent using the reply or anonreply command.","Cog: Modmail -Command: edit" -"Edit a message that was sent using the reply or anonreply command. - -If no `message_id` is provided, -the last message sent by a staff will be edited. - -Note: attachments **cannot** be edited.","Edit a message that was sent using the reply or anonreply command. - -If no `message_id` is provided, -the last message sent by a staff will be edited. - -Note: attachments **cannot** be edited.","Cog: Modmail -Command: edit" -"Create a thread with a specified member.","Create a thread with a specified member.","Cog: Modmail -Command: contact" -"Create a thread with a specified member. - -If `category` is specified, the thread -will be created in that specified category. - -`category`, if specified, may be a category ID, mention, or name. -`user` may be a user ID, mention, or name.","Create a thread with a specified member. - -If `category` is specified, the thread -will be created in that specified category. - -`category`, if specified, may be a category ID, mention, or name. -`user` may be a user ID, mention, or name.","Cog: Modmail -Command: contact" -"Retrieve a list of blocked users.","Retrieve a list of blocked users.","Cog: Modmail -Command: blocked" -"Retrieve a list of blocked users.","Retrieve a list of blocked users.","Cog: Modmail -Command: blocked" -"Whitelist or un-whitelist a user from getting blocked.","Whitelist or un-whitelist a user from getting blocked.","Cog: Modmail -Command: blocked whitelist" -"Whitelist or un-whitelist a user from getting blocked. - -Useful for preventing users from getting blocked by account_age/guild_age restrictions.","Whitelist or un-whitelist a user from getting blocked. - -Useful for preventing users from getting blocked by account_age/guild_age restrictions.","Cog: Modmail -Command: blocked whitelist" -"Block a user from using Modmail.","Block a user from using Modmail.","Cog: Modmail -Command: block" -"Block a user from using Modmail. - -You may choose to set a time as to when the user will automatically be unblocked. - -Leave `user` blank when this command is used within a -thread channel to block the current recipient. -`user` may be a user ID, mention, or name. -`duration` may be a simple ""human-readable"" time text. See `{prefix}help close` for examples.","Block a user from using Modmail. - -You may choose to set a time as to when the user will automatically be unblocked. - -Leave `user` blank when this command is used within a -thread channel to block the current recipient. -`user` may be a user ID, mention, or name. -`duration` may be a simple ""human-readable"" time text. See `{prefix}help close` for examples.","Cog: Modmail -Command: block" -"Unblock a user from using Modmail.","Unblock a user from using Modmail.","Cog: Modmail -Command: unblock" -"Unblock a user from using Modmail. - -Leave `user` blank when this command is used within a -thread channel to unblock the current recipient. -`user` may be a user ID, mention, or name.","Unblock a user from using Modmail. - -Leave `user` blank when this command is used within a -thread channel to unblock the current recipient. -`user` may be a user ID, mention, or name.","Cog: Modmail -Command: unblock" -"Delete a message that was sent using the reply command or a note.","Delete a message that was sent using the reply command or a note.","Cog: Modmail -Command: delete" -"Delete a message that was sent using the reply command or a note. - -Deletes the previous message, unless a message ID is provided, -which in that case, deletes the message with that message ID. - -Notes can only be deleted when a note ID is provided.","Delete a message that was sent using the reply command or a note. - -Deletes the previous message, unless a message ID is provided, -which in that case, deletes the message with that message ID. - -Notes can only be deleted when a note ID is provided.","Cog: Modmail -Command: delete" -"Repair a thread broken by Discord.","Repair a thread broken by Discord.","Cog: Modmail -Command: repair" -"Repair a thread broken by Discord.","Repair a thread broken by Discord.","Cog: Modmail -Command: repair" -"Re-enables DM functionalities of Modmail.","Re-enables DM functionalities of Modmail.","Cog: Modmail -Command: enable" -"Re-enables DM functionalities of Modmail. - -Undo's the `{prefix}disable` command, all DM will be relayed after running this command.","Re-enables DM functionalities of Modmail. - -Undo's the `{prefix}disable` command, all DM will be relayed after running this command.","Cog: Modmail -Command: enable" -"Disable partial or full Modmail thread functions.","Disable partial or full Modmail thread functions.","Cog: Modmail -Command: disable" -"Disable partial or full Modmail thread functions. - -To stop all new threads from being created, do `{prefix}disable new`. -To stop all existing threads from DMing Modmail, do `{prefix}disable all`. -To check if the DM function for Modmail is enabled, do `{prefix}isenable`.","Disable partial or full Modmail thread functions. - -To stop all new threads from being created, do `{prefix}disable new`. -To stop all existing threads from DMing Modmail, do `{prefix}disable all`. -To check if the DM function for Modmail is enabled, do `{prefix}isenable`.","Cog: Modmail -Command: disable" -"Stop accepting new Modmail threads.","Stop accepting new Modmail threads.","Cog: Modmail -Command: disable new" -"Stop accepting new Modmail threads. - -No new threads can be created through DM.","Stop accepting new Modmail threads. - -No new threads can be created through DM.","Cog: Modmail -Command: disable new" -"Disables all DM functionalities of Modmail.","Disables all DM functionalities of Modmail.","Cog: Modmail -Command: disable all" -"Disables all DM functionalities of Modmail. - -No new threads can be created through DM nor no further DM messages will be relayed.","Disables all DM functionalities of Modmail. - -No new threads can be created through DM nor no further DM messages will be relayed.","Cog: Modmail -Command: disable all" -"Check if the DM functionalities of Modmail is enabled.","Check if the DM functionalities of Modmail is enabled.","Cog: Modmail -Command: isenable" -"Check if the DM functionalities of Modmail is enabled.","Check if the DM functionalities of Modmail is enabled.","Cog: Modmail -Command: isenable" -"Plugins expand Modmail functionality by allowing third-party addons. - -These addons could have a range of features from moderation to simply -making your life as a moderator easier! -Learn how to create a plugin yourself here: -https://github.com/kyb3r/modmail/wiki/Plugins","Plugins expand Modmail functionality by allowing third-party addons. - -These addons could have a range of features from moderation to simply -making your life as a moderator easier! -Learn how to create a plugin yourself here: -https://github.com/kyb3r/modmail/wiki/Plugins","Cog: Plugins" -"Manage plugins for Modmail.","Manage plugins for Modmail.","Cog: Plugins -Command: plugins" -"Manage plugins for Modmail.","Manage plugins for Modmail.","Cog: Plugins -Command: plugins" -"Install a new plugin for the bot.","Install a new plugin for the bot.","Cog: Plugins -Command: plugins add" -"Install a new plugin for the bot. - -`plugin_name` can be the name of the plugin found in `{prefix}plugin registry`, -or a direct reference to a GitHub hosted plugin (in the format `user/repo/name[@branch]`).","Install a new plugin for the bot. - -`plugin_name` can be the name of the plugin found in `{prefix}plugin registry`, -or a direct reference to a GitHub hosted plugin (in the format `user/repo/name[@branch]`).","Cog: Plugins -Command: plugins add" -"Remove an installed plugin of the bot.","Remove an installed plugin of the bot.","Cog: Plugins -Command: plugins remove" -"Remove an installed plugin of the bot. - -`plugin_name` can be the name of the plugin found in `{prefix}plugin registry`, or a direct reference -to a GitHub hosted plugin (in the format `user/repo/name[@branch]`).","Remove an installed plugin of the bot. - -`plugin_name` can be the name of the plugin found in `{prefix}plugin registry`, or a direct reference -to a GitHub hosted plugin (in the format `user/repo/name[@branch]`).","Cog: Plugins -Command: plugins remove" -"Update a plugin for the bot.","Update a plugin for the bot.","Cog: Plugins -Command: plugins update" -"Update a plugin for the bot. - -`plugin_name` can be the name of the plugin found in `{prefix}plugin registry`, or a direct reference -to a GitHub hosted plugin (in the format `user/repo/name[@branch]`). - -To update all plugins, do `{prefix}plugins update`.","Update a plugin for the bot. - -`plugin_name` can be the name of the plugin found in `{prefix}plugin registry`, or a direct reference -to a GitHub hosted plugin (in the format `user/repo/name[@branch]`). - -To update all plugins, do `{prefix}plugins update`.","Cog: Plugins -Command: plugins update" -"Show a list of currently loaded plugins.","Show a list of currently loaded plugins.","Cog: Plugins -Command: plugins loaded" -"Show a list of currently loaded plugins.","Show a list of currently loaded plugins.","Cog: Plugins -Command: plugins loaded" -"Shows a list of all approved plugins.","Shows a list of all approved plugins.","Cog: Plugins -Command: plugins registry" -"Shows a list of all approved plugins. - -Usage: -`{prefix}plugin registry` Details about all plugins. -`{prefix}plugin registry plugin-name` Details about the indicated plugin. -`{prefix}plugin registry page-number` Jump to a page in the registry.","Shows a list of all approved plugins. - -Usage: -`{prefix}plugin registry` Details about all plugins. -`{prefix}plugin registry plugin-name` Details about the indicated plugin. -`{prefix}plugin registry page-number` Jump to a page in the registry.","Cog: Plugins -Command: plugins registry" -"Shows a compact view of all plugins within the registry.","Shows a compact view of all plugins within the registry.","Cog: Plugins -Command: plugins registry compact" -"Shows a compact view of all plugins within the registry.","Shows a compact view of all plugins within the registry.","Cog: Plugins -Command: plugins registry compact" -"General commands that provide utility.","General commands that provide utility.","Cog: Utility" -"Shows the changelog of the Modmail.","Shows the changelog of the Modmail.","Cog: Utility -Command: changelog" -"Shows the changelog of the Modmail.","Shows the changelog of the Modmail.","Cog: Utility -Command: changelog" -"Shows information about this bot.","Shows information about this bot.","Cog: Utility -Command: about" -"Shows information about this bot.","Shows information about this bot.","Cog: Utility -Command: about" -"Shows a list of sponsors.","Shows a list of sponsors.","Cog: Utility -Command: sponsors" -"Shows a list of sponsors.","Shows a list of sponsors.","Cog: Utility -Command: sponsors" -"Shows the recent application logs of the bot.","Shows the recent application logs of the bot.","Cog: Utility -Command: debug" -"Shows the recent application logs of the bot.","Shows the recent application logs of the bot.","Cog: Utility -Command: debug" -"Posts application-logs to Hastebin.","Posts application-logs to Hastebin.","Cog: Utility -Command: debug hastebin" -"Posts application-logs to Hastebin.","Posts application-logs to Hastebin.","Cog: Utility -Command: debug hastebin" -"Clears the locally cached logs.","Clears the locally cached logs.","Cog: Utility -Command: debug clear" -"Clears the locally cached logs.","Clears the locally cached logs.","Cog: Utility -Command: debug clear" -"Set an activity status for the bot.","Set an activity status for the bot.","Cog: Utility -Command: activity" -"Set an activity status for the bot. - -Possible activity types: - - `playing` - - `streaming` - - `listening` - - `watching` - -When activity type is set to `listening`, -it must be followed by a ""to"": ""listening to..."" - -When activity type is set to `streaming`, you can set -the linked twitch page: -- `{prefix}config set twitch_url https://www.twitch.tv/somechannel/` - -To remove the current activity status: -- `{prefix}activity clear`","Set an activity status for the bot. - -Possible activity types: - - `playing` - - `streaming` - - `listening` - - `watching` - -When activity type is set to `listening`, -it must be followed by a ""to"": ""listening to..."" - -When activity type is set to `streaming`, you can set -the linked twitch page: -- `{prefix}config set twitch_url https://www.twitch.tv/somechannel/` - -To remove the current activity status: -- `{prefix}activity clear`","Cog: Utility -Command: activity" -"Set a status for the bot.","Set a status for the bot.","Cog: Utility -Command: status" -"Set a status for the bot. - -Possible status types: - - `online` - - `idle` - - `dnd` or `do not disturb` - - `invisible` or `offline` - -To remove the current status: -- `{prefix}status clear`","Set a status for the bot. - -Possible status types: - - `online` - - `idle` - - `dnd` or `do not disturb` - - `invisible` or `offline` - -To remove the current status: -- `{prefix}status clear`","Cog: Utility -Command: status" -"Pong! Returns your websocket latency.","Pong! Returns your websocket latency.","Cog: Utility -Command: ping" -"Pong! Returns your websocket latency.","Pong! Returns your websocket latency.","Cog: Utility -Command: ping" -"Change what the bot mentions at the start of each thread.","Change what the bot mentions at the start of each thread.","Cog: Utility -Command: mention" -"Change what the bot mentions at the start of each thread. - -Type only `{prefix}mention` to retrieve your current ""mention"" message.","Change what the bot mentions at the start of each thread. - -Type only `{prefix}mention` to retrieve your current ""mention"" message.","Cog: Utility -Command: mention" -"Change the prefix of the bot.","Change the prefix of the bot.","Cog: Utility -Command: prefix" -"Change the prefix of the bot. - -Type only `{prefix}prefix` to retrieve your current bot prefix.","Change the prefix of the bot. - -Type only `{prefix}prefix` to retrieve your current bot prefix.","Cog: Utility -Command: prefix" -"Modify changeable configuration variables for this bot.","Modify changeable configuration variables for this bot.","Cog: Utility -Command: config" -"Modify changeable configuration variables for this bot. - -Type `{prefix}config options` to view a list -of valid configuration variables. - -Type `{prefix}config help config-name` for info - on a config. - -To set a configuration variable: -- `{prefix}config set config-name value here` - -To remove a configuration variable: -- `{prefix}config remove config-name`","Modify changeable configuration variables for this bot. - -Type `{prefix}config options` to view a list -of valid configuration variables. - -Type `{prefix}config help config-name` for info - on a config. - -To set a configuration variable: -- `{prefix}config set config-name value here` - -To remove a configuration variable: -- `{prefix}config remove config-name`","Cog: Utility -Command: config" -"Return a list of valid configuration names you can change.","Return a list of valid configuration names you can change.","Cog: Utility -Command: config options" -"Return a list of valid configuration names you can change.","Return a list of valid configuration names you can change.","Cog: Utility -Command: config options" -"Set a configuration variable and its value.","Set a configuration variable and its value.","Cog: Utility -Command: config set" -"Set a configuration variable and its value.","Set a configuration variable and its value.","Cog: Utility -Command: config set" -"Delete a set configuration variable.","Delete a set configuration variable.","Cog: Utility -Command: config remove" -"Delete a set configuration variable.","Delete a set configuration variable.","Cog: Utility -Command: config remove" -"Show the configuration variables that are currently set.","Show the configuration variables that are currently set.","Cog: Utility -Command: config get" -"Show the configuration variables that are currently set. - -Leave `key` empty to show all currently set configuration variables.","Show the configuration variables that are currently set. - -Leave `key` empty to show all currently set configuration variables.","Cog: Utility -Command: config get" -"Show information on a specified configuration.","Show information on a specified configuration.","Cog: Utility -Command: config help" -"Show information on a specified configuration.","Show information on a specified configuration.","Cog: Utility -Command: config help" -"Create shortcuts to bot commands.","Create shortcuts to bot commands.","Cog: Utility -Command: alias" -"Create shortcuts to bot commands. - -When `{prefix}alias` is used by itself, this will retrieve -a list of alias that are currently set. `{prefix}alias-name` will show what the -alias point to. - -To use alias: - -First create an alias using: -- `{prefix}alias add alias-name other-command` - -For example: -- `{prefix}alias add r reply` -- Now you can use `{prefix}r` as an replacement for `{prefix}reply`. - -See also `{prefix}snippet`.","Create shortcuts to bot commands. - -When `{prefix}alias` is used by itself, this will retrieve -a list of alias that are currently set. `{prefix}alias-name` will show what the -alias point to. - -To use alias: - -First create an alias using: -- `{prefix}alias add alias-name other-command` - -For example: -- `{prefix}alias add r reply` -- Now you can use `{prefix}r` as an replacement for `{prefix}reply`. - -See also `{prefix}snippet`.","Cog: Utility -Command: alias" -"View the raw content of an alias.","View the raw content of an alias.","Cog: Utility -Command: alias raw" -"View the raw content of an alias.","View the raw content of an alias.","Cog: Utility -Command: alias raw" -"Add an alias.","Add an alias.","Cog: Utility -Command: alias add" -"Add an alias. - -Alias also supports multi-step aliases, to create a multi-step alias use quotes -to wrap each step and separate each step with `&&`. For example: - -- `{prefix}alias add movenreply ""move admin-category"" && ""reply Thanks for reaching out to the admins""` - -However, if you run into problems, try wrapping the command with quotes. For example: - -- This will fail: `{prefix}alias add reply You'll need to type && to work` -- Correct method: `{prefix}alias add reply ""You'll need to type && to work""`","Add an alias. - -Alias also supports multi-step aliases, to create a multi-step alias use quotes -to wrap each step and separate each step with `&&`. For example: - -- `{prefix}alias add movenreply ""move admin-category"" && ""reply Thanks for reaching out to the admins""` - -However, if you run into problems, try wrapping the command with quotes. For example: - -- This will fail: `{prefix}alias add reply You'll need to type && to work` -- Correct method: `{prefix}alias add reply ""You'll need to type && to work""`","Cog: Utility -Command: alias add" -"Remove an alias.","Remove an alias.","Cog: Utility -Command: alias remove" -"Remove an alias.","Remove an alias.","Cog: Utility -Command: alias remove" -"Edit an alias.","Edit an alias.","Cog: Utility -Command: alias edit" -"Edit an alias.","Edit an alias.","Cog: Utility -Command: alias edit" -"Set the permissions for Modmail commands.","Set the permissions for Modmail commands.","Cog: Utility -Command: permissions" -"Set the permissions for Modmail commands. - -You may set permissions based on individual command names, or permission -levels. - -Acceptable permission levels are: - - **Owner** [5] (absolute control over the bot) - - **Administrator** [4] (administrative powers such as setting activities) - - **Moderator** [3] (ability to block) - - **Supporter** [2] (access to core Modmail supporting functions) - - **Regular** [1] (most basic interactions such as help and about) - -By default, owner is set to the absolute bot owner and regular is `@everyone`. - -To set permissions, see `{prefix}help permissions add`; and to change permission level for specific -commands see `{prefix}help permissions override`. - -Note: You will still have to manually give/take permission to the Modmail -category to users/roles.","Set the permissions for Modmail commands. - -You may set permissions based on individual command names, or permission -levels. - -Acceptable permission levels are: - - **Owner** [5] (absolute control over the bot) - - **Administrator** [4] (administrative powers such as setting activities) - - **Moderator** [3] (ability to block) - - **Supporter** [2] (access to core Modmail supporting functions) - - **Regular** [1] (most basic interactions such as help and about) - -By default, owner is set to the absolute bot owner and regular is `@everyone`. - -To set permissions, see `{prefix}help permissions add`; and to change permission level for specific -commands see `{prefix}help permissions override`. - -Note: You will still have to manually give/take permission to the Modmail -category to users/roles.","Cog: Utility -Command: permissions" -"Change a permission level for a specific command.","Change a permission level for a specific command.","Cog: Utility -Command: permissions override" -"Change a permission level for a specific command. - -Examples: -- `{prefix}perms override reply administrator` -- `{prefix}perms override ""plugin enabled"" moderator` - -To undo a permission override, see `{prefix}help permissions remove`. - -Example: -- `{prefix}perms remove override reply` -- `{prefix}perms remove override plugin enabled` - -You can retrieve a single or all command level override(s), see`{prefix}help permissions get`.","Change a permission level for a specific command. - -Examples: -- `{prefix}perms override reply administrator` -- `{prefix}perms override ""plugin enabled"" moderator` - -To undo a permission override, see `{prefix}help permissions remove`. - -Example: -- `{prefix}perms remove override reply` -- `{prefix}perms remove override plugin enabled` - -You can retrieve a single or all command level override(s), see`{prefix}help permissions get`.","Cog: Utility -Command: permissions override" -"Add a permission to a command or a permission level.","Add a permission to a command or a permission level.","Cog: Utility -Command: permissions add" -"Add a permission to a command or a permission level. - -For sub commands, wrap the complete command name with quotes. -To find a list of permission levels, see `{prefix}help perms`. - -Examples: -- `{prefix}perms add level REGULAR everyone` -- `{prefix}perms add command reply @user` -- `{prefix}perms add command ""plugin enabled"" @role` -- `{prefix}perms add command help 984301093849028` - -Do not ping `@everyone` for granting permission to everyone, use ""everyone"" or ""all"" instead.","Add a permission to a command or a permission level. - -For sub commands, wrap the complete command name with quotes. -To find a list of permission levels, see `{prefix}help perms`. - -Examples: -- `{prefix}perms add level REGULAR everyone` -- `{prefix}perms add command reply @user` -- `{prefix}perms add command ""plugin enabled"" @role` -- `{prefix}perms add command help 984301093849028` - -Do not ping `@everyone` for granting permission to everyone, use ""everyone"" or ""all"" instead.","Cog: Utility -Command: permissions add" -"Remove permission to use a command, permission level, or command level override.","Remove permission to use a command, permission level, or command level override.","Cog: Utility -Command: permissions remove" -"Remove permission to use a command, permission level, or command level override. - -For sub commands, wrap the complete command name with quotes. -To find a list of permission levels, see `{prefix}help perms`. - -Examples: -- `{prefix}perms remove level REGULAR everyone` -- `{prefix}perms remove command reply @user` -- `{prefix}perms remove command ""plugin enabled"" @role` -- `{prefix}perms remove command help 984301093849028` -- `{prefix}perms remove override block` -- `{prefix}perms remove override ""snippet add""` - -Do not ping `@everyone` for granting permission to everyone, use ""everyone"" or ""all"" instead.","Remove permission to use a command, permission level, or command level override. - -For sub commands, wrap the complete command name with quotes. -To find a list of permission levels, see `{prefix}help perms`. - -Examples: -- `{prefix}perms remove level REGULAR everyone` -- `{prefix}perms remove command reply @user` -- `{prefix}perms remove command ""plugin enabled"" @role` -- `{prefix}perms remove command help 984301093849028` -- `{prefix}perms remove override block` -- `{prefix}perms remove override ""snippet add""` - -Do not ping `@everyone` for granting permission to everyone, use ""everyone"" or ""all"" instead.","Cog: Utility -Command: permissions remove" -"View the currently-set permissions.","View the currently-set permissions.","Cog: Utility -Command: permissions get" -"View the currently-set permissions. - -To find a list of permission levels, see `{prefix}help perms`. - -To view all command and level permissions: - -Examples: -- `{prefix}perms get @user` -- `{prefix}perms get 984301093849028` - -To view all users and roles of a command or level permission: - -Examples: -- `{prefix}perms get command reply` -- `{prefix}perms get command plugin remove` -- `{prefix}perms get level SUPPORTER` - -To view command level overrides: - -Examples: -- `{prefix}perms get override block` -- `{prefix}perms get override permissions add` - -Do not ping `@everyone` for granting permission to everyone, use ""everyone"" or ""all"" instead.","View the currently-set permissions. - -To find a list of permission levels, see `{prefix}help perms`. - -To view all command and level permissions: - -Examples: -- `{prefix}perms get @user` -- `{prefix}perms get 984301093849028` - -To view all users and roles of a command or level permission: - -Examples: -- `{prefix}perms get command reply` -- `{prefix}perms get command plugin remove` -- `{prefix}perms get level SUPPORTER` - -To view command level overrides: - -Examples: -- `{prefix}perms get override block` -- `{prefix}perms get override permissions add` - -Do not ping `@everyone` for granting permission to everyone, use ""everyone"" or ""all"" instead.","Cog: Utility -Command: permissions get" -"Commands relating to logviewer oauth2 login authentication.","Commands relating to logviewer oauth2 login authentication.","Cog: Utility -Command: oauth" -"Commands relating to logviewer oauth2 login authentication. - -This functionality on your logviewer site is a [**Patron**](https://patreon.com/kyber) only feature.","Commands relating to logviewer oauth2 login authentication. - -This functionality on your logviewer site is a [**Patron**](https://patreon.com/kyber) only feature.","Cog: Utility -Command: oauth" -"Whitelist or un-whitelist a user or role to have access to logs.","Whitelist or un-whitelist a user or role to have access to logs.","Cog: Utility -Command: oauth whitelist" -"Whitelist or un-whitelist a user or role to have access to logs. - -`target` may be a role ID, name, mention, user ID, name, or mention.","Whitelist or un-whitelist a user or role to have access to logs. - -`target` may be a role ID, name, mention, user ID, name, or mention.","Cog: Utility -Command: oauth whitelist" -"Shows a list of users and roles that are whitelisted to view logs.","Shows a list of users and roles that are whitelisted to view logs.","Cog: Utility -Command: oauth show" -"Shows a list of users and roles that are whitelisted to view logs.","Shows a list of users and roles that are whitelisted to view logs.","Cog: Utility -Command: oauth show" -"Evaluates Python code.","Evaluates Python code.","Cog: Utility -Command: eval" -"Evaluates Python code.","Evaluates Python code.","Cog: Utility -Command: eval" -"Shows this help message.","Shows this help message.","Cog: Utility -Command: help" -"Shows this help message.","Shows this help message.","Cog: Utility -Command: help" diff --git a/modmail.sh b/modmail.sh new file mode 100644 index 0000000000..4f29170f0a --- /dev/null +++ b/modmail.sh @@ -0,0 +1,3 @@ +#!/bin/sh + +pipenv run python3 bot.py \ No newline at end of file diff --git a/plugins/registry.json b/plugins/registry.json index 1c268d89be..5c343ebcff 100644 --- a/plugins/registry.json +++ b/plugins/registry.json @@ -8,6 +8,15 @@ "icon_url": "https://i.imgur.com/951szZ3.jpg", "thumbnail_url": "https://i.imgur.com/951szZ3.jpg" }, + "music": { + "repository": "lorenzo132/modmail-plugins", + "branch": "master", + "description": "Play your favourite music on your bot! Note: Only works on VPS follow this guide: https://gist.github.com/lorenzo132/5ef328e5dfcfaec19cb81dc7a63eaffa", + "bot_version": "2.20.1", + "title": "Music", + "icon_url": "https://i.imgur.com/R2olclk.png", + "thumbnail_url": "https://i.imgur.com/xuoQjPu.gif" + }, "dragory-migrate": { "repository": "kyb3r/modmail-plugins", "branch": "master", @@ -17,58 +26,22 @@ "icon_url": "https://cdn1.iconfinder.com/data/icons/web-hosting-2-4/52/200-512.png", "thumbnail_url": "https://cdn1.iconfinder.com/data/icons/web-hosting-2-4/52/200-512.png" }, - "autorole": { - "repository": "papiersnipper/modmail-plugins", + "media-only": { + "repository": "lorenzo132/modmail-plugins", "branch": "master", - "description": "Easily auto-assign a role to a user when they join your server.", + "description": "Make a channel mediaonly, only the following mediatypes will be accepted `.png` / `.gif` / `.jpg` / `.mp4`/ `.jpeg`", "bot_version": "2.20.1", - "title": "Autorole Plugin", - "icon_url": "https://i.imgur.com/67bEi82.png", - "thumbnail_url": "https://i.imgur.com/67bEi82.png" + "title": "Media-only", + "icon_url": "https://i.imgur.com/ussAoIi.png", + "thumbnail_url": "https://i.imgur.com/ussAoIi.png" }, "anti-steal-close": { - "repository": "officialpiyush/modmail-plugins", - "branch": "master", - "description": "Don't let anyone steal ya close.", - "title": "Anti Steal Close", - "icon_url": "https://i.imgur.com/LovxyV3.png", - "thumbnail_url": "https://i.imgur.com/LovxyV3.png" - }, - "embedder": { - "repository": "papiersnipper/modmail-plugins", - "branch": "master", - "description": "Easily make embeds for a nicer presence.", - "bot_version": "2.20.1", - "title": "Embedder Plugin", - "icon_url": "https://i.imgur.com/l3uzJwD.png", - "thumbnail_url": "https://i.imgur.com/l3uzJwD.png" - }, - "leveling": { - "repository": "papiersnipper/modmail-plugins", - "branch": "master", - "description": "A leveling system for your server: see who's active and who's not.", - "bot_version": "2.20.1", - "title": "Leveling Plugin", - "icon_url": "https://i.imgur.com/VuZ60QX.png", - "thumbnail_url": "https://i.imgur.com/VuZ60QX.png" - }, - "purger": { - "repository": "papiersnipper/modmail-plugins", + "repository": "officialpiyush/modmail-plugins", "branch": "master", - "description": "Delete multiple messages at a time.", - "bot_version": "2.20.1", - "title": "Purger Plugin", - "icon_url": "https://i.imgur.com/HnC42jM.png", - "thumbnail_url": "https://i.imgur.com/HnC42jM.png" - }, - "supporters": { - "repository": "papiersnipper/modmail-plugins", - "branch": "master", - "description": "Let your users know who is part of the support team.", - "bot_version": "2.20.1", - "title": "Supporters Plugin", - "icon_url": "https://i.imgur.com/1QXRutA.png", - "thumbnail_url": "https://i.imgur.com/1QXRutA.png" + "description": "Don't let anyone steal ya close.", + "title": "Anti Steal Close", + "icon_url": "https://i.imgur.com/LovxyV3.png", + "thumbnail_url": "https://i.imgur.com/LovxyV3.png" }, "announcement": { "repository": "officialpiyush/modmail-plugins", @@ -115,15 +88,6 @@ "icon_url": "https://images.ionadev.ml/b/ZIDUUsl.png", "thumbnail_url": "https://images.ionadev.ml/b/ZIDUUsl.png" }, - "translator": { - "repository": "officialpiyush/modmail-plugins", - "branch": "master", - "description": "You can auto translate thread msgs using this plugin or translate text fromm any language to English!", - "bot_version": "2.20.1", - "title": "Translator Plugin", - "icon_url": "https://images.ionadev.ml/b/ZIDUUsl.png", - "thumbnail_url": "https://images.ionadev.ml/b/ZIDUUsl.png" - }, "welcomer": { "repository": "fourjr/modmail-plugins", "branch": "master", @@ -150,7 +114,7 @@ "title": "Backup Database (backupdb)", "icon_url": "https://images.ionadev.ml/b/nKAlOC4.jpg", "thumbnail_url": "https://images.ionadev.ml/b/nKAlOC4.jpg" - }, + }, "colors": { "repository": "Taaku18/modmail-plugins", "branch": "master", @@ -160,7 +124,7 @@ "icon_url": "https://cdn1.iconfinder.com/data/icons/weather-19/32/rainbow-512.png", "thumbnail_url": "https://i.imgur.com/fSxnc9W.jpg" }, - "fun": { + "fun": { "repository": "TheKinG2149/modmail-plugins", "branch": "master", "description": "Some fun commands like 8ball, dadjokes", @@ -168,9 +132,9 @@ "title": "Fun", "icon_url": "https://cdn.discordapp.com/attachments/584692239893135362/591588754142265354/43880032.png", "thumbnail_url": "https://cdn.discordapp.com/attachments/584692239893135362/591588754142265354/43880032.png" - }, + }, "stats": { - "repository": "MiTonder/modmail-plugins", + "repository": "KarateWumpus/modmail-plugins", "branch": "master", "description": "Get useful stats directly in an embed about either the Modmail bot, a user or the server.", "bot_version": "2.24.1", @@ -190,10 +154,55 @@ "serverstats": { "repository": "dazvise/modmail-plugins", "branch": "master", - "description": "Interesting and accurate statistics about your server.", + "description": "Voice channels containing interesting and accurate statistics about your server such as Member Count.", "bot_version": "2.20.1", "title": "Server Stats", "icon_url": "https://i.gyazo.com/fadb70740e83f2448b23ffe192a1f32d.png", "thumbnail_url": "https://i.gyazo.com/fadb70740e83f2448b23ffe192a1f32d.png" + }, + "suggest": { + "repository": "realcyguy/modmail-plugins", + "branch": "master", + "description": "Send suggestions to a selected server! It even has moderation...", + "bot_version": "3.4.1", + "title": "Suggest stuff.", + "icon_url": "https://i.imgur.com/qtE7AH8.png", + "thumbnail_url": "https://i.imgur.com/qtE7AH8.png" + }, + "githubstats": { + "repository": "mischievousdev/modmail-plugins", + "branch": "master", + "description": "Github statistics in discord", + "bot_version": "2.20.1", + "title": "Github Stats", + "icon_url": "https://raw.githubusercontent.com/mischievousdev/modmail-plugins/master/download%20(9).jpeg", + "thumbnail_url": "https://raw.githubusercontent.com/mischievousdev/modmail-plugins/master/download%20(9).jpeg" + }, + "slowmode": { + "repository": "teen1/modmail-plugins", + "branch": "master", + "description": "Configure slow mode for your channels with Modmail!", + "bot_version": "2.20.1", + "title": "Slow Mode", + "icon_url": "https://cdn.discordapp.com/attachments/717029057635549274/717033838966210601/Slow_mode_-_icon.png", + "thumbnail_url": "https://cdn.discordapp.com/attachments/717029057635549274/717029110907666482/Slow_mode_plugin_-_thumbnail.png" + }, + "publish": { + "repository": "codeinteger6/modmail-plugins", + "branch": "master", + "description": "Publish messages sent in announcement channels.", + "bot_version": "3.5.0", + "title": "Publish", + "icon_url": "https://user-images.githubusercontent.com/44692189/89184422-96de3600-d5ba-11ea-98ea-d096aa385ad5.png", + "thumbnail_url": "https://user-images.githubusercontent.com/44692189/89184422-96de3600-d5ba-11ea-98ea-d096aa385ad5.png" + }, + "translate": { + "repository": "WebKide/modmail-plugins", + "branch": "master", + "description": "(βˆ©ο½€-Β΄)βŠƒβ”β˜†οΎŸ.*ο½₯q゚ translate text from one language to another (defaults to English)\n\nGet full list of available languages at: https://github.com/WebKide/modmail-plugins/blob/master/translate/langs.json\n\nThis command conflicts with Translator-plugin", + "bot_version": "3.5.0", + "title": "Translate", + "icon_url": "https://i.imgur.com/yeHFKgl.png", + "thumbnail_url": "https://i.imgur.com/yeHFKgl.png" } -} +} \ No newline at end of file diff --git a/poetry.lock b/poetry.lock index 1841d893a1..4aa8248546 100644 --- a/poetry.lock +++ b/poetry.lock @@ -4,13 +4,13 @@ description = "Async http client/server framework (asyncio)" name = "aiohttp" optional = false python-versions = ">=3.5.3" -version = "3.5.4" +version = "3.6.2" [package.dependencies] async-timeout = ">=3.0,<4.0" attrs = ">=17.3.0" chardet = ">=2.0,<4.0" -multidict = ">=4.0,<5.0" +multidict = ">=4.5,<5.0" yarl = ">=1.0,<2.0" [[package]] @@ -19,20 +19,20 @@ description = "A small Python module for determining appropriate platform-specif name = "appdirs" optional = false python-versions = "*" -version = "1.4.3" +version = "1.4.4" [[package]] category = "dev" description = "An abstract syntax tree for Python with inference support." name = "astroid" optional = false -python-versions = ">=3.5.*" -version = "2.3.3" +python-versions = ">=3.5" +version = "2.4.1" [package.dependencies] lazy-object-proxy = ">=1.4.0,<1.5.0" six = ">=1.12,<2.0" -wrapt = ">=1.11.0,<1.12.0" +wrapt = ">=1.11,<2.0" [package.dependencies.typed-ast] python = "<3.8" @@ -75,13 +75,16 @@ description = "The uncompromising code formatter." name = "black" optional = false python-versions = ">=3.6" -version = "19.3b0" +version = "19.10b0" [package.dependencies] appdirs = "*" attrs = ">=18.1.0" click = ">=6.5" +pathspec = ">=0.6,<1" +regex = "*" toml = ">=0.9.4" +typed-ast = ">=1.4.0" [[package]] category = "main" @@ -96,8 +99,8 @@ category = "dev" description = "Composable command line interface toolkit" name = "click" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" -version = "7.0" +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +version = "7.1.2" [[package]] category = "main" @@ -109,15 +112,15 @@ version = "0.4.3" [[package]] category = "main" -description = "A python wrapper for the Discord API" +description = "A Python wrapper for the Discord API" name = "discord.py" optional = false python-versions = ">=3.5.3" -version = "1.2.5" +version = "1.3.3" [package.dependencies] -aiohttp = ">=3.3.0,<3.6.0" -websockets = ">=6.0,<7.0" +aiohttp = ">=3.6.0,<3.7.0" +websockets = ">=6.0,<7.0 || >7.0,<8.0 || >8.0,<8.0.1 || >8.0.1,<9.0" [[package]] category = "main" @@ -146,24 +149,24 @@ version = "3.1.1" [[package]] category = "dev" description = "Git Object Database" -name = "gitdb2" +name = "gitdb" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" -version = "2.0.6" +python-versions = ">=3.4" +version = "4.0.5" [package.dependencies] -smmap2 = ">=2.0.0" +smmap = ">=3.0.1,<4" [[package]] category = "dev" description = "Python Git Library" name = "gitpython" optional = false -python-versions = ">=3.0, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" -version = "3.0.5" +python-versions = ">=3.4" +version = "3.1.3" [package.dependencies] -gitdb2 = ">=2.0.0" +gitdb = ">=4.0.1,<5" [[package]] category = "main" @@ -171,7 +174,7 @@ description = "Internationalized Domain Names in Applications (IDNA)" name = "idna" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" -version = "2.8" +version = "2.9" [[package]] category = "main" @@ -226,7 +229,7 @@ description = "multidict implementation" name = "multidict" optional = false python-versions = ">=3.5" -version = "4.7.1" +version = "4.7.6" [[package]] category = "main" @@ -245,7 +248,15 @@ description = "Parse human-readable date/time text." name = "parsedatetime" optional = false python-versions = "*" -version = "2.5" +version = "2.6" + +[[package]] +category = "dev" +description = "Utility library for gitignore style pattern matching of file paths." +name = "pathspec" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +version = "0.8.0" [[package]] category = "dev" @@ -253,7 +264,7 @@ description = "Python Build Reasonableness" name = "pbr" optional = false python-versions = "*" -version = "5.4.4" +version = "5.4.5" [[package]] category = "dev" @@ -261,13 +272,14 @@ description = "python code static checker" name = "pylint" optional = false python-versions = ">=3.5.*" -version = "2.4.4" +version = "2.5.2" [package.dependencies] -astroid = ">=2.3.0,<2.4" +astroid = ">=2.4.0,<=2.5" colorama = "*" isort = ">=4.2.5,<5" mccabe = ">=0.6,<0.7" +toml = ">=0.7.1" [[package]] category = "main" @@ -275,7 +287,7 @@ description = "Python driver for MongoDB " name = "pymongo" optional = true python-versions = "*" -version = "3.10.0" +version = "3.10.1" [[package]] category = "main" @@ -294,43 +306,50 @@ description = "Add .env support to your django/flask apps in development and dep name = "python-dotenv" optional = false python-versions = "*" -version = "0.10.3" +version = "0.10.5" [[package]] category = "dev" description = "YAML parser and emitter for Python" name = "pyyaml" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" -version = "5.2" +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +version = "5.3.1" + +[[package]] +category = "dev" +description = "Alternative regular expression module, to replace re." +name = "regex" +optional = false +python-versions = "*" +version = "2020.6.7" [[package]] category = "main" description = "Python 2 and 3 compatibility utilities" name = "six" optional = false -python-versions = ">=2.6, !=3.0.*, !=3.1.*" -version = "1.13.0" +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" +version = "1.15.0" [[package]] category = "dev" description = "A pure Python implementation of a sliding window memory map manager" -name = "smmap2" +name = "smmap" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" -version = "2.0.5" +version = "3.0.4" [[package]] category = "dev" description = "Manage dynamic plugins for Python applications" name = "stevedore" optional = false -python-versions = "*" -version = "1.31.0" +python-versions = ">=3.6" +version = "2.0.0" [package.dependencies] pbr = ">=2.0.0,<2.1.0 || >2.1.0" -six = ">=1.10.0" [[package]] category = "dev" @@ -338,16 +357,15 @@ description = "Python Library for Tom's Obvious, Minimal Language" name = "toml" optional = false python-versions = "*" -version = "0.10.0" +version = "0.10.1" [[package]] category = "dev" description = "a fork of Python 2 and 3 ast modules with type comment support" -marker = "implementation_name == \"cpython\" and python_version < \"3.8\"" name = "typed-ast" optional = false python-versions = "*" -version = "1.4.0" +version = "1.4.1" [[package]] category = "main" @@ -362,8 +380,8 @@ category = "main" description = "An implementation of the WebSocket Protocol (RFC 6455 & 7692)" name = "websockets" optional = false -python-versions = ">=3.4" -version = "6.0" +python-versions = ">=3.6.1" +version = "8.1" [[package]] category = "dev" @@ -371,7 +389,7 @@ description = "Module for decorators, wrappers and monkey patching." name = "wrapt" optional = false python-versions = "*" -version = "1.11.2" +version = "1.12.1" [[package]] category = "main" @@ -385,48 +403,53 @@ version = "1.4.2" idna = ">=2.0" multidict = ">=4.0" +[extras] +mongodb = ["motor"] + [metadata] -content-hash = "fbe9e329f33e482854cff5bf05b006de9830c2d46bf3874e2ee4f8a8da0b1797" +content-hash = "68d5c15e62c4bf5f65fd3b0a9a2586b15557f724c3de5b756534bacd52cbfe40" python-versions = "^3.7" [metadata.hashes] -aiohttp = ["00d198585474299c9c3b4f1d5de1a576cc230d562abc5e4a0e81d71a20a6ca55", "0155af66de8c21b8dba4992aaeeabf55503caefae00067a3b1139f86d0ec50ed", "09654a9eca62d1bd6d64aa44db2498f60a5c1e0ac4750953fdd79d5c88955e10", "199f1d106e2b44b6dacdf6f9245493c7d716b01d0b7fbe1959318ba4dc64d1f5", "296f30dedc9f4b9e7a301e5cc963012264112d78a1d3094cd83ef148fdf33ca1", "368ed312550bd663ce84dc4b032a962fcb3c7cae099dbbd48663afc305e3b939", "40d7ea570b88db017c51392349cf99b7aefaaddd19d2c78368aeb0bddde9d390", "629102a193162e37102c50713e2e31dc9a2fe7ac5e481da83e5bb3c0cee700aa", "6d5ec9b8948c3d957e75ea14d41e9330e1ac3fed24ec53766c780f82805140dc", "87331d1d6810214085a50749160196391a712a13336cd02ce1c3ea3d05bcf8d5", "9a02a04bbe581c8605ac423ba3a74999ec9d8bce7ae37977a3d38680f5780b6d", "9c4c83f4fa1938377da32bc2d59379025ceeee8e24b89f72fcbccd8ca22dc9bf", "9cddaff94c0135ee627213ac6ca6d05724bfe6e7a356e5e09ec57bd3249510f6", "a25237abf327530d9561ef751eef9511ab56fd9431023ca6f4803f1994104d72", "a5cbd7157b0e383738b8e29d6e556fde8726823dae0e348952a61742b21aeb12", "a97a516e02b726e089cffcde2eea0d3258450389bbac48cbe89e0f0b6e7b0366", "acc89b29b5f4e2332d65cd1b7d10c609a75b88ef8925d487a611ca788432dfa4", "b05bd85cc99b06740aad3629c2585bda7b83bd86e080b44ba47faf905fdf1300", "c2bec436a2b5dafe5eaeb297c03711074d46b6eb236d002c13c42f25c4a8ce9d", "cc619d974c8c11fe84527e4b5e1c07238799a8c29ea1c1285149170524ba9303", "d4392defd4648badaa42b3e101080ae3313e8f4787cb517efd3f5b8157eaefd6", "e1c3c582ee11af7f63a34a46f0448fca58e59889396ffdae1f482085061a2889"] -appdirs = ["9e5896d1372858f8dd3344faf4e5014d21849c756c8d5701f78f8a103b372d92", "d8b24664561d0d34ddfaec54636d502d7cea6e29c3eaf68f3df6180863e2166e"] -astroid = ["71ea07f44df9568a75d0f354c49143a4575d90645e9fead6dfb52c26a85ed13a", "840947ebfa8b58f318d42301cf8c0a20fd794a33b61cc4638e28e9e61ba32f42"] +aiohttp = ["1e984191d1ec186881ffaed4581092ba04f7c61582a177b187d3a2f07ed9719e", "259ab809ff0727d0e834ac5e8a283dc5e3e0ecc30c4d80b3cd17a4139ce1f326", "2f4d1a4fdce595c947162333353d4a44952a724fba9ca3205a3df99a33d1307a", "32e5f3b7e511aa850829fbe5aa32eb455e5534eaa4b1ce93231d00e2f76e5654", "344c780466b73095a72c616fac5ea9c4665add7fc129f285fbdbca3cccf4612a", "460bd4237d2dbecc3b5ed57e122992f60188afe46e7319116da5eb8a9dfedba4", "4c6efd824d44ae697814a2a85604d8e992b875462c6655da161ff18fd4f29f17", "50aaad128e6ac62e7bf7bd1f0c0a24bc968a0c0590a726d5a955af193544bcec", "6206a135d072f88da3e71cc501c59d5abffa9d0bb43269a6dcd28d66bfafdbdd", "65f31b622af739a802ca6fd1a3076fd0ae523f8485c52924a89561ba10c49b48", "ae55bac364c405caa23a4f2d6cfecc6a0daada500274ffca4a9230e7129eac59", "b778ce0c909a2653741cb4b1ac7015b5c130ab9c897611df43ae6a58523cb965"] +appdirs = ["7d5d0167b2b1ba821647616af46a749d1c653740dd0d2415100fe26e27afdf41", "a841dacd6b99318a741b166adb07e19ee71a274450e68237b4650ca1055ab128"] +astroid = ["4c17cea3e592c21b6e222f673868961bad77e1f985cb1694ed077475a89229c1", "d8506842a3faf734b81599c8b98dcc423de863adcc1999248480b18bd31a0f38"] async-timeout = ["0c3c816a028d47f659d6ff5c745cb2acf1f966da1fe5c19c77a70282b25f4c5f", "4291ca197d287d274d0b6cb5d6f8f8f82d434ed288f962539ff18cc9012f9ea3"] attrs = ["08a96c641c3a74e44eb59afb61a24f2cb9f4d7188748e76ba4bb5edfa3cb7d1c", "f7b7ce16570fe9965acd6d30101a28f62fb4a7f9e926b3bbc9b61f8b04247e72"] bandit = ["336620e220cf2d3115877685e264477ff9d9abaeb0afe3dc7264f55fa17a3952", "41e75315853507aa145d62a78a2a6c5e3240fe14ee7c601459d0df9418196065"] -black = ["09a9dcb7c46ed496a9850b76e4e825d6049ecd38b611f1224857a79bd985a8cf", "68950ffd4d9169716bcb8719a56c07a2f4485354fec061cdd5910aa07369731c"] +black = ["1b30e59be925fafc1ee4565e5e08abef6b03fe455102883820fe5ee2e4734e0b", "c2edb73a08e9e0e6f65a0e6af18b059b8b1cdd5bef997d7a0b181df93dc81539"] chardet = ["84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae", "fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691"] -click = ["2335065e6395b9e67ca716de5f7526736bfa6ceead690adf616d925bdc622b13", "5b94b49521f6456670fdb30cd82a4eca9412788a93fa6dd6df72c94d5a8ff2d7"] +click = ["d2b5255c7c6349bc1bd1e59e08cd12acbbd63ce649f2588755783aa94dfb6b1a", "dacca89f4bfadd5de3d7489b7c8a566eee0d3676333fbb50030263894c38c0dc"] colorama = ["7d73d2a99753107a36ac6b455ee49046802e59d9d076ef8e47b61499fa29afff", "e96da0d330793e2cb9485e9ddfd918d456036c7149416295932478192f4436a1"] -"discord.py" = ["7c843b523bb011062b453864e75c7b675a03faf573c58d14c9f096e85984329d"] +"discord.py" = ["406871b06d86c3dc49fba63238519f28628dac946fef8a0e22988ff58ec05580", "ad00e34c72d2faa8db2157b651d05f3c415d7d05078e7e41dc9e8dc240051beb"] dnspython = ["36c5e8e38d4369a08b6780b7f27d790a292b2b08eea01607865bf0936c558e01", "f69c21288a962f4da86e56c4905b49d11aba7938d3d740e80d9e366ee4f1632d"] emoji = ["60652d3a2dcee5b8af8acb097c31776fb6d808027aeb7221830f72cdafefc174"] futures = ["3a44f286998ae64f0cc083682fcfec16c406134a81a589a5de445d7bb7c2751b", "51ecb45f0add83c806c68e4b06106f90db260585b25ef2abfcda0bd95c0132fd", "c4884a65654a7c45435063e14ae85280eb1f111d94e542396717ba9828c4337f"] -gitdb2 = ["1b6df1433567a51a4a9c1a5a0de977aa351a405cc56d7d35f3388bad1f630350", "96bbb507d765a7f51eb802554a9cfe194a174582f772e0d89f4e87288c288b7b"] -gitpython = ["9c2398ffc3dcb3c40b27324b316f08a4f93ad646d5a6328cafbb871aa79f5e42", "c155c6a2653593ccb300462f6ef533583a913e17857cfef8fc617c246b6dc245"] -idna = ["c357b3f628cf53ae2c4c05627ecc484553142ca23264e593d327bcde5e9c3407", "ea8b7f6188e6fa117537c3df7da9fc686d485087abf6ac197f9c46432f7e4a3c"] +gitdb = ["91f36bfb1ab7949b3b40e23736db18231bf7593edada2ba5c3a174a7b23657ac", "c9e1f2d0db7ddb9a704c2a0217be31214e91a4fe1dea1efad19ae42ba0c285c9"] +gitpython = ["e107af4d873daed64648b4f4beb89f89f0cfbe3ef558fc7821ed2331c2f8da1a", "ef1d60b01b5ce0040ad3ec20bc64f783362d41fa0822a2742d3586e1f49bb8ac"] +idna = ["7588d1c14ae4c77d74036e8c22ff447b26d0fde8f007354fd48a7814db15b7cb", "a068a21ceac8a4d63dbfd964670474107f541babbd2250d61922f029858365fa"] isodate = ["2e364a3d5759479cdb2d37cce6b9376ea504db2ff90252a2e5b7cc89cc9ff2d8", "aa4d33c06640f5352aca96e4b81afd8ab3b47337cc12089822d6f322ac772c81"] isort = ["54da7e92468955c4fceacd0c86bd0ec997b0e1ee80d97f67c35a78b719dccab1", "6e811fcb295968434526407adb8796944f1988c5b65e8139058f2014cbe100fd"] lazy-object-proxy = ["0c4b206227a8097f05c4dbdd323c50edf81f15db3b8dc064d08c62d37e1a504d", "194d092e6f246b906e8f70884e620e459fc54db3259e60cf69a4d66c3fda3449", "1be7e4c9f96948003609aa6c974ae59830a6baecc5376c25c92d7d697e684c08", "4677f594e474c91da97f489fea5b7daa17b5517190899cf213697e48d3902f5a", "48dab84ebd4831077b150572aec802f303117c8cc5c871e182447281ebf3ac50", "5541cada25cd173702dbd99f8e22434105456314462326f06dba3e180f203dfd", "59f79fef100b09564bc2df42ea2d8d21a64fdcda64979c0fa3db7bdaabaf6239", "8d859b89baf8ef7f8bc6b00aa20316483d67f0b1cbf422f5b4dc56701c8f2ffb", "9254f4358b9b541e3441b007a0ea0764b9d056afdeafc1a5569eee1cc6c1b9ea", "9651375199045a358eb6741df3e02a651e0330be090b3bc79f6d0de31a80ec3e", "97bb5884f6f1cdce0099f86b907aa41c970c3c672ac8b9c8352789e103cf3156", "9b15f3f4c0f35727d3a0fba4b770b3c4ebbb1fa907dbcc046a1d2799f3edd142", "a2238e9d1bb71a56cd710611a1614d1194dc10a175c1e08d75e1a7bcc250d442", "a6ae12d08c0bf9909ce12385803a543bfe99b95fe01e752536a60af2b7797c62", "ca0a928a3ddbc5725be2dd1cf895ec0a254798915fb3a36af0964a0a4149e3db", "cb2c7c57005a6804ab66f106ceb8482da55f5314b7fcb06551db1edae4ad1531", "d74bb8693bf9cf75ac3b47a54d716bbb1a92648d5f781fc799347cfc95952383", "d945239a5639b3ff35b70a88c5f2f491913eb94871780ebfabb2568bd58afc5a", "eba7011090323c1dadf18b3b689845fd96a61ba0a1dfbd7f24b921398affc357", "efa1909120ce98bbb3777e8b6f92237f5d5c8ea6758efea36a473e1d38f7d3e4", "f3900e8a5de27447acbf900b4750b0ddfd7ec1ea7fbaf11dfa911141bc522af0"] mccabe = ["ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42", "dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f"] motor = ["599719bc6dcddc3b9ea4e09659fb0073d5fadcc24735999b2902f48cef33f909", "756c587985d166166e644ccd36fb8b586fb987eb42fc0fc60cce9a3d76d809b4", "97b4fc0a00a84df30f866d18693c503eef46c7642f75218a2c44d74d835be38a"] -multidict = ["09c19f642e055550c9319d5123221b7e07fc79bda58122aa93910e52f2ab2f29", "0c1a5d5f7aa7189f7b83c4411c2af8f1d38d69c4360d5de3eea129c65d8d7ce2", "12f22980e7ed0972a969520fb1e55682c9fca89a68b21b49ec43132e680be812", "258660e9d6b52de1a75097944e12718d3aa59adc611b703361e3577d69167aaf", "3374a23e707848f27b3438500db0c69eca82929337656fce556bd70031fbda74", "503b7fce0054c73aa631cc910a470052df33d599f3401f3b77e54d31182525d5", "6ce55f2c45ffc90239aab625bb1b4864eef33f73ea88487ef968291fbf09fb3f", "725496dde5730f4ad0a627e1a58e2620c1bde0ad1c8080aae15d583eb23344ce", "a3721078beff247d0cd4fb19d915c2c25f90907cf8d6cd49d0413a24915577c6", "ba566518550f81daca649eded8b5c7dd09210a854637c82351410aa15c49324a", "c42362750a51a15dc905cb891658f822ee5021bfbea898c03aa1ed833e2248a5", "cf14aaf2ab067ca10bca0b14d5cbd751dd249e65d371734bc0e47ddd8fafc175", "cf24e15986762f0e75a622eb19cfe39a042e952b8afba3e7408835b9af2be4fb", "d7b6da08538302c5245cd3103f333655ba7f274915f1f5121c4f4b5fbdb3febe", "e27e13b9ff0a914a6b8fb7e4947d4ac6be8e4f61ede17edffabd088817df9e26", "e53b205f8afd76fc6c942ef39e8ee7c519c775d336291d32874082a87802c67c", "ec804fc5f68695d91c24d716020278fcffd50890492690a7e1fef2e741f7172c"] +multidict = ["1ece5a3369835c20ed57adadc663400b5525904e53bae59ec854a5d36b39b21a", "275ca32383bc5d1894b6975bb4ca6a7ff16ab76fa622967625baeebcf8079000", "3750f2205b800aac4bb03b5ae48025a64e474d2c6cc79547988ba1d4122a09e2", "4538273208e7294b2659b1602490f4ed3ab1c8cf9dbdd817e0e9db8e64be2507", "5141c13374e6b25fe6bf092052ab55c0c03d21bd66c94a0e3ae371d3e4d865a5", "51a4d210404ac61d32dada00a50ea7ba412e6ea945bbe992e4d7a595276d2ec7", "5cf311a0f5ef80fe73e4f4c0f0998ec08f954a6ec72b746f3c179e37de1d210d", "6513728873f4326999429a8b00fc7ceddb2509b01d5fd3f3be7881a257b8d463", "7388d2ef3c55a8ba80da62ecfafa06a1c097c18032a501ffd4cabbc52d7f2b19", "9456e90649005ad40558f4cf51dbb842e32807df75146c6d940b6f5abb4a78f3", "c026fe9a05130e44157b98fea3ab12969e5b60691a276150db9eda71710cd10b", "d14842362ed4cf63751648e7672f7174c9818459d169231d03c56e84daf90b7c", "e0d072ae0f2a179c375f67e3da300b47e1a83293c554450b29c900e50afaae87", "f07acae137b71af3bb548bd8da720956a3bc9f9a0b87733e0899226a2317aeb7", "fbb77a75e529021e7c4a8d4e823d88ef4d23674a202be4f5addffc72cbb91430", "fcfbb44c59af3f8ea984de67ec7c306f618a3ec771c2843804069917a8f2e255", "feed85993dbdb1dbc29102f50bca65bdc68f2c0c8d352468c25b54874f23c39d"] natural = ["18c83662d2d33fd7e6eee4e3b0d7366e1ce86225664e3127a2aaf0a3233f7df2"] -parsedatetime = ["3b835fc54e472c17ef447be37458b400e3fefdf14bb1ffdedb5d2c853acf4ba1", "d2e9ddb1e463de871d32088a3f3cea3dc8282b1b2800e081bd0ef86900451667"] -pbr = ["139d2625547dbfa5fb0b81daebb39601c478c21956dc57e2e07b74450a8c506b", "61aa52a0f18b71c5cc58232d2cf8f8d09cd67fcad60b742a60124cb8d6951488"] -pylint = ["3db5468ad013380e987410a8d6956226963aed94ecb5f9d3a28acca6d9ac36cd", "886e6afc935ea2590b462664b161ca9a5e40168ea99e5300935f6591ad467df4"] -pymongo = ["0369136c6e79c5edc16aa5de2b48a1b1c1fe5e6f7fc5915a2deaa98bd6e9dad5", "08364e1bea1507c516b18b826ec790cb90433aec2f235033ec5eecfd1011633b", "0af1d2bc8cc9503bf92ec3669a77ec3a6d7938193b583fb867b7e9696eed52e8", "0cfd1aeeb8c0a634646ab3ebeb4ce6828b94b2e33553a69ff7e6c07c250bf201", "15bbd2b5397f7d22498e2f2769fd698a8a247b9cc1a630ee8dabf647fb333480", "1b4a13dff15641e58620524db15d7a323d60572b2b187261c5cb58c36d74778d", "22fbdb908257f9aaaa372a7684f3e094a05ca52eb84f8f381c8b1827c49556fd", "264272fd1c95fc48002ad85d5e41270831777b4180f2500943e45e12b2a3ab43", "3372e98eebbfd05ebf020388003f8a4438bed41e0fef1ef696d2c13633c416c8", "339d24ecdc42745d2dc09b26fda8151988e806ca81134a7bd10513c4031d91e1", "38281855fc3961ba5510fbb503b8d16cc1fcb326e9f7ba0dd096ed4eb72a7084", "4acdd2e16392472bfd49ca49038845c95e5254b5af862b55f7f2cc79aa258886", "4e0c006bc6e98e861b678432e05bf64ba3eb889b6ab7e7bf1ebaecf9f1ba0e58", "4e4284bcbe4b7be1b37f9641509085b715c478e7fbf8f820358362b5dd359379", "4e5e94a5f9823f0bd0c56012a57650bc6772636c29d83d253260c26b908fcfd9", "4e61f30800a40f1770b2ec56bbf5dc0f0e3f7e9250eb05fa4feb9ccb7bbe39ca", "53577cf57ba9d93b58ab41d45250277828ff83c5286dde14f855e4b17ec19976", "681cb31e8631882804a6cc3c8cc8f54a74ff3a82261a78e50f20c5eec05ac855", "6dfc2710f43dd1d66991a0f160d196356732ccc8aa9dbc6875aeba78388fa142", "72218201b13d8169be5736417987e9a0a3b10d4349e40e4db7a6a5ac670c7ef2", "7247fbcdbf7ab574eb70743461b3cfc14d9cfae3f27a9afb6ce14d87f67dd0b5", "72651f4b4adf50201891580506c8cca465d94d38f26ed92abfc56440662c723c", "87b3aaf12ad6a9b5570b12d2a4b8802757cb3588a903aafd3c25f07f9caf07e3", "87c28b7b37617c5a01eb396487f7d3b61a453e1fa0475a175ab87712d6f5d52f", "88efe627b628f36ef53f09abb218d4630f83d8ebde7028689439559475c43dae", "89bfbca22266f12df7fb80092b7c876734751d02b93789580b68957ad4a8bf56", "908a3caf348a672b28b8a06fe7b4a27c2fdcf7f873df671e4027d48bcd7f971f", "9128e7bea85f3a3041306fa14a7aa82a24b47881918500e1b8396dd1c933b5a6", "9737d6d688a15b8d5c0bfa909638b79261e195be817b9f1be79c722bbb23cd76", "98a8305da158f46e99e7e51db49a2f8b5fcdd7683ea7083988ccb9c4450507a6", "99285cd44c756f0900cbdb5fe75f567c0a76a273b7e0467f23cb76f47e60aac0", "9ed568f8026ffeb00ce31e5351e0d09d704cc19a29549ba4da0ac145d2a26fdf", "a006162035032021dfd00a879643dc06863dac275f9210d843278566c719eebc", "a03cb336bc8d25a11ff33b94967478a9775b0d2b23b39e952d9cc6cb93b75d69", "a863ceb67be163060d1099b7e89b6dd83d6dd50077c7ceae31ac844c4c2baff9", "b82628eaf0a16c1f50e1c205fd1dd406d7874037dd84643da89e91b5043b5e82", "bc6446a41fb7eeaf2c808bab961b9bac81db0f5de69eab74eebe1b8b072399f7", "c42d290ed54096355838421cf9d2a56e150cb533304d2439ef1adf612a986eaf", "c43879fe427ea6aa6e84dae9fbdc5aa14428a4cfe613fe0fee2cc004bf3f307c", "c566cbdd1863ba3ccf838656a1403c3c81fdb57cbe3fdd3515be7c9616763d33", "c5b7a0d7e6ca986de32b269b6dbbd5162c1a776ece72936f55decb4d1b197ee9", "ca109fe9f74da4930590bb589eb8fdf80e5d19f5cd9f337815cac9309bbd0a76", "d0260ba68f9bafd8775b2988b5aeace6e69a37593ec256e23e150c808160c05c", "d12d86e771fc3072a0e6bdbf4e417c63fec85ee47cb052ba7ad239403bf5e154", "d2ce33501149b373118fcfec88a292a87ef0b333fb30c7c6aac72fe64700bdf6", "d582ea8496e2a0e124e927a67dca55c8833f0dbfbc2c84aaf0e5949a2dd30c51", "d68b9ab0a900582a345fb279675b0ad4fac07d6a8c2678f12910d55083b7240d", "dbf1fa571db6006907aeaf6473580aaa76041f4f3cd1ff8a0039fd0f40b83f6d", "e032437a7d2b89dab880c79379d88059cee8019da0ff475d924c4ccab52db88f", "e0f5798f3ad60695465a093e3d002f609c41fef3dcb97fcefae355d24d3274cf", "e756355704a2cf91a7f4a649aa0bbf3bbd263018b9ed08f60198c262f4ee24b6", "e824b4b87bd88cbeb25c8babeadbbaaaf06f02bbb95a93462b7c6193a064974e", "ea1171470b52487152ed8bf27713cc2480dc8b0cd58e282a1bff742541efbfb8", "fa19aef44d5ed8f798a8136ff981aedfa508edac3b1bed481eca5dde5f14fd3d", "faf83d20c041637cb277e5fdb59abc217c40ab3202dd87cc95d6fbd9ce5ffd9b", "fceb6ae5a149a42766efb8344b0df6cfb21b55c55f360170abaddb11d43af0f1"] +parsedatetime = ["4cb368fbb18a0b7231f4d76119165451c8d2e35951455dfee97c62a87b04d455", "cb96edd7016872f58479e35879294258c71437195760746faffedb692aef000b"] +pathspec = ["7d91249d21749788d07a2d0f94147accd8f845507400749ea19c1ec9054a12b0", "da45173eb3a6f2a5a487efba21f050af2b41948be6ab52b6a1e3ff22bb8b7061"] +pbr = ["07f558fece33b05caf857474a366dfcc00562bca13dd8b47b2b3e22d9f9bf55c", "579170e23f8e0c2f24b0de612f71f648eccb79fb1322c814ae6b3c07b5ba23e8"] +pylint = ["b95e31850f3af163c2283ed40432f053acbc8fc6eba6a069cb518d9dbf71848c", "dd506acce0427e9e08fb87274bcaa953d38b50a58207170dbf5b36cf3e16957b"] +pymongo = ["01b4e10027aef5bb9ecefbc26f5df3368ce34aef81df43850f701e716e3fe16d", "0fc5aa1b1acf7f61af46fe0414e6a4d0c234b339db4c03a63da48599acf1cbfc", "1396eb7151e0558b1f817e4b9d7697d5599e5c40d839a9f7270bd90af994ad82", "18e84a3ec5e73adcb4187b8e5541b2ad61d716026ed9863267e650300d8bea33", "19adf2848b80cb349b9891cc854581bbf24c338be9a3260e73159bdeb2264464", "20ee0475aa2ba437b0a14806f125d696f90a8433d820fb558fdd6f052acde103", "26798795097bdeb571f13942beef7e0b60125397811c75b7aa9214d89880dd1d", "26e707a4eb851ec27bb969b5f1413b9b2eac28fe34271fa72329100317ea7c73", "2a3c7ad01553b27ec553688a1e6445e7f40355fb37d925c11fcb50b504e367f8", "2f07b27dbf303ea53f4147a7922ce91a26b34a0011131471d8aaf73151fdee9a", "316f0cf543013d0c085e15a2c8abe0db70f93c9722c0f99b6f3318ff69477d70", "31d11a600eea0c60de22c8bdcb58cda63c762891facdcb74248c36713240987f", "334ef3ffd0df87ea83a0054454336159f8ad9c1b389e19c0032d9cb8410660e6", "358ba4693c01022d507b96a980ded855a32dbdccc3c9331d0667be5e967f30ed", "3a6568bc53103df260f5c7d2da36dffc5202b9a36c85540bba1836a774943794", "444bf2f44264578c4085bb04493bfed0e5c1b4fe7c2704504d769f955cc78fe4", "47a00b22c52ee59dffc2aad02d0bbfb20c26ec5b8de8900492bf13ad6901cf35", "4c067db43b331fc709080d441cb2e157114fec60749667d12186cc3fc8e7a951", "4c092310f804a5d45a1bcaa4191d6d016c457b6ed3982a622c35f729ff1c7f6b", "53b711b33134e292ef8499835a3df10909c58df53a2a0308f598c432e9a62892", "568d6bee70652d8a5af1cd3eec48b4ca1696fb1773b80719ebbd2925b72cb8f6", "56fa55032782b7f8e0bf6956420d11e2d4e9860598dfe9c504edec53af0fc372", "5a2c492680c61b440272341294172fa3b3751797b1ab983533a770e4fb0a67ac", "61235cc39b5b2f593086d1d38f3fc130b2d125bd8fc8621d35bc5b6bdeb92bd2", "619ac9aaf681434b4d4718d1b31aa2f0fce64f2b3f8435688fcbdc0c818b6c54", "6238ac1f483494011abde5286282afdfacd8926659e222ba9b74c67008d3a58c", "63752a72ca4d4e1386278bd43d14232f51718b409e7ac86bcf8810826b531113", "6fdc5ccb43864065d40dd838437952e9e3da9821b7eac605ba46ada77f846bdf", "7abc3a6825a346fa4621a6f63e3b662bbb9e0f6ffc32d30a459d695f20fb1a8b", "7aef381bb9ae8a3821abd7f9d4d93978dbd99072b48522e181baeffcd95b56ae", "80df3caf251fe61a3f0c9614adc6e2bfcffd1cd3345280896766712fb4b4d6d7", "95f970f34b59987dee6f360d2e7d30e181d58957b85dff929eee4423739bd151", "993257f6ca3cde55332af1f62af3e04ca89ce63c08b56a387cdd46136c72f2fa", "9c0a57390549affc2b5dda24a38de03a5c7cbc58750cd161ff5d106c3c6eec80", "a0794e987d55d2f719cc95fcf980fc62d12b80e287e6a761c4be14c60bd9fecc", "a3b98121e68bf370dd8ea09df67e916f93ea95b52fc010902312168c4d1aff5d", "a60756d55f0887023b3899e6c2923ba5f0042fb11b1d17810b4e07395404f33e", "a676bd2fbc2309092b9bbb0083d35718b5420af3a42135ebb1e4c3633f56604d", "a732838c78554c1257ff2492f5c8c4c7312d0aecd7f732149e255f3749edd5ee", "ad3dc88dfe61f0f1f9b99c6bc833ea2f45203a937a18f0d2faa57c6952656012", "ae65d65fde4135ef423a2608587c9ef585a3551fc2e4e431e7c7e527047581be", "b070a4f064a9edb70f921bfdc270725cff7a78c22036dd37a767c51393fb956f", "b6da85949aa91e9f8c521681344bd2e163de894a5492337fba8b05c409225a4f", "bbf47110765b2a999803a7de457567389253f8670f7daafb98e059c899ce9764", "bd9c1e6f92b4888ae3ef7ae23262c513b962f09f3fb3b48581dde5df7d7a860a", "c06b3f998d2d7160db58db69adfb807d2ec307e883e2f17f6b87a1ef6c723f11", "c318fb70542be16d3d4063cde6010b1e4d328993a793529c15a619251f517c39", "c4aef42e5fa4c9d5a99f751fb79caa880dac7eaf8a65121549318b984676a1b7", "c9ca545e93a9c2a3bdaa2e6e21f7a43267ff0813e8055adf2b591c13164c0c57", "da2c3220eb55c4239dd8b982e213da0b79023cac59fe54ca09365f2bc7e4ad32", "dd8055da300535eefd446b30995c0813cc4394873c9509323762a93e97c04c03", "e2b46e092ea54b732d98c476720386ff2ccd126de1e52076b470b117bff7e409", "e334c4f39a2863a239d38b5829e442a87f241a92da9941861ee6ec5d6380b7fe", "e5c54f04ca42bbb5153aec5d4f2e3d9f81e316945220ac318abd4083308143f5", "f4d06764a06b137e48db6d569dc95614d9d225c89842c885669ee8abc9f28c7a", "f96333f9d2517c752c20a35ff95de5fc2763ac8cdb1653df0f6f45d281620606"] python-dateutil = ["73ebfe9dbf22e832286dafa60473e4cd239f8592f699aa5adaf10050e6e1823c", "75bb3f31ea686f1197762692a9ee6a7550b59fc6ca3a1f4b5d7e32fb98e2da2a"] -python-dotenv = ["debd928b49dbc2bf68040566f55cdb3252458036464806f4094487244e2a4093", "f157d71d5fec9d4bd5f51c82746b6344dffa680ee85217c123f4a0c8117c4544"] -pyyaml = ["0e7f69397d53155e55d10ff68fdfb2cf630a35e6daf65cf0bdeaf04f127c09dc", "2e9f0b7c5914367b0916c3c104a024bb68f269a486b9d04a2e8ac6f6597b7803", "35ace9b4147848cafac3db142795ee42deebe9d0dad885ce643928e88daebdcc", "38a4f0d114101c58c0f3a88aeaa44d63efd588845c5a2df5290b73db8f246d15", "483eb6a33b671408c8529106df3707270bfacb2447bf8ad856a4b4f57f6e3075", "4b6be5edb9f6bb73680f5bf4ee08ff25416d1400fbd4535fe0069b2994da07cd", "7f38e35c00e160db592091751d385cd7b3046d6d51f578b29943225178257b31", "8100c896ecb361794d8bfdb9c11fce618c7cf83d624d73d5ab38aef3bc82d43f", "c0ee8eca2c582d29c3c2ec6e2c4f703d1b7f1fb10bc72317355a746057e7346c", "e4c015484ff0ff197564917b4b4246ca03f411b9bd7f16e02a2f586eb48b6d04", "ebc4ed52dcc93eeebeae5cf5deb2ae4347b3a81c3fa12b0b8c976544829396a4"] -six = ["1f1b7d42e254082a9db6279deae68afb421ceba6158efa6131de7b3003ee93fd", "30f610279e8b2578cab6db20741130331735c781b56053c59c4076da27f06b66"] -smmap2 = ["0555a7bf4df71d1ef4218e4807bbf9b201f910174e6e08af2e138d4e517b4dde", "29a9ffa0497e7f2be94ca0ed1ca1aa3cd4cf25a1f6b4f5f87f74b46ed91d609a"] -stevedore = ["01d9f4beecf0fbd070ddb18e5efb10567801ba7ef3ddab0074f54e3cd4e91730", "e0739f9739a681c7a1fda76a102b65295e96a144ccdb552f2ae03c5f0abe8a14"] -toml = ["229f81c57791a41d65e399fc06bf0848bab550a9dfd5ed66df18ce5f05e73d5c", "235682dd292d5899d361a811df37e04a8828a5b1da3115886b73cf81ebc9100e", "f1db651f9657708513243e61e6cc67d101a39bad662eaa9b5546f789338e07a3"] -typed-ast = ["1170afa46a3799e18b4c977777ce137bb53c7485379d9706af8a59f2ea1aa161", "18511a0b3e7922276346bcb47e2ef9f38fb90fd31cb9223eed42c85d1312344e", "262c247a82d005e43b5b7f69aff746370538e176131c32dda9cb0f324d27141e", "2b907eb046d049bcd9892e3076c7a6456c93a25bebfe554e931620c90e6a25b0", "354c16e5babd09f5cb0ee000d54cfa38401d8b8891eefa878ac772f827181a3c", "48e5b1e71f25cfdef98b013263a88d7145879fbb2d5185f2a0c79fa7ebbeae47", "4e0b70c6fc4d010f8107726af5fd37921b666f5b31d9331f0bd24ad9a088e631", "630968c5cdee51a11c05a30453f8cd65e0cc1d2ad0d9192819df9978984529f4", "66480f95b8167c9c5c5c87f32cf437d585937970f3fc24386f313a4c97b44e34", "71211d26ffd12d63a83e079ff258ac9d56a1376a25bc80b1cdcdf601b855b90b", "7954560051331d003b4e2b3eb822d9dd2e376fa4f6d98fee32f452f52dd6ebb2", "838997f4310012cf2e1ad3803bce2f3402e9ffb71ded61b5ee22617b3a7f6b6e", "95bd11af7eafc16e829af2d3df510cecfd4387f6453355188342c3e79a2ec87a", "bc6c7d3fa1325a0c6613512a093bc2a2a15aeec350451cbdf9e1d4bffe3e3233", "cc34a6f5b426748a507dd5d1de4c1978f2eb5626d51326e43280941206c209e1", "d755f03c1e4a51e9b24d899561fec4ccaf51f210d52abdf8c07ee2849b212a36", "d7c45933b1bdfaf9f36c579671fec15d25b06c8398f113dab64c18ed1adda01d", "d896919306dd0aa22d0132f62a1b78d11aaf4c9fc5b3410d3c666b818191630a", "fdc1c9bbf79510b76408840e009ed65958feba92a88833cdceecff93ae8fff66", "ffde2fbfad571af120fcbfbbc61c72469e72f550d676c3342492a9dfdefb8f12"] +python-dotenv = ["440c7c23d53b7d352f9c94d6f70860242c2f071cf5c029dd661ccb22d64ae42b", "f254bfd0c970d64ccbb6c9ebef3667ab301a71473569c991253a481f1c98dddc"] +pyyaml = ["06a0d7ba600ce0b2d2fe2e78453a470b5a6e000a985dd4a4e54e436cc36b0e97", "240097ff019d7c70a4922b6869d8a86407758333f02203e0fc6ff79c5dcede76", "4f4b913ca1a7319b33cfb1369e91e50354d6f07a135f3b901aca02aa95940bd2", "69f00dca373f240f842b2931fb2c7e14ddbacd1397d57157a9b005a6a9942648", "73f099454b799e05e5ab51423c7bcf361c58d3206fa7b0d555426b1f4d9a3eaf", "74809a57b329d6cc0fdccee6318f44b9b8649961fa73144a98735b0aaf029f1f", "7739fc0fa8205b3ee8808aea45e968bc90082c10aef6ea95e855e10abf4a37b2", "95f71d2af0ff4227885f7a6605c37fd53d3a106fcab511b8860ecca9fcf400ee", "b8eac752c5e14d3eca0e6dd9199cd627518cb5ec06add0de9d32baeee6fe645d", "cc8955cfbfc7a115fa81d85284ee61147059a753344bc51098f3ccd69b0d7e0c", "d13155f591e6fcc1ec3b30685d50bf0711574e2c0dfffd7644babf8b5102ca1a"] +regex = ["150125da109fccdcc8fec3b0b386b2a5d6ca7cff076f8b622486d1ca868b0c10", "163bc0805e46acfa098dfc8c0b07f371577d505f603e48afc425ff475cdac3a5", "20c513893ff80bdbe4b4ce11ea2e93d49481f05b270595d82af69ffc402010a6", "21fc17cb868c4264f0813f992f46f9ae6fc8c309d4741091de4153bd1f6a6176", "2c928bc8e0c453d73dffa3193a6e37ee752ea36df0dd4601e21024d98274dfad", "2d9beca70e36f9c60d679e108c5fe49f3d4da79d13a13f91e5e759443bd954f9", "5735f26cacdb50b3d6d35ebf8fdeb504bd8b381e2d079d2d9f12ce534fc14ecd", "6edc5c190248d3b612f2cca45448cf8ebc3621d41afcd1c5708853cbb1dbb3b3", "7606dba82435429641efe4fbc580574942f89cf2b9c5c1f8bc1eab2bacbf7e8b", "8d1ee3796795e609ef7a3a5a35eaf4728038d986aa12c06b3fd1b92ee81911f4", "8d9bb2d90e23c51aacbc58c1a11320f49b335cd67a91986cdbebcc3e843e4de8", "97d414c41f19fd2362e493810caa8445c05e0a2d63a14081c972aad66284a8d2", "9e37502817225ee99d91d8418f5119e98c380b00e772d06915690c05290f32ee", "af7209b2fcc79ee2b0ad4ea080d70bb748450ec4f282cc9e864861e469b1072e", "c0849b0864ff451f04c8afb5fc28e9ed592262e03debdd227cf0f53e04a55dcd", "c4ac9215650688e78dea29b46adbdafb7b85058eebe92ef6ea848e14466c915f", "dcda6d4e1bbfc939b177c237aee41c9678eaaf71df482688f8986e8251e12345", "dd8501b8d9ea1aba53c4bc7d47bc72933f9b4213d534cf400f16c1431f51c8ba", "ec0e509ed1877ff1cbc6f0864689bb60384a303502c4d72d9a635f8a4676fd3f", "f6c8c3f56fef719180464855346e6e80971b86dfd9e5a0e356664b5baca53072", "ffd4f80602490a309064cf2b203e220d581c51660e01055c64bf5da450485ee6"] +six = ["30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259", "8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced"] +smmap = ["54c44c197c819d5ef1991799a7e30b662d1e520f2ac75c9efbeb54a742214cf4", "9c98bbd1f9786d22f14b3d4126894d56befb835ec90cef151af566c7e19b5d24"] +stevedore = ["001e90cd704be6470d46cc9076434e2d0d566c1379187e7013eb296d3a6032d9", "471c920412265cc809540ae6fb01f3f02aba89c79bbc7091372f4745a50f9691"] +toml = ["926b612be1e5ce0634a2ca03470f95169cf16f939018233a670519cb4ac58b0f", "bda89d5935c2eac546d648028b9901107a595863cb36bae0c73ac804a9b4ce88"] +typed-ast = ["0666aa36131496aed8f7be0410ff974562ab7eeac11ef351def9ea6fa28f6355", "0c2c07682d61a629b68433afb159376e24e5b2fd4641d35424e462169c0a7919", "249862707802d40f7f29f6e1aad8d84b5aa9e44552d2cc17384b209f091276aa", "24995c843eb0ad11a4527b026b4dde3da70e1f2d8806c99b7b4a7cf491612652", "269151951236b0f9a6f04015a9004084a5ab0d5f19b57de779f908621e7d8b75", "4083861b0aa07990b619bd7ddc365eb7fa4b817e99cf5f8d9cf21a42780f6e01", "498b0f36cc7054c1fead3d7fc59d2150f4d5c6c56ba7fb150c013fbc683a8d2d", "4e3e5da80ccbebfff202a67bf900d081906c358ccc3d5e3c8aea42fdfdfd51c1", "6daac9731f172c2a22ade6ed0c00197ee7cc1221aa84cfdf9c31defeb059a907", "715ff2f2df46121071622063fc7543d9b1fd19ebfc4f5c8895af64a77a8c852c", "73d785a950fc82dd2a25897d525d003f6378d1cb23ab305578394694202a58c3", "8c8aaad94455178e3187ab22c8b01a3837f8ee50e09cf31f1ba129eb293ec30b", "8ce678dbaf790dbdb3eba24056d5364fb45944f33553dd5869b7580cdbb83614", "aaee9905aee35ba5905cfb3c62f3e83b3bec7b39413f0a7f19be4e547ea01ebb", "bcd3b13b56ea479b3650b82cabd6b5343a625b0ced5429e4ccad28a8973f301b", "c9e348e02e4d2b4a8b2eedb48210430658df6951fa484e59de33ff773fbd4b41", "d205b1b46085271b4e15f670058ce182bd1199e56b317bf2ec004b6a44f911f6", "d43943ef777f9a1c42bf4e552ba23ac77a6351de620aa9acf64ad54933ad4d34", "d5d33e9e7af3b34a40dc05f498939f0ebf187f07c385fd58d591c533ad8562fe", "fc0fea399acb12edbf8a628ba8d2312f583bdbdb3335635db062fa98cf71fca4", "fe460b922ec15dd205595c9b5b99e2f056fd98ae8f9f56b888e7a17dc2b757e7"] uvloop = ["08b109f0213af392150e2fe6f81d33261bb5ce968a288eb698aad4f46eb711bd", "123ac9c0c7dd71464f58f1b4ee0bbd81285d96cdda8bc3519281b8973e3a461e", "4315d2ec3ca393dd5bc0b0089d23101276778c304d42faff5dc4579cb6caef09", "4544dcf77d74f3a84f03dd6278174575c44c67d7165d4c42c71db3fdc3860726", "afd5513c0ae414ec71d24f6f123614a80f3d27ca655a4fcf6cabe50994cc1891", "b4f591aa4b3fa7f32fb51e2ee9fea1b495eb75b0b3c8d0ca52514ad675ae63f7", "bcac356d62edd330080aed082e78d4b580ff260a677508718f88016333e2c9c5", "e7514d7a48c063226b7d06617cbb12a14278d4323a065a8d46a7962686ce2e95", "f07909cd9fc08c52d294b1570bba92186181ca01fe3dc9ffba68955273dd7362"] -websockets = ["0e2f7d6567838369af074f0ef4d0b802d19fa1fee135d864acc656ceefa33136", "2a16dac282b2fdae75178d0ed3d5b9bc3258dabfae50196cbb30578d84b6f6a6", "5a1fa6072405648cb5b3688e9ed3b94be683ce4a4e5723e6f5d34859dee495c1", "5c1f55a1274df9d6a37553fef8cff2958515438c58920897675c9bc70f5a0538", "669d1e46f165e0ad152ed8197f7edead22854a6c90419f544e0f234cc9dac6c4", "695e34c4dbea18d09ab2c258994a8bf6a09564e762655408241f6a14592d2908", "6b2e03d69afa8d20253455e67b64de1a82ff8612db105113cccec35d3f8429f0", "79ca7cdda7ad4e3663ea3c43bfa8637fc5d5604c7737f19a8964781abbd1148d", "7fd2dd9a856f72e6ed06f82facfce01d119b88457cd4b47b7ae501e8e11eba9c", "82c0354ac39379d836719a77ee360ef865377aa6fdead87909d50248d0f05f4d", "8f3b956d11c5b301206382726210dc1d3bee1a9ccf7aadf895aaf31f71c3716c", "91ec98640220ae05b34b79ee88abf27f97ef7c61cf525eec57ea8fcea9f7dddb", "952be9540d83dba815569d5cb5f31708801e0bbfc3a8c5aef1890b57ed7e58bf", "99ac266af38ba1b1fe13975aea01ac0e14bb5f3a3200d2c69f05385768b8568e", "9fa122e7adb24232247f8a89f2d9070bf64b7869daf93ac5e19546b409e47e96", "a0873eadc4b8ca93e2e848d490809e0123eea154aa44ecd0109c4d0171869584", "cb998bd4d93af46b8b49ecf5a72c0a98e5cc6d57fdca6527ba78ad89d6606484", "e02e57346f6a68523e3c43bbdf35dde5c440318d1f827208ae455f6a2ace446d", "e79a5a896bcee7fff24a788d72e5c69f13e61369d055f28113e71945a7eb1559", "ee55eb6bcf23ecc975e6b47c127c201b913598f38b6a300075f84eeef2d3baff", "f1414e6cbcea8d22843e7eafdfdfae3dd1aba41d1945f6ca66e4806c07c4f454"] -wrapt = ["565a021fd19419476b9362b05eeaa094178de64f8361e44468f9e9d7843901e1"] +websockets = ["0e4fb4de42701340bd2353bb2eee45314651caa6ccee80dbd5f5d5978888fed5", "1d3f1bf059d04a4e0eb4985a887d49195e15ebabc42364f4eb564b1d065793f5", "20891f0dddade307ffddf593c733a3fdb6b83e6f9eef85908113e628fa5a8308", "295359a2cc78736737dd88c343cd0747546b2174b5e1adc223824bcaf3e164cb", "2db62a9142e88535038a6bcfea70ef9447696ea77891aebb730a333a51ed559a", "3762791ab8b38948f0c4d281c8b2ddfa99b7e510e46bd8dfa942a5fff621068c", "3db87421956f1b0779a7564915875ba774295cc86e81bc671631379371af1170", "3ef56fcc7b1ff90de46ccd5a687bbd13a3180132268c4254fc0fa44ecf4fc422", "4f9f7d28ce1d8f1295717c2c25b732c2bc0645db3215cf757551c392177d7cb8", "5c01fd846263a75bc8a2b9542606927cfad57e7282965d96b93c387622487485", "5c65d2da8c6bce0fca2528f69f44b2f977e06954c8512a952222cea50dad430f", "751a556205d8245ff94aeef23546a1113b1dd4f6e4d102ded66c39b99c2ce6c8", "7ff46d441db78241f4c6c27b3868c9ae71473fe03341340d2dfdbe8d79310acc", "965889d9f0e2a75edd81a07592d0ced54daa5b0785f57dc429c378edbcffe779", "9b248ba3dd8a03b1a10b19efe7d4f7fa41d158fdaa95e2cf65af5a7b95a4f989", "9bef37ee224e104a413f0780e29adb3e514a5b698aabe0d969a6ba426b8435d1", "c1ec8db4fac31850286b7cd3b9c0e1b944204668b8eb721674916d4e28744092", "c8a116feafdb1f84607cb3b14aa1418424ae71fee131642fc568d21423b51824", "ce85b06a10fc65e6143518b96d3dca27b081a740bae261c2fb20375801a9d56d", "d705f8aeecdf3262379644e4b55107a3b55860eb812b673b28d0fbc347a60c55", "e898a0863421650f0bebac8ba40840fc02258ef4714cb7e1fd76b6a6354bda36", "f8a7bff6e8664afc4e6c28b983845c5bc14965030e3fb98789734d416af77c4b"] +wrapt = ["b62ffa81fb85f4332a4f609cab4ac40709470da05643a082ec1eb88e6d9b97d7"] yarl = ["0c2ab325d33f1b824734b3ef51d4d54a54e0e7a23d13b86974507602334c2cce", "0ca2f395591bbd85ddd50a82eb1fde9c1066fafe888c5c7cc1d810cf03fd3cc6", "2098a4b4b9d75ee352807a95cdf5f10180db903bc5b7270715c6bbe2551f64ce", "25e66e5e2007c7a39541ca13b559cd8ebc2ad8fe00ea94a2aad28a9b1e44e5ae", "26d7c90cb04dee1665282a5d1a998defc1a9e012fdca0f33396f81508f49696d", "308b98b0c8cd1dfef1a0311dc5e38ae8f9b58349226aa0533f15a16717ad702f", "3ce3d4f7c6b69c4e4f0704b32eca8123b9c58ae91af740481aa57d7857b5e41b", "58cd9c469eced558cd81aa3f484b2924e8897049e06889e8ff2510435b7ef74b", "5b10eb0e7f044cf0b035112446b26a3a2946bca9d7d7edb5e54a2ad2f6652abb", "6faa19d3824c21bcbfdfce5171e193c8b4ddafdf0ac3f129ccf0cdfcb083e462", "944494be42fa630134bf907714d40207e646fd5a94423c90d5b514f7b0713fea", "a161de7e50224e8e3de6e184707476b5a989037dcb24292b391a3d66ff158e70", "a4844ebb2be14768f7994f2017f70aca39d658a96c786211be5ddbe1c68794c1", "c2b509ac3d4b988ae8769901c66345425e361d518aecbe4acbfc2567e416626a", "c9959d49a77b0e07559e579f38b2f3711c2b8716b8410b320bf9713013215a1b", "d8cdee92bc930d8b09d8bd2043cedd544d9c8bd7436a77678dd602467a993080", "e15199cdb423316e15f108f51249e44eb156ae5dba232cb73be555324a1d49c2"] diff --git a/pyproject.toml b/pyproject.toml index 9c7122c417..0d47eb844a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.black] line-length = 99 -target-version = ['py36'] +target-version = ['py37'] include = '\.pyi?$' exclude = ''' ( @@ -21,8 +21,8 @@ exclude = ''' [tool.poetry] name = 'Modmail' -version = '3.5.0-dev0' -description = 'Modmail is similar to Reddits Modmail both in functionality and purpose. It serves as a shared inbox for server staff to communicate with their users in a seamless way.' +version = '3.6.0' +description = "Modmail is similar to Reddit's Modmail, both in functionality and purpose. It serves as a shared inbox for server staff to communicate with their users in a seamless way." license = 'AGPL-3.0-only' authors = [ 'kyb3r ', @@ -36,10 +36,10 @@ keywords = ['discord', 'modmail'] [tool.poetry.dependencies] python = "^3.7" -"discord.py" = "=1.2.5" -uvloop = "^0.14.0" -python-dotenv = "^0.10.3" -parsedatetime = "^2.5" +"discord.py" = "=1.5.1" +uvloop = {version = ">=0.12.0", markers = "sys_platform != 'win32'"} +python-dotenv = ">=0.10.3" +parsedatetime = "^2.6" dnspython = "^1.16" isodate = "^0.6.0" natural = "^0.2.0" @@ -47,10 +47,12 @@ motor = {version = "^2.1", optional = true} emoji = "^0.5.4" python-dateutil = "^2.8" colorama = "^0.4.3" -aiohttp = "<3.6.0,>=3.3.0" +aiohttp = ">=3.6.0,<3.7.0" [tool.poetry.dev-dependencies] -black = {version = "=19.3b0", allows-prereleases = true} +black = {version = "=19.10b0", allow-prereleases = true} pylint = "^2.4" bandit = "^1.6" +[tool.poetry.extras] +mongodb = ["motor"] diff --git a/requirements.min.txt b/requirements.min.txt index dded194188..9fd4adf1a3 100644 --- a/requirements.min.txt +++ b/requirements.min.txt @@ -1,24 +1,24 @@ -# Generated as of October, 2019 +# Generated as of June, 2020 # This is the bare minimum requirements.txt for running Modmail. # To install requirements.txt run: pip install -r requirements.min.txt -aiohttp==3.5.4 +aiohttp==3.6.2 async-timeout==3.0.1 attrs==19.3.0 chardet==3.0.4 -discord.py==1.2.5 +discord.py==1.5.1 dnspython==1.16.0 emoji==0.5.4 -future==0.18.1 -idna==2.8 +future==0.18.2 +idna==2.9 isodate==0.6.0 -motor==2.0.0 -multidict==4.5.2 +motor==2.1.0 +multidict==4.7.6 natural==0.2.0 -parsedatetime==2.4 -pymongo==3.9.0 -python-dateutil==2.8.0 -python-dotenv==0.10.3 -six==1.12.0 -websockets==6.0 -yarl==1.3.0 +parsedatetime==2.6 +pymongo==3.10.1 +python-dateutil==2.8.1 +python-dotenv==0.14.0 +six==1.15.0 +websockets==8.1 +yarl==1.4.2 diff --git a/runtime.txt b/runtime.txt index aefcfbece7..67068f10fe 100644 --- a/runtime.txt +++ b/runtime.txt @@ -1 +1 @@ -python-3.7.5 +python-3.9.0 \ No newline at end of file diff --git a/translation_files.py b/translation_files.py deleted file mode 100644 index 07c6979f63..0000000000 --- a/translation_files.py +++ /dev/null @@ -1,127 +0,0 @@ -import csv -import glob -import re -import string - -from discord.ext import commands - -from core import translations -from cogs.modmail import Modmail -from cogs.plugins import Plugins -from cogs.utility import Utility - - -data = [("Identifier", "English", "Context")] -all_identifiers = [] -identifiers = {} - - -class FormatError(Exception): - def __init__(self, reason, string): - super().__init__(f"Unable to parse {reason}: {string}") - - -for filename in glob.glob("**/*.py") + glob.glob("*.py"): - if filename == "translation_files.py": - continue - with open(filename, encoding="utf8") as f: - filedata = f.read() - regex_matches = re.findall( - r"(?:^|[^A-z])_\(.+?(?:\'|\")\)+?", filedata, flags=re.DOTALL | re.MULTILINE - ) - - for i in regex_matches: - if "f'" in i or 'f"' in i: - print(FormatError("f-string", i)) - identifier = "" - read = False - ignore_inverted = False - newline = False - counter = 0 - mode = None - for n in range(len(i)): - triggered = False - x = i[n] - - if x in ("'", '"') and not ignore_inverted: - if not mode: - mode = x - if mode == x: - read = not read - triggered = True - - if x == "\n": - newline = True - - if read and not triggered: - newline = False - identifier += x - - if newline and x not in string.whitespace: - counter += 1 - if counter > 1: - break - else: - counter = 0 - - if x == "\\": - ignore_inverted = True - elif ignore_inverted: - ignore_inverted = False - - all_identifiers.append(identifier) - - filedata_lines = filedata.splitlines() - fullline = list(filter((lambda x: x.find(i.splitlines()[0]) != -1), filedata_lines))[0] - count = 0 - for nline, line in enumerate(filedata_lines): - if line == fullline: - count += 1 - if count == all_identifiers.count(identifier): - linenum = nline + 1 - break - - if identifier in identifiers.keys(): - if filename not in data[identifiers[identifier]][2]: - data[identifiers[identifier]][2] += f" {filename}/L{linenum}" - elif str(linenum) not in data[identifiers[identifier]][2]: - split_space = data[identifiers[identifier]][2].split(" ") - for nx, x in enumerate(split_space): - if filename in x: - split_space[nx] += f"/L{linenum}" - break - data[identifiers[identifier]][2] = (" ").join(split_space) - else: - data.append([identifier, identifier, f"File: {filename}/L{linenum}"]) - identifiers[identifier] = len(data) - 1 - - print(filename) - -translations.init() -done = set() -bot = commands.Bot(command_prefix=None) -cogs = [Modmail(bot), Plugins(bot), Utility(bot)] -for i in cogs: - if i.description: - data.append([i.description, i.description, f"Cog: {i.__cog_name__}"]) - - for cmd in i.walk_commands(): - if cmd not in done: - if cmd.short_doc: - print(cmd) - data.append( - [ - cmd.short_doc, - cmd.short_doc, - f"Cog: {i.__cog_name__}\nCommand: {cmd.qualified_name}", - ] - ) - data.append( - [cmd.help, cmd.help, f"Cog: {i.__cog_name__}\nCommand: {cmd.qualified_name}"] - ) - - done.add(cmd) - - -with open("languages/en.csv", "w+") as f: - csv.writer(f, dialect="unix").writerows(data)