mirror of
https://github.com/Cog-Creators/Red-DiscordBot.git
synced 2025-12-05 17:02:32 -05:00
Compare commits
195 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7685c4d5d5 | ||
|
|
e701ec9617 | ||
|
|
6c1ee096a1 | ||
|
|
2df282222f | ||
|
|
43c7bd48c7 | ||
|
|
86579068d9 | ||
|
|
8e6ab9aa35 | ||
|
|
77566a887a | ||
|
|
9d0eca1914 | ||
|
|
79a3164d9d | ||
|
|
eb73e48192 | ||
|
|
cd6af7f185 | ||
|
|
3d6020b9cf | ||
|
|
461f03aac0 | ||
|
|
35149f8837 | ||
|
|
c0d01f32a6 | ||
|
|
83a0459b6a | ||
|
|
50f6dcef2f | ||
|
|
5c514fd663 | ||
|
|
1c2196f78f | ||
|
|
43cc3c40f3 | ||
|
|
7a6a4cf59d | ||
|
|
3bcf375204 | ||
|
|
a175bdc1c7 | ||
|
|
b557b437a3 | ||
|
|
d1f0b59b5d | ||
|
|
3ece3a1f2b | ||
|
|
1f1a85de18 | ||
|
|
e08c9dafa6 | ||
|
|
ad27607ccc | ||
|
|
c1bcca4432 | ||
|
|
9f2ed694ce | ||
|
|
edadd8f2fd | ||
|
|
afa08713e0 | ||
|
|
d23620727e | ||
|
|
b456c6ad3b | ||
|
|
0298b53803 | ||
|
|
bfd6e4af3f | ||
|
|
31612aae4a | ||
|
|
219367e7c1 | ||
|
|
7b64f10fc7 | ||
|
|
1ad1744054 | ||
|
|
7b825f2cd7 | ||
|
|
3759fce090 | ||
|
|
470521f7c8 | ||
|
|
a070dffb93 | ||
|
|
9e7bc94aab | ||
|
|
033d0113a5 | ||
|
|
d0a53ed2df | ||
|
|
49b80e9fe3 | ||
|
|
d5f5ddbec5 | ||
|
|
17c7dd658d | ||
|
|
ca19ecaefc | ||
|
|
c149f00f82 | ||
|
|
b041d59fc7 | ||
|
|
b983d5904b | ||
|
|
8b15053dd4 | ||
|
|
e15815cd97 | ||
|
|
94a64d8fae | ||
|
|
fd7088de1a | ||
|
|
7d4946560d | ||
|
|
b7c9647e1a | ||
|
|
36b9f64aae | ||
|
|
60a72b2ba4 | ||
|
|
f830f73ae6 | ||
|
|
95f51e1126 | ||
|
|
8916f55d52 | ||
|
|
4aaef9558a | ||
|
|
0b78664792 | ||
|
|
db5d4d5158 | ||
|
|
0dfd8b6453 | ||
|
|
11a2fb1088 | ||
|
|
40feeff442 | ||
|
|
a0a2976e0a | ||
|
|
741f3cbdcc | ||
|
|
a6965c4b5a | ||
|
|
19b05e632c | ||
|
|
8610b47a68 | ||
|
|
2ab8890540 | ||
|
|
5de5a519c3 | ||
|
|
0d193d3e9e | ||
|
|
622382f425 | ||
|
|
c1f09326cc | ||
|
|
ddbbba4aaa | ||
|
|
bcf7ea30c5 | ||
|
|
35e9fab701 | ||
|
|
864b6d313e | ||
|
|
d47d12e961 | ||
|
|
9f0e752318 | ||
|
|
34bd5ead15 | ||
|
|
1fd5dffdc7 | ||
|
|
6d7a900bbb | ||
|
|
fb4f921159 | ||
|
|
14cc701b25 | ||
|
|
d8c4113d24 | ||
|
|
9eb6bb7738 | ||
|
|
de96f8b9f9 | ||
|
|
e34975001c | ||
|
|
f3b282062b | ||
|
|
84732a24fa | ||
|
|
dad775b494 | ||
|
|
05ad3fcd5c | ||
|
|
6ae02d2d02 | ||
|
|
757a3114dc | ||
|
|
94b9878c6c | ||
|
|
7775b16199 | ||
|
|
f01d48f9ae | ||
|
|
179883094e | ||
|
|
971ccf9df4 | ||
|
|
07eb6bf88e | ||
|
|
5afd8174ca | ||
|
|
f1fea38712 | ||
|
|
f275c6e5e7 | ||
|
|
5ec25959df | ||
|
|
4f270f3aab | ||
|
|
4028dd3009 | ||
|
|
706b04610d | ||
|
|
014e3baea0 | ||
|
|
92ca7c935a | ||
|
|
5c9b1c9a3d | ||
|
|
5ebee60c97 | ||
|
|
3337a9cbab | ||
|
|
54975eb812 | ||
|
|
537531803a | ||
|
|
f4b640126b | ||
|
|
1de3251127 | ||
|
|
7e98076e4a | ||
|
|
c58c55b752 | ||
|
|
928be5717f | ||
|
|
ccbaa926ce | ||
|
|
d1208d7d19 | ||
|
|
099fe59a97 | ||
|
|
889acaec82 | ||
|
|
c42e9d4c5c | ||
|
|
4378e5295d | ||
|
|
73a427f6aa | ||
|
|
abfee70eb3 | ||
|
|
77cdbf8dd6 | ||
|
|
28bc68c916 | ||
|
|
ecb64cc2ec | ||
|
|
23706a1ba9 | ||
|
|
d3f406a34a | ||
|
|
55afc7eb33 | ||
|
|
7a70d12efd | ||
|
|
1ecaf6f8d5 | ||
|
|
e01cdbb091 | ||
|
|
b88b5a2601 | ||
|
|
e7476edd68 | ||
|
|
cbbeb412f9 | ||
|
|
f544890f00 | ||
|
|
72560fa6d0 | ||
|
|
4637ff78c0 | ||
|
|
501aff41ea | ||
|
|
449b1bfe9e | ||
|
|
4a8358ecb4 | ||
|
|
8f74e4dd31 | ||
|
|
2b35d9f012 | ||
|
|
35001107e0 | ||
|
|
a7d7b90ae8 | ||
|
|
119ba7ef8b | ||
|
|
28bbe9c646 | ||
|
|
8739c04024 | ||
|
|
57240d25b9 | ||
|
|
15ea5440a3 | ||
|
|
1e60d1c265 | ||
|
|
b7cd097c43 | ||
|
|
6c934b02e6 | ||
|
|
fcb9b40b43 | ||
|
|
7a6884e4b1 | ||
|
|
e86698cfeb | ||
|
|
53650aefa6 | ||
|
|
1d80a0cad1 | ||
|
|
f6d27a0f43 | ||
|
|
f71aa9dd21 | ||
|
|
1cb5394e96 | ||
|
|
2b2dbd25f7 | ||
|
|
dd4cd0eeb1 | ||
|
|
ee7b0cf730 | ||
|
|
95ef5d6348 | ||
|
|
23192b9ef6 | ||
|
|
7cd98c8a63 | ||
|
|
fca7686701 | ||
|
|
be767478f4 | ||
|
|
b3ad5d90ed | ||
|
|
fb093b7411 | ||
|
|
e4ea3110e3 | ||
|
|
79676c4f72 | ||
|
|
d61827b92c | ||
|
|
1f1f46c70f | ||
|
|
9188e4a7ec | ||
|
|
e5a780eb0c | ||
|
|
d8c85a2b15 | ||
|
|
83080bc5a2 | ||
|
|
233bfc59ac | ||
|
|
c606caf3a3 |
3
.github/CODEOWNERS
vendored
3
.github/CODEOWNERS
vendored
@@ -24,6 +24,7 @@ redbot/core/utils/mod.py @palmtree5
|
||||
redbot/core/utils/data_converter.py @mikeshardmind
|
||||
redbot/core/utils/antispam.py @mikeshardmind
|
||||
redbot/core/utils/tunnel.py @mikeshardmind
|
||||
redbot/core/utils/caching.py @mikeshardmind
|
||||
|
||||
# Cogs
|
||||
redbot/cogs/admin/* @tekulvw
|
||||
@@ -43,6 +44,8 @@ redbot/cogs/streams/* @Twentysix26 @palmtree5
|
||||
redbot/cogs/trivia/* @Tobotimus
|
||||
redbot/cogs/dataconverter/* @mikeshardmind
|
||||
redbot/cogs/reports/* @mikeshardmind
|
||||
redbot/cogs/permissions/* @mikeshardmind
|
||||
redbot/cogs/warnings/* @palmtree5
|
||||
|
||||
# Docs
|
||||
docs/* @tekulvw @palmtree5
|
||||
|
||||
116
.github/CONTRIBUTING.md
vendored
116
.github/CONTRIBUTING.md
vendored
@@ -1,23 +1,43 @@
|
||||
# Introduction
|
||||
### Welcome!
|
||||
First off, thank you for contributing to the further development of Red. We're always looking for new ways to improve our project and we appreciate any help you can give us.
|
||||
# Contents
|
||||
* [1. Introduction](#1-introduction)
|
||||
* [1.1 Why do these guidelines exist?](#11-why-do-these-guidelines-exist)
|
||||
* [1.2 What kinds of contributions are we looking for?](#12-what-kinds-of-contributions-are-we-looking-for)
|
||||
* [2. Ground Rules](#2-ground-rules)
|
||||
* [3. Your First Contribution](#3-your-first-contribution)
|
||||
* [4. Getting Started](#4-getting-started)
|
||||
* [4.1 Setting up your development environment](#41-setting-up-your-development-environment)
|
||||
* [4.2 Testing](#42-testing)
|
||||
* [4.3 Style](#43-style)
|
||||
* [4.4 Make](#44-make)
|
||||
* [4.5 Keeping your dependencies up to date](#45-keeping-your-dependencies-up-to-date)
|
||||
* [4.6 To contribute changes](#46-to-contribute-changes)
|
||||
* [4.7 How To Report A Bug](#47-how-to-report-a-bug)
|
||||
* [4.8 How To Suggest A Feature Or Enhancement](#48-how-to-suggest-a-feature-or-enhancement)
|
||||
* [5. Code Review Process](#5-code-review-process)
|
||||
* [5.1 Issues](#51-issues)
|
||||
* [5.2 Pull Requests](#52-pull-requests)
|
||||
* [5.3 Differences between "new features" and "improvements"](#53-differences-between-new-features-and-improvements)
|
||||
* [6. Community](#6-community)
|
||||
|
||||
### Why do these guidelines exist?
|
||||
# 1. Introduction
|
||||
**Welcome!** First off, thank you for contributing to the further development of Red. We're always looking for new ways to improve our project and we appreciate any help you can give us.
|
||||
|
||||
### 1.1 Why do these guidelines exist?
|
||||
Red is an open source project. This means that each and every one of the developers and contributors who have helped make Red what it is today have done so by volunteering their time and effort. It takes a lot of time to coordinate and organize issues and new features and to review and test pull requests. By following these guidelines you will help the developers streamline the contribution process and save them time. In doing so we hope to get back to each and every issue and pull request in a timely manner.
|
||||
|
||||
### What kinds of contributions are we looking for?
|
||||
### 1.2 What kinds of contributions are we looking for?
|
||||
We love receiving contributions from our community. Any assistance you can provide with regards to bug fixes, feature enhancements, and documentation is more than welcome.
|
||||
|
||||
# Ground Rules
|
||||
# 2. Ground Rules
|
||||
We've made a point to use [ZenHub](https://www.zenhub.com/) (a plugin for GitHub) as our main source of collaboration and coordination. Your experience contributing to Red will be greatly improved if you go get that plugin.
|
||||
1. Ensure cross compatibility for Windows, Mac OS and Linux.
|
||||
2. Ensure all Python features used in contributions exist and work in Python 3.5 and above.
|
||||
2. Ensure all Python features used in contributions exist and work in Python 3.6 and above.
|
||||
3. Create new tests for code you add or bugs you fix. It helps us help you by making sure we don't accidentally break anything :grinning:
|
||||
4. Create any issues for new features you'd like to implement and explain why this feature is useful to everyone and not just you personally.
|
||||
5. Don't add new cogs unless specifically given approval in an issue discussing said cog idea.
|
||||
6. Be welcoming to newcomers and encourage diverse new contributors from all backgrounds. See [Python Community Code of Conduct](https://www.python.org/psf/codeofconduct/).
|
||||
|
||||
# Your First Contribution
|
||||
# 3. Your First Contribution
|
||||
Unsure of how to get started contributing to Red? Please take a look at the Issues section of this repo and sort by the following labels:
|
||||
|
||||
* beginner - issues that can normally be fixed in just a few lines of code and maybe a test or two.
|
||||
@@ -27,35 +47,87 @@ Unsure of how to get started contributing to Red? Please take a look at the Issu
|
||||
|
||||
At this point you're ready to start making changes. Feel free to ask for help; everyone was a beginner at some point!
|
||||
|
||||
# Getting Started
|
||||
### Testing
|
||||
We've recently started adding unit-testing into Red. All current tests can be found in the `tests/` directory at the root level of the repository. You will need `py.test` installed in order to run them (which is already in `requirement.txt`). Tests can be run by simply calling `pytest` once you've `cd`'d into the Red repository folder.
|
||||
# 4. Getting Started
|
||||
|
||||
### To contribute changes
|
||||
1. Create your own fork of the Red repository.
|
||||
2. Make the changes in your own fork.
|
||||
Red's repository is configured to follow a particular development workflow, using various reputable tools. We kindly ask that you stick to this workflow when contributing to Red, by following the guides below. This will help you to easily produce quality code, identify errors early, and streamline the code review process.
|
||||
|
||||
### 4.1 Setting up your development environment
|
||||
The following requirements must be installed prior to setting up:
|
||||
- Python 3.6
|
||||
- git
|
||||
- pip
|
||||
- pipenv
|
||||
|
||||
If you're not on Windows, you can optionally install [pyenv](https://github.com/pyenv/pyenv), which will help you run tests for different python versions.
|
||||
|
||||
1. Fork and clone the repository to a directory on your local machine.
|
||||
2. Open a command line in that directory and execute the following commands:
|
||||
```bash
|
||||
pip install pipenv
|
||||
pipenv install --dev
|
||||
```
|
||||
Red, its dependencies, and all required development tools, are now installed to a virtual environment. Red is installed in editable mode, meaning that edits you make to the source code in the repository will be reflected when you run Red.
|
||||
3. Activate the new virtual environment with the command:
|
||||
```bash
|
||||
pipenv shell
|
||||
```
|
||||
From here onwards, we will assume you are executing commands from within this shell. Each time you open a new command line, you should execute this command first.
|
||||
|
||||
Note: If you haven't used `pipenv` before but are comfortable with virtualenvs, just run `pip install pipenv` in the virtualenv you're already using and invoke the command above from the cloned Red repo. It will do the correct thing.
|
||||
|
||||
### 4.2 Testing
|
||||
We've recently started using [tox](https://github.com/tox-dev/tox) to run all of our tests. It's extremely simple to use, and if you followed the previous section correctly, it is already installed to your virtual environment.
|
||||
|
||||
Currently, tox does the following, creating its own virtual environments for each stage:
|
||||
- Runs all of our unit tests with [pytest](https://github.com/pytest-dev/pytest) on python 3.6 (test environment `py36`)
|
||||
- Ensures documentation builds without warnings, and all hyperlinks have a valid destination (test environment `docs`)
|
||||
- Ensures that the code meets our style guide with [black](https://github.com/ambv/black) (test environment `style`)
|
||||
|
||||
To run all of these tests, just run the command `tox` in the project directory.
|
||||
|
||||
To run a subset of these tests, use the command `tox -e <env>`, where `<env>` is the test environment you want tox to run. The test environments are noted in the dot points above.
|
||||
|
||||
Your PR will not be merged until all of these tests pass.
|
||||
|
||||
### 4.3 Style
|
||||
Our style checker of choice, [black](https://github.com/ambv/black), actually happens to be an auto-formatter. The checking functionality simply detects whether or not it would try to reformat something in your code, should you run the formatter on it. For this reason, we recommend using this tool as a formatter, regardless of any disagreements you might have with the style it enforces.
|
||||
|
||||
Use the command `black --help` to see how to use this tool. The full style guide is explained in detail on [black's GitHub repository](https://github.com/ambv/black). **There is one exception to this**, however, which is that we set the line length to 99, instead of black's default 88. When using `black` on the command line, simply use it like so: `black -l 99 <src>`.
|
||||
|
||||
### 4.4 Make
|
||||
You may have noticed we have a `Makefile` and a `make.bat` in the top-level directory. For now, you can do two things with them:
|
||||
1. `make reformat`: Reformat all python files in the project with Black
|
||||
2. `make stylecheck`: Check if any `.py` files in the project need reformatting
|
||||
|
||||
### 4.5 Keeping your dependencies up to date
|
||||
Whenever you pull from upstream (V3/develop on the main repository) and you notice the file `Pipfile.lock` has been changed, it usually means one of the package dependencies have been updated, added or removed. To make sure you're testing and formatting with the most up-to-date versions of our dependencies, run `pipenv install --dev` again.
|
||||
|
||||
### 4.6 To contribute changes
|
||||
|
||||
1. Create a new branch on your fork
|
||||
2. Make the changes
|
||||
3. If you like the changes and think the main Red project could use it:
|
||||
* Ensure your code follows (generally) the PEP8 Python style guide
|
||||
* Run tests with `tox` to ensure your code is up to scratch
|
||||
* Create a Pull Request on GitHub with your changes
|
||||
|
||||
### How To Report A Bug
|
||||
### 4.7 How To Report A Bug
|
||||
Please see our **ISSUES.MD** for more information.
|
||||
|
||||
### How To Suggest A Feature Or Enhancement
|
||||
### 4.8 How To Suggest A Feature Or Enhancement
|
||||
The goal of Red is to be as useful to as many people as possible, this means that all features must be useful to anyone and any server that uses Red.
|
||||
|
||||
If you find yourself wanting a feature that Red does not already have, you're probably not alone. There's bound to be a great number of users out there needing the same thing and a lot of the features that Red has today have been added because of the needs of our users. Open an issue on our issues list and describe the feature you would like to see, how you would use it, how it should work, and why it would be useful to the Red community as a whole.
|
||||
|
||||
# Code Review Process
|
||||
# 5. Code Review Process
|
||||
|
||||
We have a core team working tirelessly to implement new features and fix bugs for the Red community. This core team looks at and evaluates new issues and PRs on a daily basis.
|
||||
|
||||
The decisions we make are based on a simple majority of that team or by decree of the project owner.
|
||||
|
||||
### Issues
|
||||
### 5.1 Issues
|
||||
Any new issues will be looked at and evaluated for validity of a bug or for the usefulness of a suggested feature. If we have questions about your issue we will get back as soon as we can (usually in a day or two) and will try to make a decision within a week.
|
||||
|
||||
### Pull Requests
|
||||
### 5.2 Pull Requests
|
||||
Pull requests are evaluated by their quality and how effectively they solve their corresponding issue. The process for reviewing pull requests is as follows:
|
||||
|
||||
1. A pull request is submitted
|
||||
@@ -66,10 +138,10 @@ Pull requests are evaluated by their quality and how effectively they solve thei
|
||||
4. If any feedback is given we expect a response within 1 week or we may decide to close the PR.
|
||||
5. If your pull request is not vetoed and no core member requests changes then it will be approved and merged into the project.
|
||||
|
||||
### Differences between "new features" and "improvements"
|
||||
### 5.3 Differences between "new features" and "improvements"
|
||||
The difference between a new feature and improvement can be quite fuzzy and the project owner reserves all rights to decide under which category your PR falls.
|
||||
|
||||
At a very basic level a PR is a new feature if it changes the intended way any part of the Red project currently works or if it modifies the user experience (UX) in any significant way. Otherwise, it is likely to be considered an improvement.
|
||||
|
||||
# Community
|
||||
# 6. Community
|
||||
You can chat with the core team and other community members about issues or pull requests in the #coding channel of the Red support server located [here](https://discord.gg/red).
|
||||
|
||||
25
.github/ISSUE_TEMPLATE/command_bug.md
vendored
Normal file
25
.github/ISSUE_TEMPLATE/command_bug.md
vendored
Normal file
@@ -0,0 +1,25 @@
|
||||
# Command bugs
|
||||
|
||||
<!--
|
||||
Did you find a bug with a command? Fill out the following:
|
||||
-->
|
||||
|
||||
#### Command name
|
||||
|
||||
<!-- Replace this line with the name of the command -->
|
||||
|
||||
#### What cog is this command from?
|
||||
|
||||
<!-- Replace this line with the name of the cog -->
|
||||
|
||||
#### What were you expecting to happen?
|
||||
|
||||
<!-- Replace this line with a description of what you were expecting to happen -->
|
||||
|
||||
#### What actually happened?
|
||||
|
||||
<!-- Replace this line with a description of what actually happened. Include any error messages -->
|
||||
|
||||
#### How can we reproduce this issue?
|
||||
|
||||
<!-- Replace with numbered steps to reproduce the issue -->
|
||||
35
.github/ISSUE_TEMPLATE/feature_req.md
vendored
Normal file
35
.github/ISSUE_TEMPLATE/feature_req.md
vendored
Normal file
@@ -0,0 +1,35 @@
|
||||
# Feature request
|
||||
|
||||
<!-- This template is for feature requests. Please fill out the following: -->
|
||||
|
||||
|
||||
#### Select the type of feature you are requesting:
|
||||
|
||||
<!-- To check a box, replace the space between the [] with a x -->
|
||||
|
||||
- [ ] Cog
|
||||
- [ ] Command
|
||||
- [ ] API functionality
|
||||
|
||||
#### Describe your requested feature
|
||||
|
||||
<!--
|
||||
Feel free to describe in as much detail as you wish.
|
||||
|
||||
If you are requesting a cog to be included in core:
|
||||
- Describe the functionality in as much detail as possible
|
||||
- Include the command structure, if possible
|
||||
- Please note that unless it's something that should be core functionality,
|
||||
we reserve the right to reject your suggestion and point you to our cog
|
||||
board to request it for a third-party cog
|
||||
|
||||
If you are requesting a command:
|
||||
- Include what cog it should be in and a name for the command
|
||||
- Describe the intended functionality for the command
|
||||
- Note any restrictions on who can use the command or where it can be used
|
||||
|
||||
If you are requesting API functionality:
|
||||
- Describe what it should do
|
||||
- Note whether it is to extend existing functionality or introduce new functionality
|
||||
|
||||
-->
|
||||
21
.github/ISSUE_TEMPLATE/other_bug.md
vendored
Normal file
21
.github/ISSUE_TEMPLATE/other_bug.md
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
# Other bugs
|
||||
|
||||
<!--
|
||||
Did you find a bug with something other than a command? Fill out the following:
|
||||
-->
|
||||
|
||||
#### What were you trying to do?
|
||||
|
||||
<!-- Replace this line with a description of what you were trying to do -->
|
||||
|
||||
#### What were you expecting to happen?
|
||||
|
||||
<!-- Replace this line with a description of what you were expecting to happen -->
|
||||
|
||||
#### What actually happened?
|
||||
|
||||
<!-- Replace this line with a description of what actually happened. Include any error messages -->
|
||||
|
||||
#### How can we reproduce this issue?
|
||||
|
||||
<!-- Replace with numbered steps to reproduce the issue -->
|
||||
14
.github/PULL_REQUEST_TEMPLATE/bugfix.md
vendored
Normal file
14
.github/PULL_REQUEST_TEMPLATE/bugfix.md
vendored
Normal file
@@ -0,0 +1,14 @@
|
||||
# Bugfix request
|
||||
|
||||
<!--
|
||||
To be used for pull requests that fix a bug
|
||||
-->
|
||||
|
||||
#### Describe the bug being fixed
|
||||
|
||||
<!--
|
||||
If an issue exists for the bug, mention
|
||||
that this PR fixes that issue
|
||||
-->
|
||||
|
||||
#### Anything we need to know about this fix?
|
||||
20
.github/PULL_REQUEST_TEMPLATE/enhancement.md
vendored
Normal file
20
.github/PULL_REQUEST_TEMPLATE/enhancement.md
vendored
Normal file
@@ -0,0 +1,20 @@
|
||||
# Enhancement request
|
||||
|
||||
<!--
|
||||
To be used for PRs which enhance existing features
|
||||
-->
|
||||
|
||||
#### Describe the enhancement
|
||||
|
||||
<!--
|
||||
Describe what your changes do.
|
||||
If adding commands, describe any restrictions on their usage.
|
||||
- For example, who can use the command? Where can it be used?
|
||||
-->
|
||||
|
||||
#### Does this enhancement break existing functionality?
|
||||
|
||||
<!-- To check a box, replace the space between the [] with a x -->
|
||||
|
||||
- [ ] Yes
|
||||
- [ ] No
|
||||
21
.github/PULL_REQUEST_TEMPLATE/new_feature.md
vendored
Normal file
21
.github/PULL_REQUEST_TEMPLATE/new_feature.md
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
# New feature addition
|
||||
|
||||
<!--
|
||||
To be used for PRs which add a new feature
|
||||
Examples of this include new APIs, new core cogs, etc.
|
||||
-->
|
||||
|
||||
#### What type of feature is this?
|
||||
|
||||
<!-- To check a box, replace the space between the [] with a x -->
|
||||
|
||||
- [ ] New core cog
|
||||
- [ ] New API
|
||||
- [ ] Other
|
||||
|
||||
#### Describe the feature
|
||||
|
||||
<!--
|
||||
If you are adding a cog, describe its commands in detail (functionality, usage restrictions, etc).
|
||||
If the new feature introduces new requirements, please try to explain why they are necessary.
|
||||
-->
|
||||
16
.github/PULL_REQUEST_TEMPLATE/release.md
vendored
Normal file
16
.github/PULL_REQUEST_TEMPLATE/release.md
vendored
Normal file
@@ -0,0 +1,16 @@
|
||||
# New release
|
||||
|
||||
<!--
|
||||
To be used by collaborators for doing releases.
|
||||
Most contributors will not need to use this.
|
||||
-->
|
||||
|
||||
#### Version
|
||||
|
||||
|
||||
|
||||
#### Has a draft release been created for this?
|
||||
|
||||
- [ ] Yes
|
||||
- [ ] No
|
||||
|
||||
5
.github/PULL_REQUEST_TEMPLATE/translations.md
vendored
Normal file
5
.github/PULL_REQUEST_TEMPLATE/translations.md
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
# Translations update
|
||||
|
||||
<!--
|
||||
Used for PRs updating translations from Crowdin
|
||||
-->
|
||||
29
.gitignore
vendored
29
.gitignore
vendored
@@ -1,40 +1,15 @@
|
||||
# Trivia list repo injection
|
||||
redbot/trivia/
|
||||
|
||||
*.json
|
||||
*.exe
|
||||
*.dll
|
||||
.data
|
||||
!/tests/cogs/dataconverter/data/**/*.json
|
||||
|
||||
### JetBrains template
|
||||
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm
|
||||
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
|
||||
|
||||
# User-specific stuff:
|
||||
.idea/**/workspace.xml
|
||||
.idea/**/tasks.xml
|
||||
.idea/dictionaries
|
||||
|
||||
# Sensitive or high-churn files:
|
||||
.idea/**/dataSources/
|
||||
.idea/**/dataSources.ids
|
||||
.idea/**/dataSources.xml
|
||||
.idea/**/dataSources.local.xml
|
||||
.idea/**/sqlDataSources.xml
|
||||
.idea/**/dynamic.xml
|
||||
.idea/**/uiDesigner.xml
|
||||
|
||||
# Gradle:
|
||||
.idea/**/gradle.xml
|
||||
.idea/**/libraries
|
||||
|
||||
# CMake
|
||||
cmake-build-debug/
|
||||
|
||||
# Mongo Explorer plugin:
|
||||
.idea/**/mongoSettings.xml
|
||||
|
||||
## File-based project format:
|
||||
.idea/
|
||||
*.iws
|
||||
|
||||
## Plugin-specific files:
|
||||
|
||||
@@ -10,5 +10,4 @@ python:
|
||||
version: 3.6
|
||||
pip_install: true
|
||||
extra_requirements:
|
||||
- docs
|
||||
- mongo
|
||||
- mongo
|
||||
|
||||
32
.travis.yml
32
.travis.yml
@@ -5,21 +5,29 @@ notifications:
|
||||
email: false
|
||||
|
||||
python:
|
||||
- 3.5.3
|
||||
- 3.6.1
|
||||
- 3.6.5
|
||||
env:
|
||||
global:
|
||||
PIPENV_IGNORE_VIRTUALENVS=1
|
||||
matrix:
|
||||
- TOXENV=py
|
||||
- TOXENV=docs
|
||||
- TOXENV=style
|
||||
|
||||
install:
|
||||
- echo "git+https://github.com/Rapptz/discord.py.git@rewrite#egg=discord.py[voice]" >> requirements.txt
|
||||
- pip install -r requirements.txt
|
||||
- pip install .[test]
|
||||
- pip install --upgrade pip pipenv
|
||||
- pipenv install --dev
|
||||
|
||||
script:
|
||||
- python -m compileall ./redbot/cogs
|
||||
- python -m pytest
|
||||
- pipenv run tox
|
||||
|
||||
jobs:
|
||||
include:
|
||||
|
||||
# These jobs only occur on tag creation for V3/develop if the prior ones succeed
|
||||
- stage: PyPi Deployment
|
||||
if: tag IS present
|
||||
python: 3.5.3
|
||||
python: 3.6.5
|
||||
env:
|
||||
- DEPLOYING=true
|
||||
deploy:
|
||||
@@ -31,14 +39,14 @@ jobs:
|
||||
on:
|
||||
repo: Cog-Creators/Red-DiscordBot
|
||||
branch: V3/develop
|
||||
python: 3.5.3
|
||||
python: 3.6.5
|
||||
tags: true
|
||||
- stage: Crowdin Deployment
|
||||
if: tag IS present
|
||||
python: 3.5.3
|
||||
python: 3.6.5
|
||||
env:
|
||||
- DEPLOYING=true
|
||||
before_deployment:
|
||||
before_deploy:
|
||||
- curl https://artifacts.crowdin.com/repo/GPG-KEY-crowdin | sudo apt-key add -
|
||||
- echo "deb https://artifacts.crowdin.com/repo/deb/ /" | sudo tee -a /etc/apt/sources.list
|
||||
- sudo apt-get update -qq
|
||||
@@ -50,5 +58,5 @@ jobs:
|
||||
on:
|
||||
repo: Cog-Creators/Red-DiscordBot
|
||||
branch: V3/develop
|
||||
python: 3.5.3
|
||||
python: 3.6.5
|
||||
tags: true
|
||||
|
||||
4
Makefile
Normal file
4
Makefile
Normal file
@@ -0,0 +1,4 @@
|
||||
reformat:
|
||||
black -l 99 `git ls-files "*.py"`
|
||||
stylecheck:
|
||||
black --check -l 99 `git ls-files "*.py"`
|
||||
20
Pipfile
Normal file
20
Pipfile
Normal file
@@ -0,0 +1,20 @@
|
||||
[[source]]
|
||||
url = "https://pypi.org/simple"
|
||||
verify_ssl = true
|
||||
name = "pypi"
|
||||
|
||||
[packages]
|
||||
"discord.py" = { git = 'git://github.com/Rapptz/discord.py', ref = '7eb918b19e3e60b56eb9039eb267f8f3477c5e17', editable = true}
|
||||
"e1839a8" = {path = ".", editable = true}
|
||||
|
||||
[dev-packages]
|
||||
tox = "*"
|
||||
pytest = "*"
|
||||
pytest-asyncio = "*"
|
||||
sphinx = ">1.7"
|
||||
sphinxcontrib-asyncio = "*"
|
||||
sphinx-rtd-theme = "*"
|
||||
black = "*"
|
||||
|
||||
[pipenv]
|
||||
allow_prereleases = true
|
||||
452
Pipfile.lock
generated
Normal file
452
Pipfile.lock
generated
Normal file
@@ -0,0 +1,452 @@
|
||||
{
|
||||
"_meta": {
|
||||
"hash": {
|
||||
"sha256": "dcd688e81a2d0e793236e0335eb7cb9558d8b4acb66934afffcc0612cce2ec53"
|
||||
},
|
||||
"pipfile-spec": 6,
|
||||
"requires": {},
|
||||
"sources": [
|
||||
{
|
||||
"name": "pypi",
|
||||
"url": "https://pypi.org/simple",
|
||||
"verify_ssl": true
|
||||
}
|
||||
]
|
||||
},
|
||||
"default": {
|
||||
"aiohttp": {
|
||||
"hashes": [
|
||||
"sha256:129d83dd067760cec3cfd4456b5c6d7ac29f2c639d856884568fd539bed5a51f",
|
||||
"sha256:33c62afd115c456b0cf1e890fe6753055effe0f31a28321efd4f787378d6f4ab",
|
||||
"sha256:666756e1d4cf161ed1486b82f65fdd386ac07dd20fb10f025abf4be54be12746",
|
||||
"sha256:9705ded5a0faa25c8f14c6afb7044002d66c9120ed7eadb4aa9ca4aad32bd00c",
|
||||
"sha256:af5bfdd164256118a0a306b3f7046e63207d1f8cba73a67dcc0bd858dcfcd3bc",
|
||||
"sha256:b80f44b99fa3c9b4530fcfa324a99b84843043c35b084e0b653566049974435d",
|
||||
"sha256:c67e105ec74b85c8cb666b6877569dee6f55b9548f982983b9bee80b3d47e6f3",
|
||||
"sha256:d15c6658de5b7783c2538407278fa062b079a46d5f814a133ae0f09bbb2cfbc4",
|
||||
"sha256:d611ebd1ef48498210b65486306e065fde031040a1f3c455ca1b6baa7bf32ad3",
|
||||
"sha256:dcc7e4dcec6b0012537b9f8a0726f8b111188894ab0f924b680d40b13d3298a0",
|
||||
"sha256:de8ef106e130b94ca143fdfc6f27cda1d8ba439462542377738af4d99d9f5dd2",
|
||||
"sha256:eb6f1405b607fff7e44168e3ceb5d3c8a8c5a2d3effe0a27f843b16ec047a6d7",
|
||||
"sha256:f0e2ac69cb709367400008cebccd5d48161dd146096a009a632a132babe5714c"
|
||||
],
|
||||
"version": "==2.2.5"
|
||||
},
|
||||
"aiohttp-json-rpc": {
|
||||
"hashes": [
|
||||
"sha256:9ec69ea70ce49c4af445f0ac56ac728708ccfad8b214272d2cc7e75bc0b31327",
|
||||
"sha256:e2b8b49779d5d9b811f3a94e98092b1fa14af6d9adbf71c3afa6b20c641fa5d5"
|
||||
],
|
||||
"version": "==0.8.7"
|
||||
},
|
||||
"appdirs": {
|
||||
"hashes": [
|
||||
"sha256:9e5896d1372858f8dd3344faf4e5014d21849c756c8d5701f78f8a103b372d92",
|
||||
"sha256:d8b24664561d0d34ddfaec54636d502d7cea6e29c3eaf68f3df6180863e2166e"
|
||||
],
|
||||
"version": "==1.4.3"
|
||||
},
|
||||
"async-timeout": {
|
||||
"hashes": [
|
||||
"sha256:00cff4d2dce744607335cba84e9929c3165632da2d27970dbc55802a0c7873d0",
|
||||
"sha256:9093db5b8ddbe4b8f6885d1a6e0ad84ae3155464cbf6877c387605244c285f3c"
|
||||
],
|
||||
"version": "==2.0.1"
|
||||
},
|
||||
"chardet": {
|
||||
"hashes": [
|
||||
"sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae",
|
||||
"sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691"
|
||||
],
|
||||
"version": "==3.0.4"
|
||||
},
|
||||
"colorama": {
|
||||
"hashes": [
|
||||
"sha256:463f8483208e921368c9f306094eb6f725c6ca42b0f97e313cb5d5512459feda",
|
||||
"sha256:48eb22f4f8461b1df5734a074b57042430fb06e1d61bd1e11b078c0fe6d7a1f1"
|
||||
],
|
||||
"version": "==0.3.9"
|
||||
},
|
||||
"discord.py": {
|
||||
"editable": true,
|
||||
"git": "git://github.com/Rapptz/discord.py",
|
||||
"ref": "7eb918b19e3e60b56eb9039eb267f8f3477c5e17"
|
||||
},
|
||||
"distro": {
|
||||
"hashes": [
|
||||
"sha256:224041cef9600e72d19ae41ba006e71c05c4dc802516da715d7fda55ba3d8742",
|
||||
"sha256:6ec8e539cf412830e5ccf521aecf879f2c7fcf60ce446e33cd16eef1ed8a0158"
|
||||
],
|
||||
"version": "==1.3.0"
|
||||
},
|
||||
"e1839a8": {
|
||||
"editable": true,
|
||||
"path": "."
|
||||
},
|
||||
"fuzzywuzzy": {
|
||||
"hashes": [
|
||||
"sha256:d40c22d2744dff84885b30bbfc07fab7875f641d070374331777a4d1808b8d4e",
|
||||
"sha256:ecf490216fb4d76b558a03042ff8f45a8782f17326caca1384d834cbaa2c7e6f"
|
||||
],
|
||||
"version": "==0.16.0"
|
||||
},
|
||||
"idna": {
|
||||
"hashes": [
|
||||
"sha256:2c6a5de3089009e3da7c5dde64a141dbc8551d5b7f6cf4ed7c2568d0cc520a8f",
|
||||
"sha256:8c7309c718f94b3a625cb648ace320157ad16ff131ae0af362c9f21b80ef6ec4"
|
||||
],
|
||||
"version": "==2.6"
|
||||
},
|
||||
"multidict": {
|
||||
"hashes": [
|
||||
"sha256:1a1d76374a1e7fe93acef96b354a03c1d7f83e7512e225a527d283da0d7ba5e0",
|
||||
"sha256:1d6e191965505652f194bc4c40270a842922685918a4f45e6936a6b15cc5816d",
|
||||
"sha256:295961a6a88f1199e19968e15d9b42f3a191c89ec13034dbc212bf9c394c3c82",
|
||||
"sha256:2be5af084de6c3b8e20d6421cb0346378a9c867dcf7c86030d6b0b550f9888e4",
|
||||
"sha256:2eb99617c7a0e9f2b90b64bc1fb742611718618572747d6f3d6532b7b78755ab",
|
||||
"sha256:4ba654c6b5ad1ae4a4d792abeb695b29ce981bb0f157a41d0fd227b385f2bef0",
|
||||
"sha256:5ba766433c30d703f6b2c17eb0b6826c6f898e5f58d89373e235f07764952314",
|
||||
"sha256:a59d58ee85b11f337b54933e8d758b2356fcdcc493248e004c9c5e5d11eedbe4",
|
||||
"sha256:a6e35d28900cf87bcc11e6ca9e474db0099b78f0be0a41d95bef02d49101b5b2",
|
||||
"sha256:b4df7ca9c01018a51e43937eaa41f2f5dce17a6382fda0086403bcb1f5c2cf8e",
|
||||
"sha256:bbd5a6bffd3ba8bfe75b16b5e28af15265538e8be011b0b9fddc7d86a453fd4a",
|
||||
"sha256:d870f399fcd58a1889e93008762a3b9a27cf7ea512818fc6e689f59495648355",
|
||||
"sha256:e9404e2e19e901121c3c5c6cffd5a8ae0d1d67919c970e3b3262231175713068"
|
||||
],
|
||||
"version": "==4.3.1"
|
||||
},
|
||||
"python-levenshtein": {
|
||||
"hashes": [
|
||||
"sha256:033a11de5e3d19ea25c9302d11224e1a1898fe5abd23c61c7c360c25195e3eb1"
|
||||
],
|
||||
"version": "==0.12.0"
|
||||
},
|
||||
"pyyaml": {
|
||||
"hashes": [
|
||||
"sha256:0c507b7f74b3d2dd4d1322ec8a94794927305ab4cebbe89cc47fe5e81541e6e8",
|
||||
"sha256:16b20e970597e051997d90dc2cddc713a2876c47e3d92d59ee198700c5427736",
|
||||
"sha256:3262c96a1ca437e7e4763e2843746588a965426550f3797a79fca9c6199c431f",
|
||||
"sha256:326420cbb492172dec84b0f65c80942de6cedb5233c413dd824483989c000608",
|
||||
"sha256:4474f8ea030b5127225b8894d626bb66c01cda098d47a2b0d3429b6700af9fd8",
|
||||
"sha256:592766c6303207a20efc445587778322d7f73b161bd994f227adaa341ba212ab",
|
||||
"sha256:5ac82e411044fb129bae5cfbeb3ba626acb2af31a8d17d175004b70862a741a7",
|
||||
"sha256:5f84523c076ad14ff5e6c037fe1c89a7f73a3e04cf0377cb4d017014976433f3",
|
||||
"sha256:827dc04b8fa7d07c44de11fabbc888e627fa8293b695e0f99cb544fdfa1bf0d1",
|
||||
"sha256:b4c423ab23291d3945ac61346feeb9a0dc4184999ede5e7c43e1ffb975130ae6",
|
||||
"sha256:bc6bced57f826ca7cb5125a10b23fd0f2fff3b7c4701d64c439a300ce665fff8",
|
||||
"sha256:c01b880ec30b5a6e6aa67b09a2fe3fb30473008c85cd6a67359a1b15ed6d83a4",
|
||||
"sha256:ca233c64c6e40eaa6c66ef97058cdc80e8d0157a443655baa1b2966e812807ca",
|
||||
"sha256:e863072cdf4c72eebf179342c94e6989c67185842d9997960b3e69290b2fa269"
|
||||
],
|
||||
"version": "==3.12"
|
||||
},
|
||||
"raven": {
|
||||
"hashes": [
|
||||
"sha256:0adae40e004dfe2181d1f2883aa3d4ca1cf16dbe449ae4b445b011c6eb220a90",
|
||||
"sha256:84da75114739191bdf2388f296ffd6177e83567a7fbaf2701e034ad6026e4f3b"
|
||||
],
|
||||
"version": "==6.5.0"
|
||||
},
|
||||
"red-trivia": {
|
||||
"hashes": [
|
||||
"sha256:39413b9fb3f9b9362d6de1dcf69a4bf635b0f3518243f7178299b96d26cbb6a7"
|
||||
],
|
||||
"version": "==1.1.1"
|
||||
},
|
||||
"websockets": {
|
||||
"hashes": [
|
||||
"sha256:09dfec40e9b73e8808c39ecdbc1733e33915a2b26b90c54566afc0af546a9ec3",
|
||||
"sha256:2aa6d52264cecb08d39741e8fda49f5ac4872aef02617230c84d02e861f3cc5a",
|
||||
"sha256:2f5b7f3920f29609086fb0b63552bb1f86a04b8cbdcc0dbf3775cc90d489dfc8",
|
||||
"sha256:3d38f76f71654268e5533b45df125ff208fee242a102d4b5ca958da5cf5fb345",
|
||||
"sha256:3fcc7dfb365e81ff8206f950c86d1e73accdf3be2f9110c0cb73be32d2e7a9a5",
|
||||
"sha256:4128212ab6f91afda03a0c697add261bdf6946b47928db83f07298ea2cd8d937",
|
||||
"sha256:43e5b9f51dd0000a4c6f646e2ade0c886bd14a784ffac08b9e079bd17a63bcc5",
|
||||
"sha256:4a932c17cb11c361c286c04842dc2385cc7157019bbba8b64808acbc89a95584",
|
||||
"sha256:5ddc5fc121eb76771e990f071071d9530e27d20e8cfb804d9f5823de055837af",
|
||||
"sha256:7347af28fcc70eb45be409760c2a428f8199e7f73c04a621916c3c219ed7ad27",
|
||||
"sha256:85ae1e4b36aa2e90de56d211d2de36d7c093d00277a9afdd9b4f81e69c0214ab",
|
||||
"sha256:8a29100079f5b91a72bcd25d35a7354db985d3babae42d00b9d629f9a0aaa8ac",
|
||||
"sha256:a7e7585c8e3c0f9277ad7d6ee6ccddc69649cd216255d5e255d68f90482aeefa",
|
||||
"sha256:aa42ecef3aed807e23218c264b1e82004cdd131a6698a10b57fc3d8af8f651fc",
|
||||
"sha256:b19e7ede1ba80ee9de6f5b8ccd31beee25402e68bef7c13eeb0b8bc46bc4b7b7",
|
||||
"sha256:c4c5b5ce2d66cb0cf193c14bc9726adca095febef0f7b2c04e5e3fa3487a97a4",
|
||||
"sha256:de743ef26b002efceea7d7756e99e5d38bf5d4f27563b8d27df2a9a5cc57340a",
|
||||
"sha256:e1e568136ad5cb6768504be36d470a136b072acbf3ea882303aee6361be01941",
|
||||
"sha256:e8992f1db371f2a1c5af59e032d9dc7c1aa92f16241efcda695b7d955b4de0c2",
|
||||
"sha256:e9c1cdbb591432c59d0b5ca64fd30b6d517024767f152fc169563b26e7bcc9da"
|
||||
],
|
||||
"version": "==3.4"
|
||||
},
|
||||
"yarl": {
|
||||
"hashes": [
|
||||
"sha256:605480ee43eead69ec8e8c52cdfefc79cef6379cc0e87d908cf290408c1e49af",
|
||||
"sha256:7fad2530cb4ddf2b74c1e4f6f9f0e28eac482094c6542f98fd71ecf67fb4fded",
|
||||
"sha256:837d866a70f1ea03005914a740bddea89a253afabd6589db981b91738768bd25",
|
||||
"sha256:885e40812ff9fc80e6f28ef04ad6396e3ae583ab504b1a76301fdcec7fc9f30f",
|
||||
"sha256:a5457e075eab1170141774a8c69906c223ea0088eaebd6ef91b04b33527fa905",
|
||||
"sha256:baa0d3f7982fa0c03a55433109c405e79a597141f2e2d6ee7e16c03eabd74886",
|
||||
"sha256:beeefbe0edd47fc8b657bf7bf44791f7a6e5b14f3de1846daf999687cb68c156",
|
||||
"sha256:cf6a3d6fd3e79d3457d520c12d5d18b030d5ca5d0b205ca6481857804d8d944d",
|
||||
"sha256:d07d3dc6849345b7437dc58ea49ad2a1960017386d86288550728ca38e482ddc",
|
||||
"sha256:d81e45bedefccb97e4e8f7d32cfae0af1d9eadd1ae795fc420c8319c3dab2a28",
|
||||
"sha256:e1da2853a92fbc7e2d0248bbfa931cd621121e70ce6dda7c1eeef3516d51b46c",
|
||||
"sha256:f1201de3e93fb1efc3111c8928d9366875edefd65d77c0f6b847fe299e8e1122",
|
||||
"sha256:fe0390a29b5c7e90975feefe863e3d3a851be546bd797b23f338d24a15efa920"
|
||||
],
|
||||
"version": "==0.18.0"
|
||||
}
|
||||
},
|
||||
"develop": {
|
||||
"alabaster": {
|
||||
"hashes": [
|
||||
"sha256:2eef172f44e8d301d25aff8068fddd65f767a3f04b5f15b0f4922f113aa1c732",
|
||||
"sha256:37cdcb9e9954ed60912ebc1ca12a9d12178c26637abdf124e3cde2341c257fe0"
|
||||
],
|
||||
"version": "==0.7.10"
|
||||
},
|
||||
"appdirs": {
|
||||
"hashes": [
|
||||
"sha256:9e5896d1372858f8dd3344faf4e5014d21849c756c8d5701f78f8a103b372d92",
|
||||
"sha256:d8b24664561d0d34ddfaec54636d502d7cea6e29c3eaf68f3df6180863e2166e"
|
||||
],
|
||||
"version": "==1.4.3"
|
||||
},
|
||||
"atomicwrites": {
|
||||
"hashes": [
|
||||
"sha256:240831ea22da9ab882b551b31d4225591e5e447a68c5e188db5b89ca1d487585",
|
||||
"sha256:a24da68318b08ac9c9c45029f4a10371ab5b20e4226738e150e6e7c571630ae6"
|
||||
],
|
||||
"version": "==1.1.5"
|
||||
},
|
||||
"attrs": {
|
||||
"hashes": [
|
||||
"sha256:4b90b09eeeb9b88c35bc642cbac057e45a5fd85367b985bd2809c62b7b939265",
|
||||
"sha256:e0d0eb91441a3b53dab4d9b743eafc1ac44476296a2053b6ca3af0b139faf87b"
|
||||
],
|
||||
"version": "==18.1.0"
|
||||
},
|
||||
"babel": {
|
||||
"hashes": [
|
||||
"sha256:6778d85147d5d85345c14a26aada5e478ab04e39b078b0745ee6870c2b5cf669",
|
||||
"sha256:8cba50f48c529ca3fa18cf81fa9403be176d374ac4d60738b839122dfaaa3d23"
|
||||
],
|
||||
"version": "==2.6.0"
|
||||
},
|
||||
"black": {
|
||||
"hashes": [
|
||||
"sha256:3efe92eafbde15f8ac06478de11cfb84e47504896ccdde64507e751d2f91ec3a",
|
||||
"sha256:fc26c4ab28c541fb824f59fa83d5702f75829495d5a1dee603b29bc4fbe79095"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==18.6b2"
|
||||
},
|
||||
"certifi": {
|
||||
"hashes": [
|
||||
"sha256:13e698f54293db9f89122b0581843a782ad0934a4fe0172d2a980ba77fc61bb7",
|
||||
"sha256:9fa520c1bacfb634fa7af20a76bcbd3d5fb390481724c597da32c719a7dca4b0"
|
||||
],
|
||||
"version": "==2018.4.16"
|
||||
},
|
||||
"chardet": {
|
||||
"hashes": [
|
||||
"sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae",
|
||||
"sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691"
|
||||
],
|
||||
"version": "==3.0.4"
|
||||
},
|
||||
"click": {
|
||||
"hashes": [
|
||||
"sha256:29f99fc6125fbc931b758dc053b3114e55c77a6e4c6c3a2674a2dc986016381d",
|
||||
"sha256:f15516df478d5a56180fbf80e68f206010e6d160fc39fa508b65e035fd75130b"
|
||||
],
|
||||
"version": "==6.7"
|
||||
},
|
||||
"docutils": {
|
||||
"hashes": [
|
||||
"sha256:02aec4bd92ab067f6ff27a38a38a41173bf01bed8f89157768c1573f53e474a6",
|
||||
"sha256:51e64ef2ebfb29cae1faa133b3710143496eca21c530f3f71424d77687764274",
|
||||
"sha256:7a4bd47eaf6596e1295ecb11361139febe29b084a87bf005bf899f9a42edc3c6"
|
||||
],
|
||||
"version": "==0.14"
|
||||
},
|
||||
"idna": {
|
||||
"hashes": [
|
||||
"sha256:2c6a5de3089009e3da7c5dde64a141dbc8551d5b7f6cf4ed7c2568d0cc520a8f",
|
||||
"sha256:8c7309c718f94b3a625cb648ace320157ad16ff131ae0af362c9f21b80ef6ec4"
|
||||
],
|
||||
"version": "==2.6"
|
||||
},
|
||||
"imagesize": {
|
||||
"hashes": [
|
||||
"sha256:3620cc0cadba3f7475f9940d22431fc4d407269f1be59ec9b8edcca26440cf18",
|
||||
"sha256:5b326e4678b6925158ccc66a9fa3122b6106d7c876ee32d7de6ce59385b96315"
|
||||
],
|
||||
"version": "==1.0.0"
|
||||
},
|
||||
"jinja2": {
|
||||
"hashes": [
|
||||
"sha256:74c935a1b8bb9a3947c50a54766a969d4846290e1e788ea44c1392163723c3bd",
|
||||
"sha256:f84be1bb0040caca4cea721fcbbbbd61f9be9464ca236387158b0feea01914a4"
|
||||
],
|
||||
"version": "==2.10"
|
||||
},
|
||||
"markupsafe": {
|
||||
"hashes": [
|
||||
"sha256:a6be69091dac236ea9c6bc7d012beab42010fa914c459791d627dad4910eb665"
|
||||
],
|
||||
"version": "==1.0"
|
||||
},
|
||||
"more-itertools": {
|
||||
"hashes": [
|
||||
"sha256:2b6b9893337bfd9166bee6a62c2b0c9fe7735dcf85948b387ec8cba30e85d8e8",
|
||||
"sha256:6703844a52d3588f951883005efcf555e49566a48afd4db4e965d69b883980d3",
|
||||
"sha256:a18d870ef2ffca2b8463c0070ad17b5978056f403fb64e3f15fe62a52db21cc0"
|
||||
],
|
||||
"version": "==4.2.0"
|
||||
},
|
||||
"packaging": {
|
||||
"hashes": [
|
||||
"sha256:e9215d2d2535d3ae866c3d6efc77d5b24a0192cce0ff20e42896cc0664f889c0",
|
||||
"sha256:f019b770dd64e585a99714f1fd5e01c7a8f11b45635aa953fd41c689a657375b"
|
||||
],
|
||||
"version": "==17.1"
|
||||
},
|
||||
"pluggy": {
|
||||
"hashes": [
|
||||
"sha256:7f8ae7f5bdf75671a718d2daf0a64b7885f74510bcd98b1a0bb420eb9a9d0cff",
|
||||
"sha256:d345c8fe681115900d6da8d048ba67c25df42973bda370783cd58826442dcd7c",
|
||||
"sha256:e160a7fcf25762bb60efc7e171d4497ff1d8d2d75a3d0df7a21b76821ecbf5c5"
|
||||
],
|
||||
"version": "==0.6.0"
|
||||
},
|
||||
"py": {
|
||||
"hashes": [
|
||||
"sha256:29c9fab495d7528e80ba1e343b958684f4ace687327e6f789a94bf3d1915f881",
|
||||
"sha256:983f77f3331356039fdd792e9220b7b8ee1aa6bd2b25f567a963ff1de5a64f6a"
|
||||
],
|
||||
"version": "==1.5.3"
|
||||
},
|
||||
"pygments": {
|
||||
"hashes": [
|
||||
"sha256:78f3f434bcc5d6ee09020f92ba487f95ba50f1e3ef83ae96b9d5ffa1bab25c5d",
|
||||
"sha256:dbae1046def0efb574852fab9e90209b23f556367b5a320c0bcb871c77c3e8cc"
|
||||
],
|
||||
"version": "==2.2.0"
|
||||
},
|
||||
"pyparsing": {
|
||||
"hashes": [
|
||||
"sha256:0832bcf47acd283788593e7a0f542407bd9550a55a8a8435214a1960e04bcb04",
|
||||
"sha256:281683241b25fe9b80ec9d66017485f6deff1af5cde372469134b56ca8447a07",
|
||||
"sha256:8f1e18d3fd36c6795bb7e02a39fd05c611ffc2596c1e0d995d34d67630426c18",
|
||||
"sha256:9e8143a3e15c13713506886badd96ca4b579a87fbdf49e550dbfc057d6cb218e",
|
||||
"sha256:b8b3117ed9bdf45e14dcc89345ce638ec7e0e29b2b579fa1ecf32ce45ebac8a5",
|
||||
"sha256:e4d45427c6e20a59bf4f88c639dcc03ce30d193112047f94012102f235853a58",
|
||||
"sha256:fee43f17a9c4087e7ed1605bd6df994c6173c1e977d7ade7b651292fab2bd010"
|
||||
],
|
||||
"version": "==2.2.0"
|
||||
},
|
||||
"pytest": {
|
||||
"hashes": [
|
||||
"sha256:26838b2bc58620e01675485491504c3aa7ee0faf335c37fcd5f8731ca4319591",
|
||||
"sha256:32c49a69566aa7c333188149ad48b58ac11a426d5352ea3d8f6ce843f88199cb"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==3.6.1"
|
||||
},
|
||||
"pytest-asyncio": {
|
||||
"hashes": [
|
||||
"sha256:286b50773e996c80d894b95afaf45df6952408a67a59979ca9839f94693ec7fd",
|
||||
"sha256:f32804bb58a66e13a3eda11f8942a71b1b6a30466b0d2ffe9214787aab0e172e"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==0.8.0"
|
||||
},
|
||||
"pytz": {
|
||||
"hashes": [
|
||||
"sha256:65ae0c8101309c45772196b21b74c46b2e5d11b6275c45d251b150d5da334555",
|
||||
"sha256:c06425302f2cf668f1bba7a0a03f3c1d34d4ebeef2c72003da308b3947c7f749"
|
||||
],
|
||||
"version": "==2018.4"
|
||||
},
|
||||
"requests": {
|
||||
"hashes": [
|
||||
"sha256:6a1b267aa90cac58ac3a765d067950e7dbbf75b1da07e895d1f594193a40a38b",
|
||||
"sha256:9c443e7324ba5b85070c4a818ade28bfabedf16ea10206da1132edaa6dda237e"
|
||||
],
|
||||
"version": "==2.18.4"
|
||||
},
|
||||
"six": {
|
||||
"hashes": [
|
||||
"sha256:70e8a77beed4562e7f14fe23a786b54f6296e34344c23bc42f07b15018ff98e9",
|
||||
"sha256:832dc0e10feb1aa2c68dcc57dbb658f1c7e65b9b61af69048abc87a2db00a0eb"
|
||||
],
|
||||
"version": "==1.11.0"
|
||||
},
|
||||
"snowballstemmer": {
|
||||
"hashes": [
|
||||
"sha256:919f26a68b2c17a7634da993d91339e288964f93c274f1343e3bbbe2096e1128",
|
||||
"sha256:9f3bcd3c401c3e862ec0ebe6d2c069ebc012ce142cce209c098ccb5b09136e89"
|
||||
],
|
||||
"version": "==1.2.1"
|
||||
},
|
||||
"sphinx": {
|
||||
"hashes": [
|
||||
"sha256:85f7e32c8ef07f4ba5aeca728e0f7717bef0789fba8458b8d9c5c294cad134f3",
|
||||
"sha256:d45480a229edf70d84ca9fae3784162b1bc75ee47e480ffe04a4b7f21a95d76d"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==1.7.5"
|
||||
},
|
||||
"sphinx-rtd-theme": {
|
||||
"hashes": [
|
||||
"sha256:aa3e190392e963551432de7df24b8a5fbe5b71a2f4fcd9d5b75808b52ad999e5",
|
||||
"sha256:de88d637a60371d4f923e06b79c4ba260490c57d2ab5a8316942ab5d9a6ce1bf"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==0.4.0"
|
||||
},
|
||||
"sphinxcontrib-asyncio": {
|
||||
"hashes": [
|
||||
"sha256:96627b1ec4eba08d09ad577ff9416c131910333ef37a2c82a2716e59646739f0"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==0.2.0"
|
||||
},
|
||||
"sphinxcontrib-websupport": {
|
||||
"hashes": [
|
||||
"sha256:68ca7ff70785cbe1e7bccc71a48b5b6d965d79ca50629606c7861a21b206d9dd",
|
||||
"sha256:9de47f375baf1ea07cdb3436ff39d7a9c76042c10a769c52353ec46e4e8fc3b9"
|
||||
],
|
||||
"version": "==1.1.0"
|
||||
},
|
||||
"toml": {
|
||||
"hashes": [
|
||||
"sha256:8e86bd6ce8cc11b9620cb637466453d94f5d57ad86f17e98a98d1f73e3baab2d"
|
||||
],
|
||||
"version": "==0.9.4"
|
||||
},
|
||||
"tox": {
|
||||
"hashes": [
|
||||
"sha256:96efa09710a3daeeb845561ebbe1497641d9cef2ee0aea30db6969058b2bda2f",
|
||||
"sha256:9ee7de958a43806402a38c0d2aa07fa8553f4d2c20a15b140e9f771c2afeade0"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==3.0.0"
|
||||
},
|
||||
"urllib3": {
|
||||
"hashes": [
|
||||
"sha256:06330f386d6e4b195fbfc736b297f58c5a892e4440e54d294d7004e3a9bbea1b",
|
||||
"sha256:cc44da8e1145637334317feebd728bd869a35285b93cbb4cca2577da7e62db4f"
|
||||
],
|
||||
"version": "==1.22"
|
||||
},
|
||||
"virtualenv": {
|
||||
"hashes": [
|
||||
"sha256:2ce32cd126117ce2c539f0134eb89de91a8413a29baac49cbab3eb50e2026669",
|
||||
"sha256:ca07b4c0b54e14a91af9f34d0919790b016923d157afda5efdde55c96718f752"
|
||||
],
|
||||
"version": "==16.0.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
119
README.rst
119
README.rst
@@ -1,42 +1,113 @@
|
||||
.. image:: https://readthedocs.org/projects/red-discordbot/badge/?version=v3-develop
|
||||
.. class:: center
|
||||
|
||||
.. image:: https://imgur.com/pY1WUFX.png
|
||||
:target: https://github.com/Cog-Creators/Red-DiscordBot/tree/V3/develop
|
||||
:alt: Red Discord Bot
|
||||
|
||||
|
||||
.. class:: center
|
||||
|
||||
Music, Moderation, Trivia, Stream Alerts and fully customizable.
|
||||
|
||||
.. class:: center
|
||||
|
||||
.. image:: https://discordapp.com/api/guilds/133049272517001216/embed.png
|
||||
:target: https://discord.gg/red
|
||||
:alt: Discord server
|
||||
|
||||
.. image:: https://api.travis-ci.org/Cog-Creators/Red-DiscordBot.svg?branch=V3/develop
|
||||
:target: https://travis-ci.org/Cog-Creators/Red-DiscordBot
|
||||
:alt: Travis CI status
|
||||
|
||||
.. image:: https://readthedocs.org/projects/red-discordbot/badge/?version=v3-develop
|
||||
:target: http://red-discordbot.readthedocs.io/en/v3-develop/?badge=v3-develop
|
||||
:alt: Documentation Status
|
||||
|
||||
.. image:: https://img.shields.io/badge/discord-py-blue.svg
|
||||
:target: https://github.com/Rapptz/discord.py
|
||||
:alt: discord.py
|
||||
|
||||
.. image:: https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat-square
|
||||
:target: http://makeapullrequest.com
|
||||
:alt: PRs Welcome
|
||||
.. image:: https://img.shields.io/badge/code%20style-black-000000.svg
|
||||
:target: https://github.com/ambv/black
|
||||
:alt: Code style: black
|
||||
|
||||
.. image:: https://d322cqt584bo4o.cloudfront.net/red-discordbot/localized.svg
|
||||
.. image:: https://d322cqt584bo4o.cloudfront.net/red-discordbot/localized.svg
|
||||
:target: https://crowdin.com/project/red-discordbot
|
||||
:alt: Crowdin
|
||||
|
||||
.. image:: https://img.shields.io/badge/Support-Red!-orange.svg
|
||||
|
||||
.. image:: https://img.shields.io/badge/Support-Red!-orange.svg
|
||||
:target: https://www.patreon.com/Red_Devs
|
||||
:alt: Patreon
|
||||
|
||||
.. image:: https://img.shields.io/badge/PRs-welcome-brightgreen.svg
|
||||
:target: http://makeapullrequest.com
|
||||
:alt: PRs open
|
||||
|
||||
********************
|
||||
Red - Discord Bot v3
|
||||
********************
|
||||
==========
|
||||
Overview
|
||||
==========
|
||||
|
||||
**This is in beta and very much a work in progress. Regular use is not recommended.
|
||||
There will not be any effort made to prevent the breaking of current installations.**
|
||||
Red is a fully modular bot – meaning all features and commands can be enabled/disabled to your liking, making it completely customizable.
|
||||
This is also a *self-hosted bot* – meaning you will need to host and maintain your own instance. You can turn Red into an admin bot, music bot, trivia bot, new best friend or all of these together!
|
||||
|
||||
How to install
|
||||
^^^^^^^^^^^^^^
|
||||
`Installation <#installation>`_ is easy, and you do **NOT** need to know anything about coding! Aside from installation and updating, every part of the bot can be controlled from within Discord.
|
||||
|
||||
Using python3 pip::
|
||||
**The default set of modules includes and is not limited to:**
|
||||
|
||||
pip install --process-dependency-links -U Red-DiscordBot
|
||||
redbot-setup
|
||||
redbot <name>
|
||||
- Moderation features (kick/ban/softban/hackban, mod-log, filter, chat cleanup)
|
||||
- Trivia (lists are included and can be easily added)
|
||||
- Music features (YouTube, SoundCloud, local files, playlists, queues)
|
||||
- Stream alerts (Twitch, Youtube, Mixer, Hitbox, Picarto)
|
||||
- Slot machine
|
||||
- Custom commands
|
||||
- Imgur/gif search
|
||||
|
||||
To install requirements for voice::
|
||||
|
||||
pip install --process-dependency-links -U Red-DiscordBot[voice]
|
||||
**Additionally, other plugins (cogs) can be easily found and added from our growing community of cog repositories.**
|
||||
|
||||
To install all requirements for docs and tests::
|
||||
- Cleverbot integration (talk to Red and she talks back)
|
||||
- Ban sync
|
||||
- Welcome messages
|
||||
- Casino
|
||||
- Reaction roles
|
||||
- Slow Mode
|
||||
- Anilist
|
||||
- And much, much more!
|
||||
|
||||
pip install --process-dependency-links -U Red-DiscordBot[test,docs]
|
||||
Feel free to take a `peek <https://github.com/Cog-Creators/Red-DiscordBot/issues/1398>`_!
|
||||
|
||||
For the latest git build, replace ``Red-DiscordBot`` in the above commands with
|
||||
``git+https://github.com/Cog-Creators/Red-DiscordBot@V3/develop``.
|
||||
==============
|
||||
Installation
|
||||
==============
|
||||
|
||||
**The following platforms are officially supported:**
|
||||
|
||||
- `Windows <https://red-discordbot.readthedocs.io/en/v3-develop/install_windows.html>`_
|
||||
- `MacOS <https://red-discordbot.readthedocs.io/en/v3-develop/install_mac.html>`_
|
||||
- `Ubuntu <https://red-discordbot.readthedocs.io/en/v3-develop/install_ubuntu.html>`_
|
||||
- `Debian Stretch <https://red-discordbot.readthedocs.io/en/v3-develop/install_debian.html>`_
|
||||
- `CentOS 7 <https://red-discordbot.readthedocs.io/en/v3-develop/install_centos.html>`_
|
||||
- `Arch Linux <https://red-discordbot.readthedocs.io/en/v3-develop/install_arch.html>`_
|
||||
- `Raspbian Stretch <https://red-discordbot.readthedocs.io/en/v3-develop/install_raspbian.html>`_
|
||||
|
||||
Already using **Red** V2? Take a look at the `Data Converter <https://red-discordbot.readthedocs.io/en/v3-develop/cog_dataconverter.html>`_ to import your data to V3.
|
||||
|
||||
If `after reading the guides <https://red-discordbot.readthedocs.io/en/v3-develop/>`_ you are still experiencing issues, feel free to join the `Official Server <https://discord.gg/red>`_ and ask in the **#support** channel for help.
|
||||
|
||||
=====================
|
||||
Join the community!
|
||||
=====================
|
||||
|
||||
**Red** is in continuous development, and it’s supported by an active community which produces new content (cogs/plugins) for everyone to enjoy. New features are constantly added. If you can’t `find <https://github.com/Cog-Creators/Red-DiscordBot/issues/1398>`_ what you’re looking for, consult our `guide <https://red-discordbot.readthedocs.io/en/v3-develop/guide_cog_creation.html>`_ on building your own cogs!
|
||||
|
||||
Join us on our `Official Discord Server <https://discord.gg/red>`_!
|
||||
|
||||
=========
|
||||
License
|
||||
=========
|
||||
|
||||
Released under the `GNU GPL v3 <#License>`_.
|
||||
|
||||
Red is named after the main character of "Transistor", a videogame by `Super Giant Games <https://www.supergiantgames.com/games/transistor/>`_
|
||||
|
||||
Artwork created by `Sinlaire <https://sinlaire.deviantart.com/>`_ on Deviant Art for the Red Bot Project.
|
||||
|
||||
@@ -14,6 +14,9 @@ help:
|
||||
|
||||
.PHONY: help Makefile
|
||||
|
||||
init:
|
||||
cd .. && pipenv lock -r --dev > docs/requirements.txt && echo 'git+https://github.com/Rapptz/discord.py@rewrite#egg=discord.py-1.0' >> docs/requirements.txt
|
||||
|
||||
# Catch-all target: route all unknown targets to Sphinx using the new
|
||||
# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS).
|
||||
%: Makefile
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
.. systemd service guide
|
||||
|
||||
==========================
|
||||
==============================================
|
||||
Setting up auto-restart using systemd on Linux
|
||||
==========================
|
||||
==============================================
|
||||
|
||||
---------------------------
|
||||
-------------------------
|
||||
Creating the service file
|
||||
---------------------------
|
||||
-------------------------
|
||||
|
||||
Create the new service file:
|
||||
|
||||
@@ -27,15 +27,18 @@ Paste the following and replace all instances of :code:`username` with the usern
|
||||
Type=idle
|
||||
Restart=always
|
||||
RestartSec=15
|
||||
RestartPreventExitStatus=0
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
|
||||
Save and exit :code:`ctrl + O; enter; ctrl + x`
|
||||
|
||||
---------------------------
|
||||
---------------------------------
|
||||
Starting and enabling the service
|
||||
---------------------------
|
||||
---------------------------------
|
||||
|
||||
.. note:: This same file can be used to start as many instances of the bot as you wish, without creating more service files, just start and enable more services and add any bot instance name after the **@**
|
||||
|
||||
To start the bot, run the service and add the instance name after the **@**:
|
||||
|
||||
@@ -45,4 +48,6 @@ To set the bot to start on boot, you must enable the service, again adding the i
|
||||
|
||||
:code:`sudo systemctl enable red@instancename`
|
||||
|
||||
.. note:: This same file can be used to start as many instances of the bot as you wish, without creating more service files, just start and enable more services and add any bot instance name after the **@**
|
||||
To view Red’s log, you can acccess through journalctl:
|
||||
|
||||
:code:`sudo journalctl -u red@instancename`
|
||||
|
||||
107
docs/cog_permissions.rst
Normal file
107
docs/cog_permissions.rst
Normal file
@@ -0,0 +1,107 @@
|
||||
.. Permissions Cog Reference
|
||||
|
||||
=========================
|
||||
Permissions Cog Reference
|
||||
=========================
|
||||
|
||||
------------
|
||||
How it works
|
||||
------------
|
||||
|
||||
When loaded, the permissions cog will allow you
|
||||
to define extra custom rules for who can use a command
|
||||
|
||||
If no applicable rules are found, the command will behave as if
|
||||
the cog was not loaded.
|
||||
|
||||
-------------
|
||||
Rule priority
|
||||
-------------
|
||||
|
||||
Rules set will be checked in the following order
|
||||
|
||||
|
||||
1. Owner level command specific settings
|
||||
2. Owner level cog specific settings
|
||||
3. Server level command specific settings
|
||||
4. Server level cog specific settings
|
||||
|
||||
For each of those, settings have varying priorities (listed below, highest to lowest priority)
|
||||
|
||||
1. User whitelist
|
||||
2. User blacklist
|
||||
3. Voice Channel whitelist
|
||||
4. Voice Channel blacklist
|
||||
5. Text Channel whitelist
|
||||
6. Text Channel blacklist
|
||||
7. Role settings (see below)
|
||||
8. Server whitelist
|
||||
9. Server blacklist
|
||||
10. Default settings
|
||||
|
||||
For the role whitelist and blacklist settings,
|
||||
roles will be checked individually in order from highest to lowest role the user has
|
||||
Each role will be checked for whitelist, then blacklist. The first role with a setting
|
||||
found will be the one used.
|
||||
|
||||
-------------------------
|
||||
Setting Rules from a file
|
||||
-------------------------
|
||||
|
||||
The permissions cog can set rules from a yaml file:
|
||||
All entries are based on ID.
|
||||
An example of the expected format is shown below.
|
||||
|
||||
.. code-block:: yaml
|
||||
|
||||
cogs:
|
||||
Admin:
|
||||
allow:
|
||||
- 78631113035100160
|
||||
deny:
|
||||
- 96733288462286848
|
||||
Audio:
|
||||
allow:
|
||||
- 133049272517001216
|
||||
default: deny
|
||||
commands:
|
||||
cleanup bot:
|
||||
allow:
|
||||
- 78631113035100160
|
||||
default: deny
|
||||
ping:
|
||||
deny:
|
||||
- 96733288462286848
|
||||
default: allow
|
||||
|
||||
----------------------
|
||||
Example configurations
|
||||
----------------------
|
||||
|
||||
Locking Audio cog to approved server(s) as a bot owner
|
||||
|
||||
.. code-block:: none
|
||||
|
||||
[p]permissions setglobaldefault Audio deny
|
||||
[p]permissions addglobalrule allow Audio [server ID or name]
|
||||
|
||||
Locking Audio to specific voice channel(s) as a serverowner or admin:
|
||||
|
||||
.. code-block:: none
|
||||
|
||||
[p]permissions setguilddefault deny play
|
||||
[p]permissions setguilddefault deny "playlist start"
|
||||
[p]permissions addguildrule allow play [voice channel ID or name]
|
||||
[p]permissions addguildrule allow "playlist start" [voice channel ID or name]
|
||||
|
||||
Allowing extra roles to use cleanup
|
||||
|
||||
.. code-block:: none
|
||||
|
||||
[p]permissions addguildrule allow Cleanup [role ID]
|
||||
|
||||
Preventing cleanup from being used in channels where message history is important:
|
||||
|
||||
.. code-block:: none
|
||||
|
||||
[p]permissions addguildrule deny Cleanup [channel ID or mention]
|
||||
91
docs/conf.py
91
docs/conf.py
@@ -19,9 +19,10 @@
|
||||
#
|
||||
import os
|
||||
import sys
|
||||
sys.path.insert(0, os.path.abspath('..'))
|
||||
|
||||
os.environ['BUILDING_DOCS'] = "1"
|
||||
sys.path.insert(0, os.path.abspath(".."))
|
||||
|
||||
os.environ["BUILDING_DOCS"] = "1"
|
||||
|
||||
|
||||
# -- General configuration ------------------------------------------------
|
||||
@@ -34,35 +35,36 @@ os.environ['BUILDING_DOCS'] = "1"
|
||||
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
|
||||
# ones.
|
||||
extensions = [
|
||||
'sphinx.ext.autodoc',
|
||||
'sphinx.ext.intersphinx',
|
||||
'sphinx.ext.viewcode',
|
||||
'sphinx.ext.napoleon',
|
||||
'sphinxcontrib.asyncio'
|
||||
"sphinx.ext.autodoc",
|
||||
"sphinx.ext.intersphinx",
|
||||
"sphinx.ext.viewcode",
|
||||
"sphinx.ext.napoleon",
|
||||
"sphinxcontrib.asyncio",
|
||||
]
|
||||
|
||||
# Add any paths that contain templates here, relative to this directory.
|
||||
templates_path = ['_templates']
|
||||
templates_path = ["_templates"]
|
||||
|
||||
# The suffix(es) of source filenames.
|
||||
# You can specify multiple suffix as a list of string:
|
||||
#
|
||||
# source_suffix = ['.rst', '.md']
|
||||
source_suffix = '.rst'
|
||||
source_suffix = ".rst"
|
||||
|
||||
# The master toctree document.
|
||||
master_doc = 'index'
|
||||
master_doc = "index"
|
||||
|
||||
# General information about the project.
|
||||
project = 'Red - Discord Bot'
|
||||
copyright = '2018, Cog Creators'
|
||||
author = 'Cog Creators'
|
||||
project = "Red - Discord Bot"
|
||||
copyright = "2018, Cog Creators"
|
||||
author = "Cog Creators"
|
||||
|
||||
# The version info for the project you're documenting, acts as replacement for
|
||||
# |version| and |release|, also used in various other places throughout the
|
||||
# built documents.
|
||||
#
|
||||
from redbot.core import __version__
|
||||
|
||||
# The short X.Y version.
|
||||
version = __version__
|
||||
# The full version, including alpha/beta/rc tags.
|
||||
@@ -78,10 +80,10 @@ language = None
|
||||
# List of patterns, relative to source directory, that match files and
|
||||
# directories to ignore when looking for source files.
|
||||
# This patterns also effect to html_static_path and html_extra_path
|
||||
exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store']
|
||||
exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"]
|
||||
|
||||
# The name of the Pygments (syntax highlighting) style to use.
|
||||
pygments_style = 'sphinx'
|
||||
pygments_style = "sphinx"
|
||||
|
||||
# If true, `todo` and `todoList` produce output, else they produce nothing.
|
||||
todo_include_todos = False
|
||||
@@ -95,7 +97,7 @@ default_role = "any"
|
||||
# The theme to use for HTML and HTML Help pages. See the documentation for
|
||||
# a list of builtin themes.
|
||||
#
|
||||
html_theme = 'sphinx_rtd_theme'
|
||||
html_theme = "sphinx_rtd_theme"
|
||||
|
||||
# Theme options are theme-specific and customize the look and feel of a theme
|
||||
# further. For a list of options available for each theme, see the
|
||||
@@ -105,16 +107,16 @@ html_theme = 'sphinx_rtd_theme'
|
||||
|
||||
html_context = {
|
||||
# Enable the "Edit in GitHub link within the header of each page.
|
||||
'display_github': True,
|
||||
'github_user': 'Cog-Creators',
|
||||
'github_repo': 'Red-DiscordBot',
|
||||
'github_version': 'V3/develop/docs/'
|
||||
"display_github": True,
|
||||
"github_user": "Cog-Creators",
|
||||
"github_repo": "Red-DiscordBot",
|
||||
"github_version": "V3/develop/docs/",
|
||||
}
|
||||
|
||||
# Add any paths that contain custom static files (such as style sheets) here,
|
||||
# relative to this directory. They are copied after the builtin static files,
|
||||
# so a file named "default.css" will overwrite the builtin "default.css".
|
||||
html_static_path = ['_static']
|
||||
# html_static_path = ['_static']
|
||||
|
||||
# Custom sidebar templates, must be a dictionary that maps document names
|
||||
# to template names.
|
||||
@@ -122,12 +124,12 @@ html_static_path = ['_static']
|
||||
# This is required for the alabaster theme
|
||||
# refs: http://alabaster.readthedocs.io/en/latest/installation.html#sidebars
|
||||
html_sidebars = {
|
||||
'**': [
|
||||
'about.html',
|
||||
'navigation.html',
|
||||
'relations.html', # needs 'show_related': True theme option to display
|
||||
'searchbox.html',
|
||||
'donate.html',
|
||||
"**": [
|
||||
"about.html",
|
||||
"navigation.html",
|
||||
"relations.html", # needs 'show_related': True theme option to display
|
||||
"searchbox.html",
|
||||
"donate.html",
|
||||
]
|
||||
}
|
||||
|
||||
@@ -135,7 +137,7 @@ html_sidebars = {
|
||||
# -- Options for HTMLHelp output ------------------------------------------
|
||||
|
||||
# Output file base name for HTML help builder.
|
||||
htmlhelp_basename = 'Red-DiscordBotdoc'
|
||||
htmlhelp_basename = "Red-DiscordBotdoc"
|
||||
|
||||
|
||||
# -- Options for LaTeX output ---------------------------------------------
|
||||
@@ -144,15 +146,12 @@ latex_elements = {
|
||||
# The paper size ('letterpaper' or 'a4paper').
|
||||
#
|
||||
# 'papersize': 'letterpaper',
|
||||
|
||||
# The font size ('10pt', '11pt' or '12pt').
|
||||
#
|
||||
# 'pointsize': '10pt',
|
||||
|
||||
# Additional stuff for the LaTeX preamble.
|
||||
#
|
||||
# 'preamble': '',
|
||||
|
||||
# Latex figure (float) alignment
|
||||
#
|
||||
# 'figure_align': 'htbp',
|
||||
@@ -162,8 +161,7 @@ latex_elements = {
|
||||
# (source start file, target name, title,
|
||||
# author, documentclass [howto, manual, or own class]).
|
||||
latex_documents = [
|
||||
(master_doc, 'Red-DiscordBot.tex', 'Red - Discord Bot Documentation',
|
||||
'Cog Creators', 'manual'),
|
||||
(master_doc, "Red-DiscordBot.tex", "Red - Discord Bot Documentation", "Cog Creators", "manual")
|
||||
]
|
||||
|
||||
|
||||
@@ -171,10 +169,7 @@ latex_documents = [
|
||||
|
||||
# One entry per manual page. List of tuples
|
||||
# (source start file, name, description, authors, manual section).
|
||||
man_pages = [
|
||||
(master_doc, 'red-discordbot', 'Red - Discord Bot Documentation',
|
||||
[author], 1)
|
||||
]
|
||||
man_pages = [(master_doc, "red-discordbot", "Red - Discord Bot Documentation", [author], 1)]
|
||||
|
||||
|
||||
# -- Options for Texinfo output -------------------------------------------
|
||||
@@ -183,15 +178,21 @@ man_pages = [
|
||||
# (source start file, target name, title, author,
|
||||
# dir menu entry, description, category)
|
||||
texinfo_documents = [
|
||||
(master_doc, 'Red-DiscordBot', 'Red - Discord Bot Documentation',
|
||||
author, 'Red-DiscordBot', 'One line description of project.',
|
||||
'Miscellaneous'),
|
||||
(
|
||||
master_doc,
|
||||
"Red-DiscordBot",
|
||||
"Red - Discord Bot Documentation",
|
||||
author,
|
||||
"Red-DiscordBot",
|
||||
"One line description of project.",
|
||||
"Miscellaneous",
|
||||
)
|
||||
]
|
||||
|
||||
|
||||
|
||||
|
||||
# Example configuration for intersphinx: refer to the Python standard library.
|
||||
intersphinx_mapping = {'python': ('https://docs.python.org/3.5', None),
|
||||
'dpy': ('https://discordpy.readthedocs.io/en/rewrite/', None),
|
||||
'motor': ('https://motor.readthedocs.io/en/stable/', None)}
|
||||
intersphinx_mapping = {
|
||||
"python": ("https://docs.python.org/3.6", None),
|
||||
"dpy": ("https://discordpy.readthedocs.io/en/rewrite/", None),
|
||||
"motor": ("https://motor.readthedocs.io/en/stable/", None),
|
||||
}
|
||||
|
||||
@@ -11,6 +11,10 @@ RedBase
|
||||
|
||||
.. autoclass:: RedBase
|
||||
:members:
|
||||
:exclude-members: get_context
|
||||
|
||||
.. automethod:: register_rpc_handler
|
||||
.. automethod:: unregister_rpc_handler
|
||||
|
||||
Red
|
||||
^^^
|
||||
|
||||
22
docs/framework_commands.rst
Normal file
22
docs/framework_commands.rst
Normal file
@@ -0,0 +1,22 @@
|
||||
.. red commands module documentation
|
||||
|
||||
================
|
||||
Commands Package
|
||||
================
|
||||
|
||||
This package acts almost identically to :doc:`discord.ext.commands <dpy:ext/commands/api>`; i.e.
|
||||
they both have the same attributes. Some of these attributes, however, have been slightly modified,
|
||||
as outlined below.
|
||||
|
||||
.. autofunction:: redbot.core.commands.command
|
||||
|
||||
.. autofunction:: redbot.core.commands.group
|
||||
|
||||
.. autoclass:: redbot.core.commands.Command
|
||||
:members:
|
||||
|
||||
.. autoclass:: redbot.core.commands.Group
|
||||
:members:
|
||||
|
||||
.. autoclass:: redbot.core.commands.Context
|
||||
:members:
|
||||
@@ -29,7 +29,7 @@ Basic Usage
|
||||
|
||||
@commands.command()
|
||||
async def return_some_data(self, ctx):
|
||||
await ctx.send(await config.foo())
|
||||
await ctx.send(await self.config.foo())
|
||||
|
||||
********
|
||||
Tutorial
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
.. red invocation context documentation
|
||||
|
||||
==========================
|
||||
Command Invocation Context
|
||||
==========================
|
||||
|
||||
.. automodule:: redbot.core.context
|
||||
|
||||
.. autoclass:: redbot.core.RedContext
|
||||
:members:
|
||||
@@ -6,21 +6,35 @@ Downloader Framework
|
||||
Info.json
|
||||
*********
|
||||
|
||||
The info.json file may exist inside every package folder in the repo,
|
||||
it is optional however. This string describes the valid keys within
|
||||
an info file (and maybe how the Downloader cog uses them).
|
||||
The optional info.json file may exist inside every package folder in the repo,
|
||||
as well as in the root of the repo. The following sections describe the valid
|
||||
keys within an info file (and maybe how the Downloader cog uses them).
|
||||
|
||||
KEYS (case sensitive):
|
||||
Keys common to both repo and cog info.json (case sensitive)
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
- ``author`` (list of strings) - list of names of authors of the cog
|
||||
- ``author`` (list of strings) - list of names of authors of the cog or repo.
|
||||
|
||||
- ``description`` (string) - A long description of the cog or repo. For cogs, this
|
||||
is displayed when a user executes ``!cog info``.
|
||||
|
||||
- ``install_msg`` (string) - The message that gets displayed when a cog
|
||||
is installed or a repo is added
|
||||
|
||||
.. tip:: You can use the ``[p]`` key in your string to use the prefix
|
||||
used for installing.
|
||||
|
||||
- ``short`` (string) - A short description of the cog or repo. For cogs, this info
|
||||
is displayed when a user executes ``!cog list``
|
||||
|
||||
Keys specific to the cog info.json (case sensitive)
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
- ``bot_version`` (list of integer) - Min version number of Red in the format ``(MAJOR, MINOR, PATCH)``
|
||||
|
||||
- ``description`` (string) - A long description of the cog that appears when a user executes ```!cog info``.
|
||||
- ``hidden`` (bool) - Determines if a cog is visible in the cog list for a repo.
|
||||
|
||||
- ``hidden`` (bool) - Determines if a cog is available for install.
|
||||
|
||||
- ``install_msg`` (string) - The message that gets displayed when a cog is installed
|
||||
- ``disabled`` (bool) - Determines if a cog is available for install.
|
||||
|
||||
- ``required_cogs`` (map of cogname to repo URL) - A map of required cogs that this cog depends on.
|
||||
Downloader will not deal with this functionality but it may be useful for other cogs.
|
||||
@@ -29,9 +43,6 @@ KEYS (case sensitive):
|
||||
passed to pip on cog install. ``SHARED_LIBRARIES`` do NOT go in this
|
||||
list.
|
||||
|
||||
- ``short`` (string) - A short description of the cog that appears when
|
||||
a user executes `!cog list`
|
||||
|
||||
- ``tags`` (list of strings) - A list of strings that are related to the
|
||||
functionality of the cog. Used to aid in searching.
|
||||
|
||||
|
||||
@@ -13,11 +13,12 @@ Basic Usage
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from discord.ext import commands
|
||||
from redbot.core.i18n import CogI18n
|
||||
from redbot.core import commands
|
||||
from redbot.core.i18n import Translator, cog_i18n
|
||||
|
||||
_ = CogI18n("ExampleCog", __file__)
|
||||
_ = Translator("ExampleCog", __file__)
|
||||
|
||||
@cog_i18n(_)
|
||||
class ExampleCog:
|
||||
"""description"""
|
||||
|
||||
@@ -39,16 +40,19 @@ In a command prompt in your cog's package (where yourcog.py is),
|
||||
create a directory called "locales".
|
||||
Then do one of the following:
|
||||
|
||||
Windows: :code:`python <your python install path>\Tools\i18n\pygettext.py -n -p locales`
|
||||
Windows: :code:`python <your python install path>\Tools\i18n\pygettext.py -D -n -p locales`
|
||||
|
||||
Mac: ?
|
||||
|
||||
Linux: :code:`pygettext3 -n -p locales`
|
||||
Linux: :code:`pygettext3 -D -n -p locales`
|
||||
|
||||
This will generate a messages.pot file with strings to be translated
|
||||
This will generate a messages.pot file with strings to be translated, including
|
||||
docstrings.
|
||||
|
||||
-------------
|
||||
API Reference
|
||||
-------------
|
||||
|
||||
.. automodule:: redbot.core.i18n
|
||||
.. automodule:: redbot.core.i18n
|
||||
:members:
|
||||
:special-members: __call__
|
||||
|
||||
@@ -4,5 +4,60 @@
|
||||
RPC
|
||||
===
|
||||
|
||||
.. automodule:: redbot.core.rpc
|
||||
:members:
|
||||
V3 comes default with an internal RPC server that may be used to remotely control the bot in various ways.
|
||||
Cogs must register functions to be exposed to RPC clients.
|
||||
Each of those functions must only take JSON serializable parameters and must return JSON serializable objects.
|
||||
|
||||
To enable the internal RPC server you must start the bot with the ``--rpc`` flag.
|
||||
|
||||
********
|
||||
Examples
|
||||
********
|
||||
|
||||
.. code-block:: Python
|
||||
|
||||
def setup(bot):
|
||||
c = Cog()
|
||||
bot.add_cog(c)
|
||||
bot.register_rpc_handler(c.rpc_method)
|
||||
|
||||
*******************************
|
||||
Interacting with the RPC Server
|
||||
*******************************
|
||||
|
||||
The RPC server opens a websocket bound to port ``6133`` on ``127.0.0.1``.
|
||||
This is not configurable for security reasons as broad access to this server gives anyone complete control over your bot.
|
||||
To access the server you must find a library that implements websocket based JSONRPC in the language of your choice.
|
||||
|
||||
There are a few built-in RPC methods to note:
|
||||
|
||||
* ``GET_METHODS`` - Returns a list of available RPC methods.
|
||||
* ``GET_METHOD_INFO`` - Will return the docstring for an available RPC method. Useful for finding information about the method's parameters and return values.
|
||||
* ``GET_TOPIC`` - Returns a list of available RPC message topics.
|
||||
* ``GET_SUBSCRIPTIONS`` - Returns a list of RPC subscriptions.
|
||||
* ``SUBSCRIBE`` - Subscribes to an available RPC message topic.
|
||||
* ``UNSUBSCRIBE`` - Unsubscribes from an RPC message topic.
|
||||
|
||||
All RPC methods accept a list of parameters.
|
||||
The built-in methods above expect their parameters to be in list format.
|
||||
|
||||
All cog-based methods expect their parameter list to take one argument, a JSON object, in the following format::
|
||||
|
||||
params = [
|
||||
{
|
||||
"args": [], # A list of positional arguments
|
||||
"kwargs": {}, # A dictionary of keyword arguments
|
||||
}
|
||||
]
|
||||
|
||||
# As an example, here's a call to "get_method_info"
|
||||
rpc_call("GET_METHOD_INFO", ["get_methods",])
|
||||
|
||||
# And here's a call to "core__load"
|
||||
rpc_call("CORE__LOAD", {"args": [["general", "economy", "downloader"],], "kwargs": {}})
|
||||
|
||||
*************
|
||||
API Reference
|
||||
*************
|
||||
|
||||
Please see the :class:`redbot.core.bot.RedBase` class for details on the RPC handler register and unregister methods.
|
||||
|
||||
@@ -16,6 +16,12 @@ Embed Helpers
|
||||
.. automodule:: redbot.core.utils.embed
|
||||
:members:
|
||||
|
||||
Menu Helpers
|
||||
============
|
||||
|
||||
.. automodule:: redbot.core.utils.menus
|
||||
:members:
|
||||
|
||||
Mod Helpers
|
||||
===========
|
||||
|
||||
|
||||
@@ -17,11 +17,10 @@ you in the process.
|
||||
Getting started
|
||||
---------------
|
||||
|
||||
To start off, be sure that you have installed Python 3.5 or higher (if you
|
||||
are on Windows, stick with 3.5). Open a terminal or command prompt and type
|
||||
To start off, be sure that you have installed Python 3.6 or higher. Open a terminal or command prompt and type
|
||||
:code:`pip install --process-dependency-links -U git+https://github.com/Cog-Creators/Red-DiscordBot@V3/develop#egg=redbot[test]`
|
||||
(note that if you get an error with this, try again but put :code:`python -m` in front of the command
|
||||
This will install the latest version of V3.
|
||||
This will install the latest version of V3.
|
||||
|
||||
--------------------
|
||||
Setting up a package
|
||||
@@ -90,6 +89,6 @@ have successfully created a cog!
|
||||
Additional resources
|
||||
--------------------
|
||||
|
||||
Be sure to check out the `migration guide </guide_migration>`_ for some resources
|
||||
Be sure to check out the :doc:`/guide_migration` for some resources
|
||||
on developing cogs for V3. This will also cover differences between V2 and V3 for
|
||||
those who developed cogs for V2.
|
||||
|
||||
@@ -12,7 +12,8 @@ Welcome to Red - Discord Bot's documentation!
|
||||
|
||||
install_windows
|
||||
install_mac
|
||||
install_ubuntu
|
||||
install_ubuntu_xenial
|
||||
install_ubuntu_bionic
|
||||
install_debian
|
||||
install_centos
|
||||
install_arch
|
||||
@@ -25,6 +26,7 @@ Welcome to Red - Discord Bot's documentation!
|
||||
:caption: Cog Reference:
|
||||
|
||||
cog_downloader
|
||||
cog_permissions
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 2
|
||||
@@ -37,12 +39,12 @@ Welcome to Red - Discord Bot's documentation!
|
||||
framework_bot
|
||||
framework_cogmanager
|
||||
framework_config
|
||||
framework_context
|
||||
framework_datamanager
|
||||
framework_downloader
|
||||
framework_events
|
||||
framework_i18n
|
||||
framework_modlog
|
||||
framework_commands
|
||||
framework_rpc
|
||||
framework_utils
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ Installing the pre-requirements
|
||||
|
||||
.. code-block:: none
|
||||
|
||||
sudo pacman -Sy python-pip git base-devel jre8-openjdk
|
||||
sudo pacman -Syu python-pip git base-devel jre8-openjdk
|
||||
|
||||
------------------
|
||||
Installing the bot
|
||||
|
||||
@@ -14,8 +14,7 @@ Installing pre-requirements
|
||||
|
||||
yum -y groupinstall development
|
||||
yum -y install https://centos7.iuscommunity.org/ius-release.rpm
|
||||
yum -y install yum-utils wget which python35u python35u-pip python35u-devel openssl-devel libffi-devel git java-1.8.0-openjdk
|
||||
sh -c "$(wget https://gist.githubusercontent.com/mustafaturan/7053900/raw/27f4c8bad3ee2bb0027a1a52dc8501bf1e53b270/latest-ffmpeg-centos6.sh -O -)"
|
||||
yum -y install yum-utils wget which python36u python36u-pip python36u-devel openssl-devel libffi-devel git java-1.8.0-openjdk
|
||||
|
||||
--------------
|
||||
Installing Red
|
||||
|
||||
@@ -12,9 +12,24 @@ Installing pre-requirements
|
||||
|
||||
.. code-block:: none
|
||||
|
||||
echo "deb http://httpredir.debian.org/debian stretch-backports main contrib non-free" >> /etc/apt/sources.list
|
||||
apt-get update
|
||||
apt-get install python3.5-dev python3-pip build-essential libssl-dev libffi-dev git unzip default-jre -y
|
||||
sudo apt install -y make build-essential libssl-dev zlib1g-dev libbz2-dev libreadline-dev libsqlite3-dev wget curl llvm libncurses5-dev libncursesw5-dev xz-utils tk-dev libffi-dev git unzip default-jre
|
||||
curl -L https://github.com/pyenv/pyenv-installer/raw/master/bin/pyenv-installer | bash
|
||||
|
||||
After that last command, you may see a warning about 'pyenv' not being in the load path. Follow the instructions given to fix that, then close and reopen your shell
|
||||
|
||||
Then run the following command:
|
||||
|
||||
.. code-block:: none
|
||||
|
||||
CONFIGURE_OPTS=--enable-optimizations pyenv install 3.6.5 -v
|
||||
|
||||
This may take a long time to complete.
|
||||
|
||||
After that is finished, run:
|
||||
|
||||
.. code-block:: none
|
||||
|
||||
pyenv global 3.6.5
|
||||
|
||||
------------------
|
||||
Installing the bot
|
||||
|
||||
@@ -12,8 +12,24 @@ Installing pre-requirements
|
||||
|
||||
.. code-block:: none
|
||||
|
||||
sudo apt-get install python3.5-dev python3-pip build-essential libssl-dev libffi-dev git unzip default-jre -y
|
||||
sudo apt install -y make build-essential libssl-dev zlib1g-dev libbz2-dev libreadline-dev libsqlite3-dev wget curl llvm libncurses5-dev libncursesw5-dev xz-utils tk-dev libffi-dev git unzip default-jre
|
||||
curl -L https://github.com/pyenv/pyenv-installer/raw/master/bin/pyenv-installer | bash
|
||||
|
||||
After that last command, you may see a warning about 'pyenv' not being in the load path. Follow the instructions given to fix that, then close and reopen your shell
|
||||
|
||||
Then run the following command:
|
||||
|
||||
.. code-block:: none
|
||||
|
||||
CONFIGURE_OPTS=--enable-optimizations pyenv install 3.6.5 -v
|
||||
|
||||
This may take a long time to complete.
|
||||
|
||||
After that is finished, run:
|
||||
|
||||
.. code-block:: none
|
||||
|
||||
pyenv global 3.6.5
|
||||
|
||||
--------------
|
||||
Installing Red
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
.. ubuntu install guide
|
||||
.. ubuntu bionic install guide
|
||||
|
||||
==============================
|
||||
Installing Red on Ubuntu 16.04
|
||||
Installing Red on Ubuntu 18.04
|
||||
==============================
|
||||
|
||||
.. warning:: For safety reasons, DO NOT install Red with a root user. Instead, `make a new one <http://manpages.ubuntu.com/manpages/artful/man8/adduser.8.html>`_.
|
||||
@@ -12,7 +12,7 @@ Installing the pre-requirements
|
||||
|
||||
.. code-block:: none
|
||||
|
||||
sudo apt install python3.5-dev python3-pip build-essential libssl-dev libffi-dev git unzip default-jre -y
|
||||
sudo apt install python3.6-dev python3-pip build-essential libssl-dev libffi-dev git unzip default-jre -y
|
||||
|
||||
|
||||
------------------
|
||||
59
docs/install_ubuntu_xenial.rst
Normal file
59
docs/install_ubuntu_xenial.rst
Normal file
@@ -0,0 +1,59 @@
|
||||
.. ubuntu xenial install guide
|
||||
|
||||
==============================
|
||||
Installing Red on Ubuntu 16.04
|
||||
==============================
|
||||
|
||||
.. warning:: For safety reasons, DO NOT install Red with a root user. Instead, `make a new one <http://manpages.ubuntu.com/manpages/artful/man8/adduser.8.html>`_.
|
||||
|
||||
-------------------------------
|
||||
Installing the pre-requirements
|
||||
-------------------------------
|
||||
|
||||
.. code-block:: none
|
||||
|
||||
sudo apt install software-properties-common
|
||||
sudo add-apt-repository ppa:deadsnakes/ppa
|
||||
sudo apt update
|
||||
sudo apt install python3.6-dev build-essential libssl-dev libffi-dev git unzip default-jre wget -y
|
||||
wget https://bootstrap.pypa.io/get-pip.py
|
||||
sudo python3.6 get-pip.py
|
||||
|
||||
|
||||
------------------
|
||||
Installing the bot
|
||||
------------------
|
||||
|
||||
To install without audio:
|
||||
|
||||
:code:`pip3.6 install -U --process-dependency-links red-discordbot --user`
|
||||
|
||||
To install with audio:
|
||||
|
||||
:code:`pip3.6 install -U --process-dependency-links red-discordbot[voice] --user`
|
||||
|
||||
To install the development version (without audio):
|
||||
|
||||
:code:`pip3.6 install -U --process-dependency-links git+https://github.com/Cog-Creators/Red-DiscordBot@V3/develop#egg=red-discordbot --user`
|
||||
|
||||
To install the development version (with audio):
|
||||
|
||||
:code:`pip3.6 install -U --process-dependency-links git+https://github.com/Cog-Creators/Red-DiscordBot@V3/develop#egg=red-discordbot[voice] --user`
|
||||
|
||||
------------------------
|
||||
Setting up your instance
|
||||
------------------------
|
||||
|
||||
Run :code:`redbot-setup` and follow the prompts. It will ask first for where you want to
|
||||
store the data (the default is :code:`~/.local/share/Red-DiscordBot`) and will then ask
|
||||
for confirmation of that selection. Next, it will ask you to choose your storage backend
|
||||
(the default here is JSON). It will then ask for a name for your instance. This can be
|
||||
anything as long as it does not contain spaces; however, keep in mind that this is the
|
||||
name you will use to run your bot, and so it should be something you can remember.
|
||||
|
||||
-----------
|
||||
Running Red
|
||||
-----------
|
||||
|
||||
Run :code:`redbot <your instance name>` and run through the initial setup. This will ask for
|
||||
your token and a prefix.
|
||||
@@ -8,11 +8,7 @@ Installing Red on Windows
|
||||
Needed Software
|
||||
---------------
|
||||
|
||||
* `Python <https://python.org/downloads/>`_ - Red needs at least Python 3.5
|
||||
|
||||
.. attention:: Please note that 3.6 has issues on some versions of Windows.
|
||||
If you try using Red with 3.6 and experience issues, uninstall
|
||||
Python 3.6 and install the latest version of Python 3.5
|
||||
* `Python <https://python.org/downloads/>`_ - Red needs Python 3.6
|
||||
|
||||
.. note:: Please make sure that the box to add Python to PATH is CHECKED, otherwise
|
||||
you may run into issues when trying to run Red
|
||||
@@ -44,4 +40,4 @@ Installing Red
|
||||
running the bot)
|
||||
|
||||
4. Once done setting up the instance, run :code:`redbot <your instance name>` to run Red.
|
||||
It will walk through the initial setup, asking for your token and a prefix
|
||||
It will walk through the initial setup, asking for your token and a prefix
|
||||
|
||||
@@ -1,4 +1,37 @@
|
||||
sphinx==1.6.5
|
||||
sphinxcontrib-asyncio
|
||||
sphinx_rtd_theme
|
||||
git+https://github.com/Rapptz/discord.py@rewrite#egg=discord.py[voice]
|
||||
-i https://pypi.org/simple
|
||||
alabaster==0.7.10
|
||||
appdirs==1.4.3
|
||||
atomicwrites==1.1.5
|
||||
attrs==18.1.0
|
||||
babel==2.6.0
|
||||
black==18.6b2
|
||||
certifi==2018.4.16
|
||||
chardet==3.0.4
|
||||
click==6.7
|
||||
docutils==0.14
|
||||
idna==2.6
|
||||
imagesize==1.0.0
|
||||
jinja2==2.10
|
||||
markupsafe==1.0
|
||||
more-itertools==4.2.0
|
||||
packaging==17.1
|
||||
pluggy==0.6.0
|
||||
py==1.5.3
|
||||
pygments==2.2.0
|
||||
pyparsing==2.2.0
|
||||
pytest-asyncio==0.8.0
|
||||
pytest==3.6.1
|
||||
pytz==2018.4
|
||||
requests==2.18.4
|
||||
six==1.11.0
|
||||
snowballstemmer==1.2.1
|
||||
sphinx-rtd-theme==0.4.0
|
||||
sphinx==1.7.5
|
||||
sphinxcontrib-asyncio==0.2.0
|
||||
sphinxcontrib-websupport==1.1.0
|
||||
toml==0.9.4
|
||||
tox==3.0.0
|
||||
urllib3==1.22
|
||||
virtualenv==16.0.0
|
||||
yarl==0.18.0
|
||||
git+https://github.com/Rapptz/discord.py@7eb918b19e3e60b56eb9039eb267f8f3477c5e17#egg=discord.py-1.0
|
||||
|
||||
14
generate_strings.py
Normal file → Executable file
14
generate_strings.py
Normal file → Executable file
@@ -1,3 +1,4 @@
|
||||
#!/usr/bin/env python3
|
||||
import subprocess
|
||||
import os
|
||||
import sys
|
||||
@@ -12,22 +13,25 @@ def main():
|
||||
if "locales" in os.listdir(os.path.join("redbot/cogs", d)):
|
||||
os.chdir(os.path.join("redbot/cogs", d, "locales"))
|
||||
if "regen_messages.py" not in os.listdir(os.getcwd()):
|
||||
print("Directory 'locales' exists for {} but no 'regen_messages.py' is available!".format(d))
|
||||
exit(1)
|
||||
print(
|
||||
f"Directory 'locales' exists for {d} but no 'regen_messages.py' is available!"
|
||||
)
|
||||
return 1
|
||||
else:
|
||||
print("Running 'regen_messages.py' for {}".format(d))
|
||||
retval = subprocess.run([interpreter, "regen_messages.py"])
|
||||
if retval.returncode != 0:
|
||||
exit(1)
|
||||
return 1
|
||||
os.chdir(root_dir)
|
||||
os.chdir("redbot/core/locales")
|
||||
print("Running 'regen_messages.py' for core")
|
||||
retval = subprocess.run([interpreter, "regen_messages.py"])
|
||||
if retval.returncode != 0:
|
||||
exit(1)
|
||||
return 1
|
||||
os.chdir(root_dir)
|
||||
subprocess.run(["crowdin", "upload"])
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
sys.exit(main())
|
||||
|
||||
30
make.bat
Normal file
30
make.bat
Normal file
@@ -0,0 +1,30 @@
|
||||
@echo off
|
||||
|
||||
if "%1"=="" goto help
|
||||
|
||||
REM This allows us to expand variables at execution
|
||||
setlocal ENABLEDELAYEDEXPANSION
|
||||
|
||||
REM This will set PYFILES as a list of tracked .py files
|
||||
set PYFILES=
|
||||
for /F "tokens=* USEBACKQ" %%A in (`git ls-files "*.py"`) do (
|
||||
set PYFILES=!PYFILES! %%A
|
||||
)
|
||||
|
||||
goto %1
|
||||
|
||||
:reformat
|
||||
black -l 99 !PYFILES!
|
||||
exit /B %ERRORLEVEL%
|
||||
|
||||
:stylecheck
|
||||
black -l 99 --check !PYFILES!
|
||||
exit /B %ERRORLEVEL%
|
||||
|
||||
:help
|
||||
echo Usage:
|
||||
echo make ^<command^>
|
||||
echo.
|
||||
echo Commands:
|
||||
echo reformat Reformat all .py files being tracked by git.
|
||||
echo stylecheck Check which tracked .py files need reformatting.
|
||||
@@ -1,11 +1,14 @@
|
||||
import sys
|
||||
import typing
|
||||
import discord
|
||||
from colorama import init, Back
|
||||
|
||||
init()
|
||||
# Let's do all the dumb version checking in one place.
|
||||
|
||||
if discord.version_info.major < 1:
|
||||
print("You are not running the rewritten version of discord.py.\n\n"
|
||||
"In order to use Red v3 you MUST be running d.py version"
|
||||
" >= 1.0.0.")
|
||||
print(
|
||||
"You are not running the rewritten version of discord.py.\n\n"
|
||||
"In order to use Red v3 you MUST be running d.py version"
|
||||
" >= 1.0.0."
|
||||
)
|
||||
sys.exit(1)
|
||||
|
||||
@@ -6,14 +6,14 @@ import sys
|
||||
import discord
|
||||
from redbot.core.bot import Red, ExitCodes
|
||||
from redbot.core.cog_manager import CogManagerUI
|
||||
from redbot.core.data_manager import load_basic_configuration, config_file
|
||||
from redbot.core.data_manager import create_temp_config, load_basic_configuration, config_file
|
||||
from redbot.core.json_io import JsonIO
|
||||
from redbot.core.global_checks import init_global_checks
|
||||
from redbot.core.events import init_events
|
||||
from redbot.core.cli import interactive_config, confirm, parse_cli_flags, ask_sentry
|
||||
from redbot.core.core_commands import Core
|
||||
from redbot.core.dev_commands import Dev
|
||||
from redbot.core import rpc, __version__
|
||||
from redbot.core import __version__
|
||||
import asyncio
|
||||
import logging.handlers
|
||||
import logging
|
||||
@@ -40,24 +40,25 @@ def init_loggers(cli_flags):
|
||||
logger = logging.getLogger("red")
|
||||
|
||||
red_format = logging.Formatter(
|
||||
'%(asctime)s %(levelname)s %(module)s %(funcName)s %(lineno)d: '
|
||||
'%(message)s',
|
||||
datefmt="[%d/%m/%Y %H:%M]")
|
||||
"%(asctime)s %(levelname)s %(module)s %(funcName)s %(lineno)d: %(message)s",
|
||||
datefmt="[%d/%m/%Y %H:%M]",
|
||||
)
|
||||
|
||||
stdout_handler = logging.StreamHandler(sys.stdout)
|
||||
stdout_handler.setFormatter(red_format)
|
||||
|
||||
if cli_flags.debug:
|
||||
os.environ['PYTHONASYNCIODEBUG'] = '1'
|
||||
os.environ["PYTHONASYNCIODEBUG"] = "1"
|
||||
logger.setLevel(logging.DEBUG)
|
||||
else:
|
||||
logger.setLevel(logging.WARNING)
|
||||
|
||||
from redbot.core.data_manager import core_data_path
|
||||
logfile_path = core_data_path() / 'red.log'
|
||||
|
||||
logfile_path = core_data_path() / "red.log"
|
||||
fhandler = logging.handlers.RotatingFileHandler(
|
||||
filename=str(logfile_path), encoding='utf-8', mode='a',
|
||||
maxBytes=10**7, backupCount=5)
|
||||
filename=str(logfile_path), encoding="utf-8", mode="a", maxBytes=10 ** 7, backupCount=5
|
||||
)
|
||||
fhandler.setFormatter(red_format)
|
||||
|
||||
logger.addHandler(fhandler)
|
||||
@@ -76,15 +77,17 @@ async def _get_prefix_and_token(red, indict):
|
||||
:param indict:
|
||||
:return:
|
||||
"""
|
||||
indict['token'] = await red.db.token()
|
||||
indict['prefix'] = await red.db.prefix()
|
||||
indict['enable_sentry'] = await red.db.enable_sentry()
|
||||
indict["token"] = await red.db.token()
|
||||
indict["prefix"] = await red.db.prefix()
|
||||
indict["enable_sentry"] = await red.db.enable_sentry()
|
||||
|
||||
|
||||
def list_instances():
|
||||
if not config_file.exists():
|
||||
print("No instances have been configured! Configure one "
|
||||
"using `redbot-setup` before trying to run the bot!")
|
||||
print(
|
||||
"No instances have been configured! Configure one "
|
||||
"using `redbot-setup` before trying to run the bot!"
|
||||
)
|
||||
sys.exit(1)
|
||||
else:
|
||||
data = JsonIO(config_file)._load_json()
|
||||
@@ -103,12 +106,20 @@ def main():
|
||||
elif cli_flags.version:
|
||||
print(description)
|
||||
sys.exit(0)
|
||||
elif not cli_flags.instance_name:
|
||||
elif not cli_flags.instance_name and not cli_flags.no_instance:
|
||||
print("Error: No instance name was provided!")
|
||||
sys.exit(1)
|
||||
if cli_flags.no_instance:
|
||||
print(
|
||||
"\033[1m"
|
||||
"Warning: The data will be placed in a temporary folder and removed on next system reboot."
|
||||
"\033[0m"
|
||||
)
|
||||
cli_flags.instance_name = "temporary_red"
|
||||
create_temp_config()
|
||||
load_basic_configuration(cli_flags.instance_name)
|
||||
log, sentry_log = init_loggers(cli_flags)
|
||||
red = Red(cli_flags, description=description, pm_help=None)
|
||||
red = Red(cli_flags=cli_flags, description=description, pm_help=None)
|
||||
init_global_checks(red)
|
||||
init_events(red, cli_flags)
|
||||
red.add_cog(Core(red))
|
||||
@@ -118,30 +129,35 @@ def main():
|
||||
loop = asyncio.get_event_loop()
|
||||
tmp_data = {}
|
||||
loop.run_until_complete(_get_prefix_and_token(red, tmp_data))
|
||||
token = os.environ.get("RED_TOKEN", tmp_data['token'])
|
||||
prefix = cli_flags.prefix or tmp_data['prefix']
|
||||
if token is None or not prefix:
|
||||
token = os.environ.get("RED_TOKEN", tmp_data["token"])
|
||||
if cli_flags.token:
|
||||
token = cli_flags.token
|
||||
prefix = cli_flags.prefix or tmp_data["prefix"]
|
||||
if not (token and prefix):
|
||||
if cli_flags.no_prompt is False:
|
||||
new_token = interactive_config(red, token_set=bool(token),
|
||||
prefix_set=bool(prefix))
|
||||
new_token = interactive_config(red, token_set=bool(token), prefix_set=bool(prefix))
|
||||
if new_token:
|
||||
token = new_token
|
||||
else:
|
||||
log.critical("Token and prefix must be set in order to login.")
|
||||
sys.exit(1)
|
||||
loop.run_until_complete(_get_prefix_and_token(red, tmp_data))
|
||||
if tmp_data['enable_sentry']:
|
||||
|
||||
if cli_flags.dry_run:
|
||||
loop.run_until_complete(red.http.close())
|
||||
sys.exit(0)
|
||||
if tmp_data["enable_sentry"]:
|
||||
red.enable_sentry()
|
||||
cleanup_tasks = True
|
||||
try:
|
||||
loop.run_until_complete(red.start(token, bot=not cli_flags.not_bot))
|
||||
except discord.LoginFailure:
|
||||
cleanup_tasks = False # No login happened, no need for this
|
||||
log.critical("This token doesn't seem to be valid. If it belongs to "
|
||||
"a user account, remember that the --not-bot flag "
|
||||
"must be used. For self-bot functionalities instead, "
|
||||
"--self-bot")
|
||||
db_token = red.db.token()
|
||||
log.critical(
|
||||
"This token doesn't seem to be valid. If it belongs to "
|
||||
"a user account, remember that the --not-bot flag "
|
||||
"must be used. For self-bot functionalities instead, "
|
||||
"--self-bot"
|
||||
)
|
||||
db_token = loop.run_until_complete(red.db.token())
|
||||
if db_token and not cli_flags.no_prompt:
|
||||
print("\nDo you want to reset the token? (y/n)")
|
||||
if confirm("> "):
|
||||
@@ -156,15 +172,16 @@ def main():
|
||||
sentry_log.critical("Fatal Exception", exc_info=e)
|
||||
loop.run_until_complete(red.logout())
|
||||
finally:
|
||||
rpc.clean_up()
|
||||
if cleanup_tasks:
|
||||
pending = asyncio.Task.all_tasks(loop=red.loop)
|
||||
gathered = asyncio.gather(
|
||||
*pending, loop=red.loop, return_exceptions=True)
|
||||
gathered.cancel()
|
||||
pending = asyncio.Task.all_tasks(loop=red.loop)
|
||||
gathered = asyncio.gather(*pending, loop=red.loop, return_exceptions=True)
|
||||
gathered.cancel()
|
||||
try:
|
||||
red.rpc.server.close()
|
||||
except AttributeError:
|
||||
pass
|
||||
|
||||
sys.exit(red._shutdown_mode.value)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
from typing import Tuple
|
||||
|
||||
import discord
|
||||
from discord.ext import commands
|
||||
|
||||
from redbot.core import Config, checks
|
||||
from redbot.core import Config, checks, commands
|
||||
|
||||
import logging
|
||||
|
||||
@@ -20,14 +19,14 @@ GENERIC_FORBIDDEN = (
|
||||
|
||||
HIERARCHY_ISSUE = (
|
||||
"I tried to add {role.name} to {member.display_name} but that role"
|
||||
" is higher than my highest role in the Discord heirarchy so I was"
|
||||
" is higher than my highest role in the Discord hierarchy so I was"
|
||||
" unable to successfully add it. Please give me a higher role and "
|
||||
"try again."
|
||||
)
|
||||
|
||||
USER_HIERARCHY_ISSUE = (
|
||||
"I tried to add {role.name} to {member.display_name} but that role"
|
||||
" is higher than your highest role in the Discord heirarchy so I was"
|
||||
" is higher than your highest role in the Discord hierarchy so I was"
|
||||
" unable to successfully add it. Please get a higher role and "
|
||||
"try again."
|
||||
)
|
||||
@@ -41,17 +40,14 @@ RUNNING_ANNOUNCEMENT = (
|
||||
|
||||
class Admin:
|
||||
def __init__(self, config=Config):
|
||||
self.conf = config.get_conf(self, 8237492837454039,
|
||||
force_registration=True)
|
||||
self.conf = config.get_conf(self, 8237492837454039, force_registration=True)
|
||||
|
||||
self.conf.register_global(
|
||||
serverlocked=False
|
||||
)
|
||||
self.conf.register_global(serverlocked=False)
|
||||
|
||||
self.conf.register_guild(
|
||||
announce_ignore=False,
|
||||
announce_channel=None, # Integer ID
|
||||
selfroles=[] # List of integer ID's
|
||||
selfroles=[], # List of integer ID's
|
||||
)
|
||||
|
||||
self.__current_announcer = None
|
||||
@@ -63,8 +59,7 @@ class Admin:
|
||||
pass
|
||||
|
||||
@staticmethod
|
||||
async def complain(ctx: commands.Context, message: str,
|
||||
**kwargs):
|
||||
async def complain(ctx: commands.Context, message: str, **kwargs):
|
||||
await ctx.send(message.format(**kwargs))
|
||||
|
||||
def is_announcing(self) -> bool:
|
||||
@@ -78,8 +73,7 @@ class Admin:
|
||||
return self.__current_announcer.active or False
|
||||
|
||||
@staticmethod
|
||||
def pass_heirarchy_check(ctx: commands.Context,
|
||||
role: discord.Role) -> bool:
|
||||
def pass_hierarchy_check(ctx: commands.Context, role: discord.Role) -> bool:
|
||||
"""
|
||||
Determines if the bot has a higher role than the given one.
|
||||
:param ctx:
|
||||
@@ -89,8 +83,7 @@ class Admin:
|
||||
return ctx.guild.me.top_role > role
|
||||
|
||||
@staticmethod
|
||||
def pass_user_heirarchy_check(ctx: commands.Context,
|
||||
role: discord.Role) -> bool:
|
||||
def pass_user_hierarchy_check(ctx: commands.Context, role: discord.Role) -> bool:
|
||||
"""
|
||||
Determines if a user is allowed to add/remove/edit the given role.
|
||||
:param ctx:
|
||||
@@ -99,50 +92,47 @@ class Admin:
|
||||
"""
|
||||
return ctx.author.top_role > role
|
||||
|
||||
async def _addrole(self, ctx: commands.Context, member: discord.Member,
|
||||
role: discord.Role):
|
||||
async def _addrole(self, ctx: commands.Context, member: discord.Member, role: discord.Role):
|
||||
try:
|
||||
await member.add_roles(role)
|
||||
except discord.Forbidden:
|
||||
if not self.pass_heirarchy_check(ctx, role):
|
||||
await self.complain(ctx, HIERARCHY_ISSUE, role=role,
|
||||
member=member)
|
||||
if not self.pass_hierarchy_check(ctx, role):
|
||||
await self.complain(ctx, HIERARCHY_ISSUE, role=role, member=member)
|
||||
else:
|
||||
await self.complain(ctx, GENERIC_FORBIDDEN)
|
||||
else:
|
||||
await ctx.send("I successfully added {role.name} to"
|
||||
" {member.display_name}".format(
|
||||
role=role, member=member
|
||||
))
|
||||
await ctx.send(
|
||||
"I successfully added {role.name} to"
|
||||
" {member.display_name}".format(role=role, member=member)
|
||||
)
|
||||
|
||||
async def _removerole(self, ctx: commands.Context, member: discord.Member,
|
||||
role: discord.Role):
|
||||
async def _removerole(self, ctx: commands.Context, member: discord.Member, role: discord.Role):
|
||||
try:
|
||||
await member.remove_roles(role)
|
||||
except discord.Forbidden:
|
||||
if not self.pass_heirarchy_check(ctx, role):
|
||||
await self.complain(ctx, HIERARCHY_ISSUE, role=role,
|
||||
member=member)
|
||||
if not self.pass_hierarchy_check(ctx, role):
|
||||
await self.complain(ctx, HIERARCHY_ISSUE, role=role, member=member)
|
||||
else:
|
||||
await self.complain(ctx, GENERIC_FORBIDDEN)
|
||||
else:
|
||||
await ctx.send("I successfully removed {role.name} from"
|
||||
" {member.display_name}".format(
|
||||
role=role, member=member
|
||||
))
|
||||
await ctx.send(
|
||||
"I successfully removed {role.name} from"
|
||||
" {member.display_name}".format(role=role, member=member)
|
||||
)
|
||||
|
||||
@commands.command()
|
||||
@commands.guild_only()
|
||||
@checks.admin_or_permissions(manage_roles=True)
|
||||
async def addrole(self, ctx: commands.Context, rolename: discord.Role, *,
|
||||
user: MemberDefaultAuthor=None):
|
||||
async def addrole(
|
||||
self, ctx: commands.Context, rolename: discord.Role, *, user: MemberDefaultAuthor = None
|
||||
):
|
||||
"""
|
||||
Adds a role to a user. If user is left blank it defaults to the
|
||||
author of the command.
|
||||
Adds a role to a user.
|
||||
If user is left blank it defaults to the author of the command.
|
||||
"""
|
||||
if user is None:
|
||||
user = ctx.author
|
||||
if self.pass_user_heirarchy_check(ctx, rolename):
|
||||
if self.pass_user_hierarchy_check(ctx, rolename):
|
||||
# noinspection PyTypeChecker
|
||||
await self._addrole(ctx, user, rolename)
|
||||
else:
|
||||
@@ -151,15 +141,16 @@ class Admin:
|
||||
@commands.command()
|
||||
@commands.guild_only()
|
||||
@checks.admin_or_permissions(manage_roles=True)
|
||||
async def removerole(self, ctx: commands.Context, rolename: discord.Role, *,
|
||||
user: MemberDefaultAuthor=None):
|
||||
async def removerole(
|
||||
self, ctx: commands.Context, rolename: discord.Role, *, user: MemberDefaultAuthor = None
|
||||
):
|
||||
"""
|
||||
Removes a role from a user. If user is left blank it defaults to the
|
||||
author of the command.
|
||||
Removes a role from a user.
|
||||
If user is left blank it defaults to the author of the command.
|
||||
"""
|
||||
if user is None:
|
||||
user = ctx.author
|
||||
if self.pass_user_heirarchy_check(ctx, rolename):
|
||||
if self.pass_user_hierarchy_check(ctx, rolename):
|
||||
# noinspection PyTypeChecker
|
||||
await self._removerole(ctx, user, rolename)
|
||||
else:
|
||||
@@ -170,12 +161,12 @@ class Admin:
|
||||
@checks.admin_or_permissions(manage_roles=True)
|
||||
async def editrole(self, ctx: commands.Context):
|
||||
"""Edits roles settings"""
|
||||
if ctx.invoked_subcommand is None:
|
||||
await ctx.send_help()
|
||||
pass
|
||||
|
||||
@editrole.command(name="colour", aliases=["color", ])
|
||||
async def editrole_colour(self, ctx: commands.Context, role: discord.Role,
|
||||
value: discord.Colour):
|
||||
@editrole.command(name="colour", aliases=["color"])
|
||||
async def editrole_colour(
|
||||
self, ctx: commands.Context, role: discord.Role, value: discord.Colour
|
||||
):
|
||||
"""Edits a role's colour
|
||||
|
||||
Use double quotes if the role contains spaces.
|
||||
@@ -185,10 +176,9 @@ class Admin:
|
||||
!editrole colour \"The Transistor\" #ff0000
|
||||
!editrole colour Test #ff9900"""
|
||||
author = ctx.author
|
||||
reason = "{}({}) changed the colour of role '{}'".format(
|
||||
author.name, author.id, role.name)
|
||||
reason = "{}({}) changed the colour of role '{}'".format(author.name, author.id, role.name)
|
||||
|
||||
if not self.pass_user_heirarchy_check(ctx, role):
|
||||
if not self.pass_user_hierarchy_check(ctx, role):
|
||||
await self.complain(ctx, USER_HIERARCHY_ISSUE)
|
||||
return
|
||||
|
||||
@@ -211,9 +201,10 @@ class Admin:
|
||||
author = ctx.message.author
|
||||
old_name = role.name
|
||||
reason = "{}({}) changed the name of role '{}' to '{}'".format(
|
||||
author.name, author.id, old_name, name)
|
||||
author.name, author.id, old_name, name
|
||||
)
|
||||
|
||||
if not self.pass_user_heirarchy_check(ctx, role):
|
||||
if not self.pass_user_hierarchy_check(ctx, role):
|
||||
await self.complain(ctx, USER_HIERARCHY_ISSUE)
|
||||
return
|
||||
|
||||
@@ -240,8 +231,7 @@ class Admin:
|
||||
await ctx.send("The announcement has begun.")
|
||||
else:
|
||||
prefix = ctx.prefix
|
||||
await self.complain(ctx, RUNNING_ANNOUNCEMENT,
|
||||
prefix=prefix)
|
||||
await self.complain(ctx, RUNNING_ANNOUNCEMENT, prefix=prefix)
|
||||
|
||||
@announce.command(name="cancel")
|
||||
@checks.is_owner()
|
||||
@@ -259,7 +249,7 @@ class Admin:
|
||||
@announce.command(name="channel")
|
||||
@commands.guild_only()
|
||||
@checks.guildowner_or_permissions(administrator=True)
|
||||
async def announce_channel(self, ctx, *, channel: discord.TextChannel=None):
|
||||
async def announce_channel(self, ctx, *, channel: discord.TextChannel = None):
|
||||
"""
|
||||
Changes the channel on which the bot makes announcements.
|
||||
"""
|
||||
@@ -267,29 +257,21 @@ class Admin:
|
||||
channel = ctx.channel
|
||||
await self.conf.guild(ctx.guild).announce_channel.set(channel.id)
|
||||
|
||||
await ctx.send("The announcement channel has been set to {}".format(
|
||||
channel.mention
|
||||
))
|
||||
await ctx.send("The announcement channel has been set to {}".format(channel.mention))
|
||||
|
||||
@announce.command(name="ignore")
|
||||
@commands.guild_only()
|
||||
@checks.guildowner_or_permissions(administrator=True)
|
||||
async def announce_ignore(self, ctx, *, guild: discord.Guild=None):
|
||||
async def announce_ignore(self, ctx):
|
||||
"""
|
||||
Toggles whether the announcements will ignore the given server.
|
||||
Defaults to the current server if none is provided.
|
||||
Toggles whether the announcements will ignore the current server.
|
||||
"""
|
||||
if guild is None:
|
||||
guild = ctx.guild
|
||||
|
||||
ignored = await self.conf.guild(guild).announce_ignore()
|
||||
await self.conf.guild(guild).announce_ignore.set(not ignored)
|
||||
ignored = await self.conf.guild(ctx.guild).announce_ignore()
|
||||
await self.conf.guild(ctx.guild).announce_ignore.set(not ignored)
|
||||
|
||||
verb = "will" if ignored else "will not"
|
||||
|
||||
await ctx.send("The server {} {} receive announcements.".format(
|
||||
guild.name, verb
|
||||
))
|
||||
await ctx.send(f"The server {ctx.guild.name} {verb} receive announcements.")
|
||||
|
||||
async def _valid_selfroles(self, guild: discord.Guild) -> Tuple[discord.Role]:
|
||||
"""
|
||||
@@ -309,11 +291,13 @@ class Admin:
|
||||
# noinspection PyTypeChecker
|
||||
return valid_roles
|
||||
|
||||
@commands.guild_only()
|
||||
@commands.group(invoke_without_command=True)
|
||||
async def selfrole(self, ctx: commands.Context, *, selfrole: SelfRole):
|
||||
"""
|
||||
Add a role to yourself that server admins have configured as
|
||||
user settable.
|
||||
Add a role to yourself that server admins have configured as user settable.
|
||||
|
||||
NOTE: The role is case sensitive!
|
||||
"""
|
||||
# noinspection PyTypeChecker
|
||||
await self._addrole(ctx, ctx.author, selfrole)
|
||||
@@ -322,15 +306,19 @@ class Admin:
|
||||
async def selfrole_remove(self, ctx: commands.Context, *, selfrole: SelfRole):
|
||||
"""
|
||||
Removes a selfrole from yourself.
|
||||
|
||||
NOTE: The role is case sensitive!
|
||||
"""
|
||||
# noinspection PyTypeChecker
|
||||
await self._removerole(ctx, ctx.author, selfrole)
|
||||
|
||||
@selfrole.command(name="add")
|
||||
@commands.has_permissions(manage_roles=True)
|
||||
@checks.admin_or_permissions(manage_roles=True)
|
||||
async def selfrole_add(self, ctx: commands.Context, *, role: discord.Role):
|
||||
"""
|
||||
Add a role to the list of available selfroles.
|
||||
|
||||
NOTE: The role is case sensitive!
|
||||
"""
|
||||
async with self.conf.guild(ctx.guild).selfroles() as curr_selfroles:
|
||||
if role.id not in curr_selfroles:
|
||||
@@ -339,10 +327,12 @@ class Admin:
|
||||
await ctx.send("The selfroles list has been successfully modified.")
|
||||
|
||||
@selfrole.command(name="delete")
|
||||
@commands.has_permissions(manage_roles=True)
|
||||
@checks.admin_or_permissions(manage_roles=True)
|
||||
async def selfrole_delete(self, ctx: commands.Context, *, role: SelfRole):
|
||||
"""
|
||||
Removes a role from the list of available selfroles.
|
||||
|
||||
NOTE: The role is case sensitive!
|
||||
"""
|
||||
async with self.conf.guild(ctx.guild).selfroles() as curr_selfroles:
|
||||
curr_selfroles.remove(role.id)
|
||||
@@ -384,8 +374,10 @@ class Admin:
|
||||
|
||||
await ctx.send("The bot {} serverlocked.".format(verb))
|
||||
|
||||
# region Event Handlers
|
||||
# region Event Handlers
|
||||
async def on_guild_join(self, guild: discord.Guild):
|
||||
if await self._serverlock_check(guild):
|
||||
return
|
||||
|
||||
|
||||
# endregion
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
import asyncio
|
||||
|
||||
import discord
|
||||
from discord.ext import commands
|
||||
from redbot.core import commands
|
||||
|
||||
|
||||
class Announcer:
|
||||
def __init__(self, ctx: commands.Context,
|
||||
message: str,
|
||||
config=None):
|
||||
def __init__(self, ctx: commands.Context, message: str, config=None):
|
||||
"""
|
||||
:param ctx:
|
||||
:param message:
|
||||
@@ -65,10 +63,7 @@ class Announcer:
|
||||
try:
|
||||
await channel.send(self.message)
|
||||
except discord.Forbidden:
|
||||
await bot_owner.send("I could not announce to server: {}".format(
|
||||
g.id
|
||||
))
|
||||
await bot_owner.send("I could not announce to server: {}".format(g.id))
|
||||
await asyncio.sleep(0.5)
|
||||
|
||||
self.active = False
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import discord
|
||||
from discord.ext import commands
|
||||
from redbot.core import commands
|
||||
|
||||
|
||||
class MemberDefaultAuthor(commands.Converter):
|
||||
@@ -28,6 +28,5 @@ class SelfRole(commands.Converter):
|
||||
role = await role_converter.convert(ctx, arg)
|
||||
|
||||
if role.id not in selfroles:
|
||||
raise commands.BadArgument("The provided role is not a valid"
|
||||
" selfrole.")
|
||||
raise commands.BadArgument("The provided role is not a valid selfrole.")
|
||||
return role
|
||||
|
||||
@@ -1,14 +1,10 @@
|
||||
import subprocess
|
||||
|
||||
TO_TRANSLATE = [
|
||||
'../admin.py'
|
||||
]
|
||||
TO_TRANSLATE = ["../admin.py"]
|
||||
|
||||
|
||||
def regen_messages():
|
||||
subprocess.run(
|
||||
['pygettext', '-n'] + TO_TRANSLATE
|
||||
)
|
||||
subprocess.run(["pygettext", "-n"] + TO_TRANSLATE)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
from .alias import Alias
|
||||
from discord.ext import commands
|
||||
from redbot.core.bot import Red
|
||||
|
||||
|
||||
def setup(bot: commands.Bot):
|
||||
def setup(bot: Red):
|
||||
bot.add_cog(Alias(bot))
|
||||
|
||||
@@ -3,17 +3,17 @@ from re import search
|
||||
from typing import Generator, Tuple, Iterable
|
||||
|
||||
import discord
|
||||
from redbot.core import Config
|
||||
from redbot.core.i18n import CogI18n
|
||||
from redbot.core import Config, commands, checks
|
||||
from redbot.core.i18n import Translator, cog_i18n
|
||||
from redbot.core.utils.chat_formatting import box
|
||||
from discord.ext import commands
|
||||
|
||||
from redbot.core.bot import Red
|
||||
from .alias_entry import AliasEntry
|
||||
|
||||
_ = CogI18n("Alias", __file__)
|
||||
_ = Translator("Alias", __file__)
|
||||
|
||||
|
||||
@cog_i18n(_)
|
||||
class Alias:
|
||||
"""
|
||||
Alias
|
||||
@@ -26,14 +26,9 @@ class Alias:
|
||||
and append them to the stored alias
|
||||
"""
|
||||
|
||||
default_global_settings = {
|
||||
"entries": []
|
||||
}
|
||||
default_global_settings = {"entries": []}
|
||||
|
||||
default_guild_settings = {
|
||||
"enabled": False,
|
||||
"entries": [] # Going to be a list of dicts
|
||||
}
|
||||
default_guild_settings = {"enabled": False, "entries": []} # Going to be a list of dicts
|
||||
|
||||
def __init__(self, bot: Red):
|
||||
self.bot = bot
|
||||
@@ -49,14 +44,17 @@ class Alias:
|
||||
return (AliasEntry.from_json(d) for d in (await self._aliases.entries()))
|
||||
|
||||
async def loaded_aliases(self, guild: discord.Guild) -> Generator[AliasEntry, None, None]:
|
||||
return (AliasEntry.from_json(d, bot=self.bot)
|
||||
for d in (await self._aliases.guild(guild).entries()))
|
||||
return (
|
||||
AliasEntry.from_json(d, bot=self.bot)
|
||||
for d in (await self._aliases.guild(guild).entries())
|
||||
)
|
||||
|
||||
async def loaded_global_aliases(self) -> Generator[AliasEntry, None, None]:
|
||||
return (AliasEntry.from_json(d, bot=self.bot) for d in (await self._aliases.entries()))
|
||||
|
||||
async def is_alias(self, guild: discord.Guild, alias_name: str,
|
||||
server_aliases: Iterable[AliasEntry]=()) -> (bool, AliasEntry):
|
||||
async def is_alias(
|
||||
self, guild: discord.Guild, alias_name: str, server_aliases: Iterable[AliasEntry] = ()
|
||||
) -> (bool, AliasEntry):
|
||||
|
||||
if not server_aliases:
|
||||
server_aliases = await self.unloaded_aliases(guild)
|
||||
@@ -76,10 +74,11 @@ class Alias:
|
||||
|
||||
@staticmethod
|
||||
def is_valid_alias_name(alias_name: str) -> bool:
|
||||
return not bool(search(r'\s', alias_name)) and alias_name.isprintable()
|
||||
return not bool(search(r"\s", alias_name)) and alias_name.isprintable()
|
||||
|
||||
async def add_alias(self, ctx: commands.Context, alias_name: str,
|
||||
command: Tuple[str], global_: bool=False) -> AliasEntry:
|
||||
async def add_alias(
|
||||
self, ctx: commands.Context, alias_name: str, command: Tuple[str], global_: bool = False
|
||||
) -> AliasEntry:
|
||||
alias = AliasEntry(alias_name, command, ctx.author, global_=global_)
|
||||
|
||||
if global_:
|
||||
@@ -93,8 +92,9 @@ class Alias:
|
||||
|
||||
return alias
|
||||
|
||||
async def delete_alias(self, ctx: commands.Context, alias_name: str,
|
||||
global_: bool=False) -> bool:
|
||||
async def delete_alias(
|
||||
self, ctx: commands.Context, alias_name: str, global_: bool = False
|
||||
) -> bool:
|
||||
if global_:
|
||||
settings = self._aliases
|
||||
else:
|
||||
@@ -120,16 +120,15 @@ class Alias:
|
||||
"""
|
||||
content = message.content
|
||||
prefix_list = await self.bot.command_prefix(self.bot, message)
|
||||
prefixes = sorted(prefix_list,
|
||||
key=lambda pfx: len(pfx),
|
||||
reverse=True)
|
||||
prefixes = sorted(prefix_list, key=lambda pfx: len(pfx), reverse=True)
|
||||
for p in prefixes:
|
||||
if content.startswith(p):
|
||||
return p
|
||||
raise ValueError(_("No prefix found."))
|
||||
|
||||
def get_extra_args_from_alias(self, message: discord.Message, prefix: str,
|
||||
alias: AliasEntry) -> str:
|
||||
def get_extra_args_from_alias(
|
||||
self, message: discord.Message, prefix: str, alias: AliasEntry
|
||||
) -> str:
|
||||
"""
|
||||
When an alias is executed by a user in chat this function tries
|
||||
to get any extra arguments passed in with the call.
|
||||
@@ -143,25 +142,27 @@ class Alias:
|
||||
extra = message.content[known_content_length:].strip()
|
||||
return extra
|
||||
|
||||
async def maybe_call_alias(self, message: discord.Message,
|
||||
aliases: Iterable[AliasEntry]=None):
|
||||
async def maybe_call_alias(
|
||||
self, message: discord.Message, aliases: Iterable[AliasEntry] = None
|
||||
):
|
||||
try:
|
||||
prefix = await self.get_prefix(message)
|
||||
except ValueError:
|
||||
return
|
||||
|
||||
try:
|
||||
potential_alias = message.content[len(prefix):].split(" ")[0]
|
||||
potential_alias = message.content[len(prefix) :].split(" ")[0]
|
||||
except IndexError:
|
||||
return False
|
||||
|
||||
is_alias, alias = await self.is_alias(message.guild, potential_alias, server_aliases=aliases)
|
||||
is_alias, alias = await self.is_alias(
|
||||
message.guild, potential_alias, server_aliases=aliases
|
||||
)
|
||||
|
||||
if is_alias:
|
||||
await self.call_alias(message, prefix, alias)
|
||||
|
||||
async def call_alias(self, message: discord.Message, prefix: str,
|
||||
alias: AliasEntry):
|
||||
async def call_alias(self, message: discord.Message, prefix: str, alias: AliasEntry):
|
||||
new_message = copy(message)
|
||||
args = self.get_extra_args_from_alias(message, prefix, alias)
|
||||
|
||||
@@ -173,97 +174,118 @@ class Alias:
|
||||
@commands.guild_only()
|
||||
async def alias(self, ctx: commands.Context):
|
||||
"""Manage per-server aliases for commands"""
|
||||
if ctx.invoked_subcommand is None:
|
||||
await ctx.send_help()
|
||||
pass
|
||||
|
||||
@alias.group(name="global")
|
||||
async def global_(self, ctx: commands.Context):
|
||||
"""
|
||||
Manage global aliases.
|
||||
"""
|
||||
if ctx.invoked_subcommand is None or \
|
||||
isinstance(ctx.invoked_subcommand, commands.Group):
|
||||
await ctx.send_help()
|
||||
pass
|
||||
|
||||
@checks.mod_or_permissions(manage_guild=True)
|
||||
@alias.command(name="add")
|
||||
@commands.guild_only()
|
||||
async def _add_alias(self, ctx: commands.Context,
|
||||
alias_name: str, *, command):
|
||||
async def _add_alias(self, ctx: commands.Context, alias_name: str, *, command):
|
||||
"""
|
||||
Add an alias for a command.
|
||||
"""
|
||||
# region Alias Add Validity Checking
|
||||
# region Alias Add Validity Checking
|
||||
is_command = self.is_command(alias_name)
|
||||
if is_command:
|
||||
await ctx.send(_("You attempted to create a new alias"
|
||||
" with the name {} but that"
|
||||
" name is already a command on this bot.").format(alias_name))
|
||||
await ctx.send(
|
||||
_(
|
||||
"You attempted to create a new alias"
|
||||
" with the name {} but that"
|
||||
" name is already a command on this bot."
|
||||
).format(alias_name)
|
||||
)
|
||||
return
|
||||
|
||||
is_alias, something_useless = await self.is_alias(ctx.guild, alias_name)
|
||||
if is_alias:
|
||||
await ctx.send(_("You attempted to create a new alias"
|
||||
" with the name {} but that"
|
||||
" alias already exists on this server.").format(alias_name))
|
||||
await ctx.send(
|
||||
_(
|
||||
"You attempted to create a new alias"
|
||||
" with the name {} but that"
|
||||
" alias already exists on this server."
|
||||
).format(alias_name)
|
||||
)
|
||||
return
|
||||
|
||||
is_valid_name = self.is_valid_alias_name(alias_name)
|
||||
if not is_valid_name:
|
||||
await ctx.send(_("You attempted to create a new alias"
|
||||
" with the name {} but that"
|
||||
" name is an invalid alias name. Alias"
|
||||
" names may not contain spaces.").format(alias_name))
|
||||
await ctx.send(
|
||||
_(
|
||||
"You attempted to create a new alias"
|
||||
" with the name {} but that"
|
||||
" name is an invalid alias name. Alias"
|
||||
" names may not contain spaces."
|
||||
).format(alias_name)
|
||||
)
|
||||
return
|
||||
# endregion
|
||||
# endregion
|
||||
|
||||
# At this point we know we need to make a new alias
|
||||
# and that the alias name is valid.
|
||||
|
||||
await self.add_alias(ctx, alias_name, command)
|
||||
|
||||
await ctx.send(_("A new alias with the trigger `{}`"
|
||||
" has been created.").format(alias_name))
|
||||
await ctx.send(_("A new alias with the trigger `{}` has been created.").format(alias_name))
|
||||
|
||||
@checks.is_owner()
|
||||
@global_.command(name="add")
|
||||
async def _add_global_alias(self, ctx: commands.Context,
|
||||
alias_name: str, *, command):
|
||||
async def _add_global_alias(self, ctx: commands.Context, alias_name: str, *, command):
|
||||
"""
|
||||
Add a global alias for a command.
|
||||
"""
|
||||
# region Alias Add Validity Checking
|
||||
# region Alias Add Validity Checking
|
||||
is_command = self.is_command(alias_name)
|
||||
if is_command:
|
||||
await ctx.send(_("You attempted to create a new global alias"
|
||||
" with the name {} but that"
|
||||
" name is already a command on this bot.").format(alias_name))
|
||||
await ctx.send(
|
||||
_(
|
||||
"You attempted to create a new global alias"
|
||||
" with the name {} but that"
|
||||
" name is already a command on this bot."
|
||||
).format(alias_name)
|
||||
)
|
||||
return
|
||||
|
||||
is_alias, something_useless = await self.is_alias(ctx.guild, alias_name)
|
||||
if is_alias:
|
||||
await ctx.send(_("You attempted to create a new global alias"
|
||||
" with the name {} but that"
|
||||
" alias already exists on this server.").format(alias_name))
|
||||
await ctx.send(
|
||||
_(
|
||||
"You attempted to create a new global alias"
|
||||
" with the name {} but that"
|
||||
" alias already exists on this server."
|
||||
).format(alias_name)
|
||||
)
|
||||
return
|
||||
|
||||
is_valid_name = self.is_valid_alias_name(alias_name)
|
||||
if not is_valid_name:
|
||||
await ctx.send(_("You attempted to create a new global alias"
|
||||
" with the name {} but that"
|
||||
" name is an invalid alias name. Alias"
|
||||
" names may not contain spaces.").format(alias_name))
|
||||
await ctx.send(
|
||||
_(
|
||||
"You attempted to create a new global alias"
|
||||
" with the name {} but that"
|
||||
" name is an invalid alias name. Alias"
|
||||
" names may not contain spaces."
|
||||
).format(alias_name)
|
||||
)
|
||||
return
|
||||
# endregion
|
||||
# endregion
|
||||
|
||||
await self.add_alias(ctx, alias_name, command, global_=True)
|
||||
|
||||
await ctx.send(_("A new global alias with the trigger `{}`"
|
||||
" has been created.").format(alias_name))
|
||||
await ctx.send(
|
||||
_("A new global alias with the trigger `{}` has been created.").format(alias_name)
|
||||
)
|
||||
|
||||
@alias.command(name="help")
|
||||
@commands.guild_only()
|
||||
async def _help_alias(self, ctx: commands.Context, alias_name: str):
|
||||
"""Tries to execute help for the base command of the alias"""
|
||||
is_alias, alias = self.is_alias(ctx.guild, alias_name=alias_name)
|
||||
is_alias, alias = await self.is_alias(ctx.guild, alias_name=alias_name)
|
||||
if is_alias:
|
||||
base_cmd = alias.command[0]
|
||||
|
||||
@@ -280,11 +302,13 @@ class Alias:
|
||||
is_alias, alias = await self.is_alias(ctx.guild, alias_name)
|
||||
|
||||
if is_alias:
|
||||
await ctx.send(_("The `{}` alias will execute the"
|
||||
" command `{}`").format(alias_name, alias.command))
|
||||
await ctx.send(
|
||||
_("The `{}` alias will execute the command `{}`").format(alias_name, alias.command)
|
||||
)
|
||||
else:
|
||||
await ctx.send(_("There is no alias with the name `{}`").format(alias_name))
|
||||
|
||||
@checks.mod_or_permissions(manage_guild=True)
|
||||
@alias.command(name="del")
|
||||
@commands.guild_only()
|
||||
async def _del_alias(self, ctx: commands.Context, alias_name: str):
|
||||
@@ -299,11 +323,13 @@ class Alias:
|
||||
return
|
||||
|
||||
if await self.delete_alias(ctx, alias_name):
|
||||
await ctx.send(_("Alias with the name `{}` was successfully"
|
||||
" deleted.").format(alias_name))
|
||||
await ctx.send(
|
||||
_("Alias with the name `{}` was successfully deleted.").format(alias_name)
|
||||
)
|
||||
else:
|
||||
await ctx.send(_("Alias with name `{}` was not found.").format(alias_name))
|
||||
|
||||
@checks.is_owner()
|
||||
@global_.command(name="del")
|
||||
async def _del_global_alias(self, ctx: commands.Context, alias_name: str):
|
||||
"""
|
||||
@@ -317,8 +343,9 @@ class Alias:
|
||||
return
|
||||
|
||||
if await self.delete_alias(ctx, alias_name, global_=True):
|
||||
await ctx.send(_("Alias with the name `{}` was successfully"
|
||||
" deleted.").format(alias_name))
|
||||
await ctx.send(
|
||||
_("Alias with the name `{}` was successfully deleted.").format(alias_name)
|
||||
)
|
||||
else:
|
||||
await ctx.send(_("Alias with name `{}` was not found.").format(alias_name))
|
||||
|
||||
@@ -328,7 +355,9 @@ class Alias:
|
||||
"""
|
||||
Lists the available aliases on this server.
|
||||
"""
|
||||
names = [_("Aliases:"), ] + sorted(["+ " + a.name for a in (await self.unloaded_aliases(ctx.guild))])
|
||||
names = [_("Aliases:")] + sorted(
|
||||
["+ " + a.name for a in (await self.unloaded_aliases(ctx.guild))]
|
||||
)
|
||||
if len(names) == 0:
|
||||
await ctx.send(_("There are no aliases on this server."))
|
||||
else:
|
||||
@@ -339,7 +368,9 @@ class Alias:
|
||||
"""
|
||||
Lists the available global aliases on this bot.
|
||||
"""
|
||||
names = [_("Aliases:"), ] + sorted(["+ " + a.name for a in await self.unloaded_global_aliases()])
|
||||
names = [_("Aliases:")] + sorted(
|
||||
["+ " + a.name for a in await self.unloaded_global_aliases()]
|
||||
)
|
||||
if len(names) == 0:
|
||||
await ctx.send(_("There are no aliases on this server."))
|
||||
else:
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
from typing import Tuple
|
||||
from discord.ext import commands
|
||||
|
||||
import discord
|
||||
from redbot.core import commands
|
||||
|
||||
|
||||
class AliasEntry:
|
||||
def __init__(self, name: str, command: Tuple[str],
|
||||
creator: discord.Member, global_: bool=False):
|
||||
def __init__(
|
||||
self, name: str, command: Tuple[str], creator: discord.Member, global_: bool = False
|
||||
):
|
||||
super().__init__()
|
||||
self.has_real_data = False
|
||||
self.name = name
|
||||
@@ -43,13 +44,12 @@ class AliasEntry:
|
||||
"creator": creator,
|
||||
"guild": guild,
|
||||
"global": self.global_,
|
||||
"uses": self.uses
|
||||
"uses": self.uses,
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def from_json(cls, data: dict, bot: commands.Bot=None):
|
||||
ret = cls(data["name"], data["command"],
|
||||
data["creator"], global_=data["global"])
|
||||
def from_json(cls, data: dict, bot: commands.Bot = None):
|
||||
ret = cls(data["name"], data["command"], data["creator"], global_=data["global"])
|
||||
|
||||
if bot:
|
||||
ret.has_real_data = True
|
||||
|
||||
@@ -1,15 +1,11 @@
|
||||
import subprocess
|
||||
|
||||
TO_TRANSLATE = [
|
||||
'../alias.py'
|
||||
]
|
||||
TO_TRANSLATE = ["../alias.py"]
|
||||
|
||||
|
||||
def regen_messages():
|
||||
subprocess.run(
|
||||
['pygettext', '-n'] + TO_TRANSLATE
|
||||
)
|
||||
subprocess.run(["pygettext", "-n"] + TO_TRANSLATE)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
regen_messages()
|
||||
regen_messages()
|
||||
|
||||
@@ -1,16 +1,18 @@
|
||||
from pathlib import Path
|
||||
from aiohttp import ClientSession
|
||||
import shutil
|
||||
import logging
|
||||
|
||||
from .audio import Audio
|
||||
from .manager import start_lavalink_server
|
||||
from discord.ext import commands
|
||||
from redbot.core import commands
|
||||
from redbot.core.data_manager import cog_data_path
|
||||
import redbot.core
|
||||
|
||||
log = logging.getLogger("red.audio")
|
||||
|
||||
LAVALINK_DOWNLOAD_URL = (
|
||||
"https://github.com/Cog-Creators/Red-DiscordBot/"
|
||||
"releases/download/{}/Lavalink.jar"
|
||||
"https://github.com/Cog-Creators/Red-DiscordBot/releases/download/{}/Lavalink.jar"
|
||||
).format(redbot.core.__version__)
|
||||
|
||||
LAVALINK_DOWNLOAD_DIR = cog_data_path(raw_name="Audio")
|
||||
@@ -21,7 +23,7 @@ BUNDLED_APP_YML_FILE = Path(__file__).parent / "application.yml"
|
||||
|
||||
|
||||
async def download_lavalink(session):
|
||||
with LAVALINK_JAR_FILE.open(mode='wb') as f:
|
||||
with LAVALINK_JAR_FILE.open(mode="wb") as f:
|
||||
async with session.get(LAVALINK_DOWNLOAD_URL) as resp:
|
||||
while True:
|
||||
chunk = await resp.content.read(512)
|
||||
@@ -34,15 +36,13 @@ async def maybe_download_lavalink(loop, cog):
|
||||
jar_exists = LAVALINK_JAR_FILE.exists()
|
||||
current_build = redbot.core.VersionInfo(*await cog.config.current_build())
|
||||
|
||||
session = ClientSession(loop=loop)
|
||||
|
||||
if not jar_exists or current_build < redbot.core.version_info:
|
||||
log.info("Downloading Lavalink.jar")
|
||||
LAVALINK_DOWNLOAD_DIR.mkdir(parents=True, exist_ok=True)
|
||||
await download_lavalink(session)
|
||||
async with ClientSession(loop=loop) as session:
|
||||
await download_lavalink(session)
|
||||
await cog.config.current_build.set(redbot.core.version_info.to_json())
|
||||
|
||||
session.close()
|
||||
|
||||
shutil.copyfile(str(BUNDLED_APP_YML_FILE), str(APP_YML_FILE))
|
||||
|
||||
|
||||
@@ -53,4 +53,5 @@ async def setup(bot: commands.Bot):
|
||||
await start_lavalink_server(bot.loop)
|
||||
|
||||
bot.add_cog(cog)
|
||||
bot.loop.create_task(cog.disconnect_timer())
|
||||
bot.loop.create_task(cog.init_config())
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,15 +1,11 @@
|
||||
import subprocess
|
||||
|
||||
TO_TRANSLATE = [
|
||||
'../audio.py'
|
||||
]
|
||||
TO_TRANSLATE = ["../audio.py"]
|
||||
|
||||
|
||||
def regen_messages():
|
||||
subprocess.run(
|
||||
['pygettext', '-n'] + TO_TRANSLATE
|
||||
)
|
||||
subprocess.run(["pygettext", "-n"] + TO_TRANSLATE)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
regen_messages()
|
||||
regen_messages()
|
||||
|
||||
@@ -4,8 +4,11 @@ import asyncio
|
||||
from subprocess import Popen, DEVNULL, PIPE
|
||||
import os
|
||||
import logging
|
||||
from typing import Optional, Tuple
|
||||
|
||||
log = logging.getLogger('red.audio.manager')
|
||||
_JavaVersion = Tuple[int, int]
|
||||
|
||||
log = logging.getLogger("red.audio.manager")
|
||||
|
||||
proc = None
|
||||
SHUTDOWN = asyncio.Event()
|
||||
@@ -13,7 +16,8 @@ SHUTDOWN = asyncio.Event()
|
||||
|
||||
def has_java_error(pid):
|
||||
from . import LAVALINK_DOWNLOAD_DIR
|
||||
poss_error_file = LAVALINK_DOWNLOAD_DIR / 'hs_err_pid{}.log'.format(pid)
|
||||
|
||||
poss_error_file = LAVALINK_DOWNLOAD_DIR / "hs_err_pid{}.log".format(pid)
|
||||
return poss_error_file.exists()
|
||||
|
||||
|
||||
@@ -29,39 +33,37 @@ async def monitor_lavalink_server(loop):
|
||||
log.info("Restarting Lavalink jar.")
|
||||
await start_lavalink_server(loop)
|
||||
else:
|
||||
log.error("Your Java is borked. Please find the hs_err_pid{}.log file"
|
||||
" in the Audio data folder and report this issue.".format(
|
||||
proc.pid
|
||||
))
|
||||
log.error(
|
||||
"Your Java is borked. Please find the hs_err_pid{}.log file"
|
||||
" in the Audio data folder and report this issue.".format(proc.pid)
|
||||
)
|
||||
|
||||
|
||||
async def has_java(loop):
|
||||
java_available = shutil.which('java') is not None
|
||||
async def has_java(loop) -> Tuple[bool, Optional[_JavaVersion]]:
|
||||
java_available = shutil.which("java") is not None
|
||||
if not java_available:
|
||||
return False
|
||||
return False, None
|
||||
|
||||
version = await get_java_version(loop)
|
||||
return version >= (1, 8), version
|
||||
|
||||
|
||||
async def get_java_version(loop):
|
||||
async def get_java_version(loop) -> _JavaVersion:
|
||||
"""
|
||||
This assumes we've already checked that java exists.
|
||||
"""
|
||||
proc = Popen(
|
||||
shlex.split("java -version", posix=os.name == 'posix'),
|
||||
stdout=PIPE, stderr=PIPE
|
||||
)
|
||||
proc = Popen(shlex.split("java -version", posix=os.name == "posix"), stdout=PIPE, stderr=PIPE)
|
||||
_, err = proc.communicate()
|
||||
|
||||
version_info = str(err, encoding='utf-8')
|
||||
version_info = str(err, encoding="utf-8")
|
||||
|
||||
version_line = version_info.split('\n')[0]
|
||||
version_line = version_info.split("\n")[0]
|
||||
version_start = version_line.find('"')
|
||||
version_string = version_line[version_start + 1:-1]
|
||||
major, minor = version_string.split('.')[:2]
|
||||
version_string = version_line[version_start + 1 : -1]
|
||||
major, minor = version_string.split(".")[:2]
|
||||
return int(major), int(minor)
|
||||
|
||||
|
||||
async def start_lavalink_server(loop):
|
||||
java_available, java_version = await has_java(loop)
|
||||
if not java_available:
|
||||
@@ -72,13 +74,15 @@ async def start_lavalink_server(loop):
|
||||
extra_flags = "-Dsun.zip.disableMemoryMapping=true"
|
||||
|
||||
from . import LAVALINK_DOWNLOAD_DIR, LAVALINK_JAR_FILE
|
||||
|
||||
start_cmd = "java {} -jar {}".format(extra_flags, LAVALINK_JAR_FILE.resolve())
|
||||
|
||||
global proc
|
||||
proc = Popen(
|
||||
shlex.split(start_cmd, posix=os.name == 'posix'),
|
||||
shlex.split(start_cmd, posix=os.name == "posix"),
|
||||
cwd=str(LAVALINK_DOWNLOAD_DIR),
|
||||
stdout=DEVNULL, stderr=DEVNULL
|
||||
stdout=DEVNULL,
|
||||
stderr=DEVNULL,
|
||||
)
|
||||
|
||||
log.info("Lavalink jar started. PID: {}".format(proc.pid))
|
||||
@@ -92,4 +96,5 @@ def shutdown_lavalink_server():
|
||||
global proc
|
||||
if proc is not None:
|
||||
proc.terminate()
|
||||
proc.wait()
|
||||
proc = None
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
import discord
|
||||
from redbot.core.utils.chat_formatting import box
|
||||
|
||||
from redbot.core import checks, bank
|
||||
from redbot.core.i18n import CogI18n
|
||||
from discord.ext import commands
|
||||
from redbot.core import checks, bank, commands
|
||||
from redbot.core.i18n import Translator, cog_i18n
|
||||
|
||||
from redbot.core.bot import Red # Only used for type hints
|
||||
|
||||
_ = CogI18n('Bank', __file__)
|
||||
_ = Translator("Bank", __file__)
|
||||
|
||||
|
||||
def check_global_setting_guildowner():
|
||||
@@ -15,15 +14,18 @@ def check_global_setting_guildowner():
|
||||
Command decorator. If the bank is not global, it checks if the author is
|
||||
either the guildowner or has the administrator permission.
|
||||
"""
|
||||
|
||||
async def pred(ctx: commands.Context):
|
||||
author = ctx.author
|
||||
if await ctx.bot.is_owner(author):
|
||||
return True
|
||||
if not await bank.is_global():
|
||||
if not isinstance(ctx.channel, discord.abc.GuildChannel):
|
||||
return False
|
||||
if await ctx.bot.is_owner(author):
|
||||
return True
|
||||
permissions = ctx.channel.permissions_for(author)
|
||||
return author == ctx.guild.owner or permissions.administrator
|
||||
else:
|
||||
return await ctx.bot.is_owner(author)
|
||||
|
||||
return commands.check(pred)
|
||||
|
||||
@@ -33,21 +35,25 @@ def check_global_setting_admin():
|
||||
Command decorator. If the bank is not global, it checks if the author is
|
||||
either a bot admin or has the manage_guild permission.
|
||||
"""
|
||||
|
||||
async def pred(ctx: commands.Context):
|
||||
author = ctx.author
|
||||
if await ctx.bot.is_owner(author):
|
||||
return True
|
||||
if not await bank.is_global():
|
||||
if not isinstance(ctx.channel, discord.abc.GuildChannel):
|
||||
return False
|
||||
if await ctx.bot.is_owner(author):
|
||||
return True
|
||||
permissions = ctx.channel.permissions_for(author)
|
||||
is_guild_owner = author == ctx.guild.owner
|
||||
admin_role = await ctx.bot.db.guild(ctx.guild).admin_role()
|
||||
return admin_role in author.roles or is_guild_owner or permissions.manage_guild
|
||||
else:
|
||||
return await ctx.bot.is_owner(author)
|
||||
|
||||
return commands.check(pred)
|
||||
|
||||
|
||||
@cog_i18n(_)
|
||||
class Bank:
|
||||
"""Bank"""
|
||||
|
||||
@@ -56,8 +62,9 @@ class Bank:
|
||||
|
||||
# SECTION commands
|
||||
|
||||
@commands.group()
|
||||
@check_global_setting_guildowner()
|
||||
@checks.guildowner_or_permissions(administrator=True)
|
||||
@commands.group(autohelp=True)
|
||||
async def bankset(self, ctx: commands.Context):
|
||||
"""Base command for bank settings"""
|
||||
if ctx.invoked_subcommand is None:
|
||||
@@ -67,25 +74,19 @@ class Bank:
|
||||
default_balance = await bank._conf.default_balance()
|
||||
else:
|
||||
if not ctx.guild:
|
||||
await ctx.send_help()
|
||||
return
|
||||
bank_name = await bank._conf.guild(ctx.guild).bank_name()
|
||||
currency_name = await bank._conf.guild(ctx.guild).currency()
|
||||
default_balance = await bank._conf.guild(ctx.guild).default_balance()
|
||||
|
||||
settings = (_(
|
||||
"Bank settings:\n\n"
|
||||
"Bank name: {}\n"
|
||||
"Currency: {}\n"
|
||||
"Default balance: {}"
|
||||
"").format(bank_name, currency_name, default_balance)
|
||||
)
|
||||
settings = _(
|
||||
"Bank settings:\n\nBank name: {}\nCurrency: {}\nDefault balance: {}"
|
||||
).format(bank_name, currency_name, default_balance)
|
||||
await ctx.send(box(settings))
|
||||
await ctx.send_help()
|
||||
|
||||
@bankset.command(name="toggleglobal")
|
||||
@checks.is_owner()
|
||||
async def bankset_toggleglobal(self, ctx: commands.Context, confirm: bool=False):
|
||||
async def bankset_toggleglobal(self, ctx: commands.Context, confirm: bool = False):
|
||||
"""Toggles whether the bank is global or not
|
||||
If the bank is global, it will become per-server
|
||||
If the bank is per-server, it will become global"""
|
||||
@@ -94,10 +95,10 @@ class Bank:
|
||||
word = _("per-server") if cur_setting else _("global")
|
||||
if confirm is False:
|
||||
await ctx.send(
|
||||
_("This will toggle the bank to be {}, deleting all accounts "
|
||||
"in the process! If you're sure, type `{}`").format(
|
||||
word, "{}bankset toggleglobal yes".format(ctx.prefix)
|
||||
)
|
||||
_(
|
||||
"This will toggle the bank to be {}, deleting all accounts "
|
||||
"in the process! If you're sure, type `{}`"
|
||||
).format(word, "{}bankset toggleglobal yes".format(ctx.prefix))
|
||||
)
|
||||
else:
|
||||
await bank.set_global(not cur_setting)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
class BankError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class BankNotGlobal(BankError):
|
||||
pass
|
||||
|
||||
@@ -34,4 +35,4 @@ class NegativeValue(BankError):
|
||||
|
||||
|
||||
class SameSenderAndReceiver(BankError):
|
||||
pass
|
||||
pass
|
||||
|
||||
@@ -1,15 +1,11 @@
|
||||
import subprocess
|
||||
|
||||
TO_TRANSLATE = [
|
||||
'../bank.py'
|
||||
]
|
||||
TO_TRANSLATE = ["../bank.py"]
|
||||
|
||||
|
||||
def regen_messages():
|
||||
subprocess.run(
|
||||
['pygettext', '-n'] + TO_TRANSLATE
|
||||
)
|
||||
subprocess.run(["pygettext", "-n"] + TO_TRANSLATE)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
regen_messages()
|
||||
regen_messages()
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
import re
|
||||
|
||||
import discord
|
||||
from discord.ext import commands
|
||||
|
||||
from redbot.core import checks, RedContext
|
||||
from redbot.core import checks, commands
|
||||
from redbot.core.bot import Red
|
||||
from redbot.core.i18n import CogI18n
|
||||
from redbot.core.i18n import Translator, cog_i18n
|
||||
from redbot.core.utils.mod import slow_deletion, mass_purge
|
||||
from redbot.cogs.mod.log import log
|
||||
|
||||
_ = CogI18n("Cleanup", __file__)
|
||||
_ = Translator("Cleanup", __file__)
|
||||
|
||||
|
||||
@cog_i18n(_)
|
||||
class Cleanup:
|
||||
"""Commands for cleaning messages"""
|
||||
|
||||
@@ -19,28 +19,44 @@ class Cleanup:
|
||||
self.bot = bot
|
||||
|
||||
@staticmethod
|
||||
async def check_100_plus(ctx: RedContext, number: int) -> bool:
|
||||
async def check_100_plus(ctx: commands.Context, number: int) -> bool:
|
||||
"""
|
||||
Called when trying to delete more than 100 messages at once
|
||||
Called when trying to delete more than 100 messages at once.
|
||||
|
||||
Prompts the user to choose whether they want to continue or not
|
||||
Prompts the user to choose whether they want to continue or not.
|
||||
|
||||
Tries its best to cleanup after itself if the response is positive.
|
||||
"""
|
||||
|
||||
def author_check(message):
|
||||
return message.author == ctx.author
|
||||
|
||||
await ctx.send(_('Are you sure you want to delete {} messages? (y/n)').format(number))
|
||||
response = await ctx.bot.wait_for('message', check=author_check)
|
||||
prompt = await ctx.send(
|
||||
_("Are you sure you want to delete {} messages? (y/n)").format(number)
|
||||
)
|
||||
response = await ctx.bot.wait_for("message", check=author_check)
|
||||
|
||||
if response.content.lower().startswith('y'):
|
||||
if response.content.lower().startswith("y"):
|
||||
await prompt.delete()
|
||||
try:
|
||||
await response.delete()
|
||||
except:
|
||||
pass
|
||||
return True
|
||||
else:
|
||||
await ctx.send(_('Cancelled.'))
|
||||
await ctx.send(_("Cancelled."))
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
async def get_messages_for_deletion(
|
||||
ctx: RedContext, channel: discord.TextChannel, number,
|
||||
check=lambda x: True, limit=100, before=None, after=None
|
||||
ctx: commands.Context,
|
||||
channel: discord.TextChannel,
|
||||
number,
|
||||
check=lambda x: True,
|
||||
limit=100,
|
||||
before=None,
|
||||
after=None,
|
||||
delete_pinned=False,
|
||||
) -> list:
|
||||
"""
|
||||
Gets a list of messages meeting the requirements to be deleted.
|
||||
@@ -50,17 +66,20 @@ class Cleanup:
|
||||
- The message passes a provided check (if no check is provided,
|
||||
this is automatically true)
|
||||
- The message is less than 14 days old
|
||||
- The message is not pinned
|
||||
"""
|
||||
to_delete = []
|
||||
too_old = False
|
||||
|
||||
while not too_old and len(to_delete) - 1 < number:
|
||||
while not too_old and len(to_delete) < number:
|
||||
message = None
|
||||
async for message in channel.history(limit=limit,
|
||||
before=before,
|
||||
after=after):
|
||||
if (not number or len(to_delete) - 1 < number) and check(message) \
|
||||
and (ctx.message.created_at - message.created_at).days < 14:
|
||||
async for message in channel.history(limit=limit, before=before, after=after):
|
||||
if (
|
||||
(not number or len(to_delete) < number)
|
||||
and check(message)
|
||||
and (ctx.message.created_at - message.created_at).days < 14
|
||||
and (delete_pinned or not message.pinned)
|
||||
):
|
||||
to_delete.append(message)
|
||||
elif (ctx.message.created_at - message.created_at).days >= 14:
|
||||
too_old = True
|
||||
@@ -75,15 +94,15 @@ class Cleanup:
|
||||
|
||||
@commands.group()
|
||||
@checks.mod_or_permissions(manage_messages=True)
|
||||
async def cleanup(self, ctx: RedContext):
|
||||
async def cleanup(self, ctx: commands.Context):
|
||||
"""Deletes messages."""
|
||||
if ctx.invoked_subcommand is None:
|
||||
await ctx.send_help()
|
||||
pass
|
||||
|
||||
@cleanup.command()
|
||||
@commands.guild_only()
|
||||
@commands.bot_has_permissions(manage_messages=True)
|
||||
async def text(self, ctx: RedContext, text: str, number: int):
|
||||
async def text(
|
||||
self, ctx: commands.Context, text: str, number: int, delete_pinned: bool = False
|
||||
):
|
||||
"""Deletes last X messages matching the specified text.
|
||||
|
||||
Example:
|
||||
@@ -92,14 +111,18 @@ class Cleanup:
|
||||
Remember to use double quotes."""
|
||||
|
||||
channel = ctx.channel
|
||||
if not channel.permissions_for(ctx.guild.me).manage_messages:
|
||||
await ctx.send("I need the Manage Messages permission to do this.")
|
||||
return
|
||||
|
||||
author = ctx.author
|
||||
is_bot = self.bot.user.bot
|
||||
|
||||
|
||||
if number > 100:
|
||||
cont = await self.check_100_plus(ctx, number)
|
||||
if not cont:
|
||||
return
|
||||
|
||||
|
||||
def check(m):
|
||||
if text in m.content:
|
||||
return True
|
||||
@@ -109,11 +132,18 @@ class Cleanup:
|
||||
return False
|
||||
|
||||
to_delete = await self.get_messages_for_deletion(
|
||||
ctx, channel, number, check=check, limit=1000, before=ctx.message)
|
||||
ctx,
|
||||
channel,
|
||||
number,
|
||||
check=check,
|
||||
limit=1000,
|
||||
before=ctx.message,
|
||||
delete_pinned=delete_pinned,
|
||||
)
|
||||
|
||||
reason = "{}({}) deleted {} messages "\
|
||||
" containing '{}' in channel {}.".format(author.name,
|
||||
author.id, len(to_delete), text, channel.id)
|
||||
reason = "{}({}) deleted {} messages containing '{}' in channel {}.".format(
|
||||
author.name, author.id, len(to_delete), text, channel.id
|
||||
)
|
||||
log.info(reason)
|
||||
|
||||
if is_bot:
|
||||
@@ -123,27 +153,40 @@ class Cleanup:
|
||||
|
||||
@cleanup.command()
|
||||
@commands.guild_only()
|
||||
@commands.bot_has_permissions(manage_messages=True)
|
||||
async def user(self, ctx: RedContext, user: discord.Member or int, number: int):
|
||||
async def user(
|
||||
self, ctx: commands.Context, user: str, number: int, delete_pinned: bool = False
|
||||
):
|
||||
"""Deletes last X messages from specified user.
|
||||
|
||||
Examples:
|
||||
cleanup user @\u200bTwentysix 2
|
||||
cleanup user Red 6"""
|
||||
|
||||
channel = ctx.channel
|
||||
if not channel.permissions_for(ctx.guild.me).manage_messages:
|
||||
await ctx.send("I need the Manage Messages permission to do this.")
|
||||
return
|
||||
|
||||
member = None
|
||||
try:
|
||||
member = await commands.converter.MemberConverter().convert(ctx, user)
|
||||
except commands.BadArgument:
|
||||
try:
|
||||
_id = int(user)
|
||||
except ValueError:
|
||||
raise commands.BadArgument()
|
||||
else:
|
||||
_id = member.id
|
||||
|
||||
author = ctx.author
|
||||
is_bot = self.bot.user.bot
|
||||
|
||||
|
||||
if number > 100:
|
||||
cont = await self.check_100_plus(ctx, number)
|
||||
if not cont:
|
||||
return
|
||||
|
||||
def check(m):
|
||||
if isinstance(user, discord.Member) and m.author == user:
|
||||
return True
|
||||
elif m.author.id == user: # Allow finding messages based on an ID
|
||||
if m.author.id == _id:
|
||||
return True
|
||||
elif m == ctx.message:
|
||||
return True
|
||||
@@ -151,12 +194,19 @@ class Cleanup:
|
||||
return False
|
||||
|
||||
to_delete = await self.get_messages_for_deletion(
|
||||
ctx, channel, number, check=check, limit=1000, before=ctx.message
|
||||
ctx,
|
||||
channel,
|
||||
number,
|
||||
check=check,
|
||||
limit=1000,
|
||||
before=ctx.message,
|
||||
delete_pinned=delete_pinned,
|
||||
)
|
||||
reason = (
|
||||
"{}({}) deleted {} messages "
|
||||
" made by {}({}) in channel {}."
|
||||
"".format(author.name, author.id, len(to_delete), member or "???", _id, channel.name)
|
||||
)
|
||||
reason = "{}({}) deleted {} messages "\
|
||||
" made by {}({}) in channel {}."\
|
||||
"".format(author.name, author.id, len(to_delete),
|
||||
user.name, user.id, channel.name)
|
||||
log.info(reason)
|
||||
|
||||
if is_bot:
|
||||
@@ -167,8 +217,7 @@ class Cleanup:
|
||||
|
||||
@cleanup.command()
|
||||
@commands.guild_only()
|
||||
@commands.bot_has_permissions(manage_messages=True)
|
||||
async def after(self, ctx: RedContext, message_id: int):
|
||||
async def after(self, ctx: commands.Context, message_id: int, delete_pinned: bool = False):
|
||||
"""Deletes all messages after specified message.
|
||||
|
||||
To get a message id, enable developer mode in Discord's
|
||||
@@ -179,12 +228,14 @@ class Cleanup:
|
||||
"""
|
||||
|
||||
channel = ctx.channel
|
||||
if not channel.permissions_for(ctx.guild.me).manage_messages:
|
||||
await ctx.send("I need the Manage Messages permission to do this.")
|
||||
return
|
||||
author = ctx.author
|
||||
is_bot = self.bot.user.bot
|
||||
|
||||
if not is_bot:
|
||||
await ctx.send(_("This command can only be used on bots with "
|
||||
"bot accounts."))
|
||||
await ctx.send(_("This command can only be used on bots with bot accounts."))
|
||||
return
|
||||
|
||||
after = await channel.get_message(message_id)
|
||||
@@ -194,42 +245,45 @@ class Cleanup:
|
||||
return
|
||||
|
||||
to_delete = await self.get_messages_for_deletion(
|
||||
ctx, channel, 0, limit=None, after=after
|
||||
ctx, channel, 0, limit=None, after=after, delete_pinned=delete_pinned
|
||||
)
|
||||
|
||||
reason = "{}({}) deleted {} messages in channel {}."\
|
||||
"".format(author.name, author.id,
|
||||
len(to_delete), channel.name)
|
||||
reason = "{}({}) deleted {} messages in channel {}.".format(
|
||||
author.name, author.id, len(to_delete), channel.name
|
||||
)
|
||||
log.info(reason)
|
||||
|
||||
await mass_purge(to_delete, channel)
|
||||
|
||||
@cleanup.command()
|
||||
@commands.guild_only()
|
||||
@commands.bot_has_permissions(manage_messages=True)
|
||||
async def messages(self, ctx: RedContext, number: int):
|
||||
async def messages(self, ctx: commands.Context, number: int, delete_pinned: bool = False):
|
||||
"""Deletes last X messages.
|
||||
|
||||
Example:
|
||||
cleanup messages 26"""
|
||||
|
||||
channel = ctx.channel
|
||||
if not channel.permissions_for(ctx.guild.me).manage_messages:
|
||||
await ctx.send("I need the Manage Messages permission to do this.")
|
||||
return
|
||||
author = ctx.author
|
||||
|
||||
is_bot = self.bot.user.bot
|
||||
|
||||
|
||||
if number > 100:
|
||||
cont = await self.check_100_plus(ctx, number)
|
||||
if not cont:
|
||||
return
|
||||
|
||||
to_delete = await self.get_messages_for_deletion(
|
||||
ctx, channel, number, limit=1000, before=ctx.message
|
||||
ctx, channel, number, limit=1000, before=ctx.message, delete_pinned=delete_pinned
|
||||
)
|
||||
to_delete.append(ctx.message)
|
||||
|
||||
reason = "{}({}) deleted {} messages in channel {}."\
|
||||
"".format(author.name, author.id,
|
||||
number, channel.name)
|
||||
reason = "{}({}) deleted {} messages in channel {}.".format(
|
||||
author.name, author.id, number, channel.name
|
||||
)
|
||||
log.info(reason)
|
||||
|
||||
if is_bot:
|
||||
@@ -237,13 +291,15 @@ class Cleanup:
|
||||
else:
|
||||
await slow_deletion(to_delete)
|
||||
|
||||
@cleanup.command(name='bot')
|
||||
@cleanup.command(name="bot")
|
||||
@commands.guild_only()
|
||||
@commands.bot_has_permissions(manage_messages=True)
|
||||
async def cleanup_bot(self, ctx: RedContext, number: int):
|
||||
async def cleanup_bot(self, ctx: commands.Context, number: int, delete_pinned: bool = False):
|
||||
"""Cleans up command messages and messages from the bot."""
|
||||
|
||||
channel = ctx.message.channel
|
||||
channel = ctx.channel
|
||||
if not channel.permissions_for(ctx.guild.me).manage_messages:
|
||||
await ctx.send("I need the Manage Messages permission to do this.")
|
||||
return
|
||||
author = ctx.message.author
|
||||
is_bot = self.bot.user.bot
|
||||
|
||||
@@ -252,13 +308,13 @@ class Cleanup:
|
||||
if not cont:
|
||||
return
|
||||
|
||||
prefixes = await self.bot.get_prefix(ctx.message) # This returns all server prefixes
|
||||
prefixes = await self.bot.get_prefix(ctx.message) # This returns all server prefixes
|
||||
if isinstance(prefixes, str):
|
||||
prefixes = [prefixes]
|
||||
|
||||
# In case some idiot sets a null prefix
|
||||
if '' in prefixes:
|
||||
prefixes.remove('')
|
||||
if "" in prefixes:
|
||||
prefixes.remove("")
|
||||
|
||||
def check(m):
|
||||
if m.author.id == self.bot.user.id:
|
||||
@@ -267,18 +323,26 @@ class Cleanup:
|
||||
return True
|
||||
p = discord.utils.find(m.content.startswith, prefixes)
|
||||
if p and len(p) > 0:
|
||||
cmd_name = m.content[len(p):].split(' ')[0]
|
||||
cmd_name = m.content[len(p) :].split(" ")[0]
|
||||
return bool(self.bot.get_command(cmd_name))
|
||||
return False
|
||||
|
||||
to_delete = await self.get_messages_for_deletion(
|
||||
ctx, channel, number, check=check, limit=1000, before=ctx.message
|
||||
ctx,
|
||||
channel,
|
||||
number,
|
||||
check=check,
|
||||
limit=1000,
|
||||
before=ctx.message,
|
||||
delete_pinned=delete_pinned,
|
||||
)
|
||||
to_delete.append(ctx.message)
|
||||
|
||||
reason = "{}({}) deleted {} "\
|
||||
" command messages in channel {}."\
|
||||
"".format(author.name, author.id, len(to_delete),
|
||||
channel.name)
|
||||
reason = (
|
||||
"{}({}) deleted {} "
|
||||
" command messages in channel {}."
|
||||
"".format(author.name, author.id, len(to_delete), channel.name)
|
||||
)
|
||||
log.info(reason)
|
||||
|
||||
if is_bot:
|
||||
@@ -286,8 +350,14 @@ class Cleanup:
|
||||
else:
|
||||
await slow_deletion(to_delete)
|
||||
|
||||
@cleanup.command(name='self')
|
||||
async def cleanup_self(self, ctx: RedContext, number: int, match_pattern: str = None):
|
||||
@cleanup.command(name="self")
|
||||
async def cleanup_self(
|
||||
self,
|
||||
ctx: commands.Context,
|
||||
number: int,
|
||||
match_pattern: str = None,
|
||||
delete_pinned: bool = False,
|
||||
):
|
||||
"""Cleans up messages owned by the bot.
|
||||
|
||||
By default, all messages are cleaned. If a third argument is specified,
|
||||
@@ -313,8 +383,7 @@ class Cleanup:
|
||||
me = ctx.guild.me
|
||||
can_mass_purge = channel.permissions_for(me).manage_messages
|
||||
|
||||
use_re = (match_pattern and match_pattern.startswith('r(') and
|
||||
match_pattern.endswith(')'))
|
||||
use_re = match_pattern and match_pattern.startswith("r(") and match_pattern.endswith(")")
|
||||
|
||||
if use_re:
|
||||
match_pattern = match_pattern[1:] # strip 'r'
|
||||
@@ -322,10 +391,14 @@ class Cleanup:
|
||||
|
||||
def content_match(c):
|
||||
return bool(match_re.match(c))
|
||||
|
||||
elif match_pattern:
|
||||
|
||||
def content_match(c):
|
||||
return match_pattern in c
|
||||
|
||||
else:
|
||||
|
||||
def content_match(_):
|
||||
return True
|
||||
|
||||
@@ -337,22 +410,29 @@ class Cleanup:
|
||||
return False
|
||||
|
||||
to_delete = await self.get_messages_for_deletion(
|
||||
ctx, channel, number, check=check, limit=1000, before=ctx.message
|
||||
ctx,
|
||||
channel,
|
||||
number,
|
||||
check=check,
|
||||
limit=1000,
|
||||
before=ctx.message,
|
||||
delete_pinned=delete_pinned,
|
||||
)
|
||||
|
||||
# Selfbot convenience, delete trigger message
|
||||
if author == self.bot.user:
|
||||
to_delete.append(ctx.message)
|
||||
|
||||
if channel.name:
|
||||
channel_name = 'channel ' + channel.name
|
||||
if ctx.guild:
|
||||
channel_name = "channel " + channel.name
|
||||
else:
|
||||
channel_name = str(channel)
|
||||
|
||||
reason = "{}({}) deleted {} messages "\
|
||||
"sent by the bot in {}."\
|
||||
"".format(author.name, author.id, len(to_delete),
|
||||
channel_name)
|
||||
reason = (
|
||||
"{}({}) deleted {} messages "
|
||||
"sent by the bot in {}."
|
||||
"".format(author.name, author.id, len(to_delete), channel_name)
|
||||
)
|
||||
log.info(reason)
|
||||
|
||||
if is_bot and can_mass_purge:
|
||||
|
||||
@@ -1,15 +1,11 @@
|
||||
import subprocess
|
||||
|
||||
TO_TRANSLATE = [
|
||||
'../cleanup.py'
|
||||
]
|
||||
TO_TRANSLATE = ["../cleanup.py"]
|
||||
|
||||
|
||||
def regen_messages():
|
||||
subprocess.run(
|
||||
['pygettext', '-n'] + TO_TRANSLATE
|
||||
)
|
||||
subprocess.run(["pygettext", "-n"] + TO_TRANSLATE)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
regen_messages()
|
||||
regen_messages()
|
||||
|
||||
@@ -4,13 +4,12 @@ import random
|
||||
from datetime import datetime
|
||||
|
||||
import discord
|
||||
from discord.ext import commands
|
||||
|
||||
from redbot.core import Config, checks
|
||||
from redbot.core import Config, checks, commands
|
||||
from redbot.core.utils.chat_formatting import box, pagify
|
||||
from redbot.core.i18n import CogI18n
|
||||
from redbot.core.i18n import Translator, cog_i18n
|
||||
|
||||
_ = CogI18n("CustomCommands", __file__)
|
||||
_ = Translator("CustomCommands", __file__)
|
||||
|
||||
|
||||
class CCError(Exception):
|
||||
@@ -26,10 +25,9 @@ class AlreadyExists(CCError):
|
||||
|
||||
|
||||
class CommandObj:
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
config = kwargs.get('config')
|
||||
self.bot = kwargs.get('bot')
|
||||
config = kwargs.get("config")
|
||||
self.bot = kwargs.get("bot")
|
||||
self.db = config.guild
|
||||
|
||||
@staticmethod
|
||||
@@ -41,22 +39,23 @@ class CommandObj:
|
||||
return customcommands
|
||||
|
||||
async def get_responses(self, ctx):
|
||||
intro = (_("Welcome to the interactive random {} maker!\n"
|
||||
"Every message you send will be added as one of the random "
|
||||
"response to choose from once this {} is "
|
||||
"triggered. To exit this interactive menu, type `{}`").format(
|
||||
"customcommand", "customcommand", "exit()"
|
||||
))
|
||||
intro = _(
|
||||
"Welcome to the interactive random {} maker!\n"
|
||||
"Every message you send will be added as one of the random "
|
||||
"responses to choose from once this {} is "
|
||||
"triggered. To exit this interactive menu, type `{}`"
|
||||
).format("customcommand", "customcommand", "exit()")
|
||||
await ctx.send(intro)
|
||||
|
||||
def check(m):
|
||||
return m.channel == ctx.channel and m.author == ctx.message.author
|
||||
|
||||
responses = []
|
||||
while True:
|
||||
await ctx.send(_("Add a random response:"))
|
||||
msg = await self.bot.wait_for('message', check=check)
|
||||
msg = await self.bot.wait_for("message", check=check)
|
||||
|
||||
if msg.content.lower() == 'exit()':
|
||||
if msg.content.lower() == "exit()":
|
||||
break
|
||||
else:
|
||||
responses.append(msg.content)
|
||||
@@ -65,44 +64,31 @@ class CommandObj:
|
||||
def get_now(self) -> str:
|
||||
# Get current time as a string, for 'created_at' and 'edited_at' fields
|
||||
# in the ccinfo dict
|
||||
return '{:%d/%m/%Y %H:%M:%S}'.format(datetime.utcnow())
|
||||
return "{:%d/%m/%Y %H:%M:%S}".format(datetime.utcnow())
|
||||
|
||||
async def get(self,
|
||||
message: discord.Message,
|
||||
command: str) -> str:
|
||||
async def get(self, message: discord.Message, command: str) -> str:
|
||||
ccinfo = await self.db(message.guild).commands.get_raw(command, default=None)
|
||||
if not ccinfo:
|
||||
raise NotFound
|
||||
else:
|
||||
return ccinfo['response']
|
||||
return ccinfo["response"]
|
||||
|
||||
async def create(self,
|
||||
ctx: commands.Context,
|
||||
command: str,
|
||||
response):
|
||||
"""Create a customcommand"""
|
||||
async def create(self, ctx: commands.Context, command: str, response):
|
||||
"""Create a custom command"""
|
||||
# Check if this command is already registered as a customcommand
|
||||
if await self.db(ctx.guild).commands.get_raw(command, default=None):
|
||||
raise AlreadyExists()
|
||||
author = ctx.message.author
|
||||
ccinfo = {
|
||||
'author': {
|
||||
'id': author.id,
|
||||
'name': author.name
|
||||
},
|
||||
'command': command,
|
||||
'created_at': self.get_now(),
|
||||
'editors': [],
|
||||
'response': response
|
||||
|
||||
"author": {"id": author.id, "name": author.name},
|
||||
"command": command,
|
||||
"created_at": self.get_now(),
|
||||
"editors": [],
|
||||
"response": response,
|
||||
}
|
||||
await self.db(ctx.guild).commands.set_raw(
|
||||
command, value=ccinfo)
|
||||
await self.db(ctx.guild).commands.set_raw(command, value=ccinfo)
|
||||
|
||||
async def edit(self,
|
||||
ctx: commands.Context,
|
||||
command: str,
|
||||
response: None):
|
||||
async def edit(self, ctx: commands.Context, command: str, response: None):
|
||||
"""Edit an already existing custom command"""
|
||||
# Check if this command is registered
|
||||
if not await self.db(ctx.guild).commands.get_raw(command, default=None):
|
||||
@@ -115,83 +101,78 @@ class CommandObj:
|
||||
return m.channel == ctx.channel and m.author == ctx.message.author
|
||||
|
||||
if not response:
|
||||
await ctx.send(
|
||||
_("Do you want to create a 'randomized' cc? {}").format("y/n")
|
||||
)
|
||||
await ctx.send(_("Do you want to create a 'randomized' cc? {}").format("y/n"))
|
||||
|
||||
msg = await self.bot.wait_for('message', check=check)
|
||||
if msg.content.lower() == 'y':
|
||||
msg = await self.bot.wait_for("message", check=check)
|
||||
if msg.content.lower() == "y":
|
||||
response = await self.get_responses(ctx=ctx)
|
||||
else:
|
||||
await ctx.send(_("What response do you want?"))
|
||||
response = (await self.bot.wait_for(
|
||||
'message', check=check)
|
||||
).content
|
||||
response = (await self.bot.wait_for("message", check=check)).content
|
||||
|
||||
ccinfo['response'] = response
|
||||
ccinfo['edited_at'] = self.get_now()
|
||||
ccinfo["response"] = response
|
||||
ccinfo["edited_at"] = self.get_now()
|
||||
|
||||
if author.id not in ccinfo['editors']:
|
||||
if author.id not in ccinfo["editors"]:
|
||||
# Add the person who invoked the `edit` coroutine to the list of
|
||||
# editors, if the person is not yet in there
|
||||
ccinfo['editors'].append(
|
||||
author.id
|
||||
)
|
||||
ccinfo["editors"].append(author.id)
|
||||
|
||||
await self.db(ctx.guild).commands.set_raw(
|
||||
command, value=ccinfo)
|
||||
await self.db(ctx.guild).commands.set_raw(command, value=ccinfo)
|
||||
|
||||
async def delete(self,
|
||||
ctx: commands.Context,
|
||||
command: str):
|
||||
async def delete(self, ctx: commands.Context, command: str):
|
||||
"""Delete an already exisiting custom command"""
|
||||
# Check if this command is registered
|
||||
if not await self.db(ctx.guild).commands.get_raw(command, default=None):
|
||||
raise NotFound()
|
||||
await self.db(ctx.guild).commands.set_raw(
|
||||
command, value=None)
|
||||
await self.db(ctx.guild).commands.set_raw(command, value=None)
|
||||
|
||||
|
||||
@cog_i18n(_)
|
||||
class CustomCommands:
|
||||
"""Custom commands
|
||||
|
||||
Creates commands used to display text"""
|
||||
|
||||
def __init__(self, bot):
|
||||
self.bot = bot
|
||||
self.key = 414589031223512
|
||||
self.config = Config.get_conf(self,
|
||||
self.key)
|
||||
self.config = Config.get_conf(self, self.key)
|
||||
self.config.register_guild(commands={})
|
||||
self.commandobj = CommandObj(config=self.config,
|
||||
bot=self.bot)
|
||||
self.commandobj = CommandObj(config=self.config, bot=self.bot)
|
||||
|
||||
@commands.group(aliases=["cc"], no_pm=True)
|
||||
@commands.group(aliases=["cc"])
|
||||
@commands.guild_only()
|
||||
async def customcom(self,
|
||||
ctx: commands.Context):
|
||||
async def customcom(self, ctx: commands.Context):
|
||||
"""Custom commands management"""
|
||||
if not ctx.invoked_subcommand:
|
||||
await ctx.send_help()
|
||||
pass
|
||||
|
||||
@customcom.group(name="add")
|
||||
@checks.mod_or_permissions(administrator=True)
|
||||
async def cc_add(self,
|
||||
ctx: commands.Context):
|
||||
async def cc_add(self, ctx: commands.Context):
|
||||
"""
|
||||
CCs can be enhanced with arguments:
|
||||
https: // twentysix26.github.io / Red - Docs / red_guide_command_args/
|
||||
"""
|
||||
if not ctx.invoked_subcommand or isinstance(ctx.invoked_subcommand,
|
||||
commands.Group):
|
||||
await ctx.send_help()
|
||||
|
||||
@cc_add.command(name='random')
|
||||
Argument What it will be substituted with
|
||||
|
||||
{message} message
|
||||
|
||||
{author} message.author
|
||||
|
||||
{channel} message.channel
|
||||
|
||||
{guild} message.guild
|
||||
|
||||
{server} message.guild
|
||||
"""
|
||||
pass
|
||||
|
||||
@cc_add.command(name="random")
|
||||
@checks.mod_or_permissions(administrator=True)
|
||||
async def cc_add_random(self,
|
||||
ctx: commands.Context,
|
||||
command: str):
|
||||
async def cc_add_random(self, ctx: commands.Context, command: str):
|
||||
"""
|
||||
Create a CC where it will randomly choose a response!
|
||||
|
||||
Note: This is interactive
|
||||
"""
|
||||
channel = ctx.channel
|
||||
@@ -199,27 +180,22 @@ class CustomCommands:
|
||||
|
||||
responses = await self.commandobj.get_responses(ctx=ctx)
|
||||
try:
|
||||
await self.commandobj.create(ctx=ctx,
|
||||
command=command,
|
||||
response=responses)
|
||||
await self.commandobj.create(ctx=ctx, command=command, response=responses)
|
||||
await ctx.send(_("Custom command successfully added."))
|
||||
except AlreadyExists:
|
||||
await ctx.send(_(
|
||||
"This command already exists. Use "
|
||||
"`{}` to edit it.").format(
|
||||
await ctx.send(
|
||||
_("This command already exists. Use `{}` to edit it.").format(
|
||||
"{}customcom edit".format(ctx.prefix)
|
||||
))
|
||||
)
|
||||
)
|
||||
|
||||
# await ctx.send(str(responses))
|
||||
|
||||
@cc_add.command(name="simple")
|
||||
@checks.mod_or_permissions(administrator=True)
|
||||
async def cc_add_simple(self,
|
||||
ctx,
|
||||
command: str,
|
||||
*,
|
||||
text):
|
||||
async def cc_add_simple(self, ctx, command: str, *, text):
|
||||
"""Adds a simple custom command
|
||||
|
||||
Example:
|
||||
[p]customcom add simple yourcommand Text you want
|
||||
"""
|
||||
@@ -229,25 +205,20 @@ class CustomCommands:
|
||||
await ctx.send(_("That command is already a standard command."))
|
||||
return
|
||||
try:
|
||||
await self.commandobj.create(ctx=ctx,
|
||||
command=command,
|
||||
response=text)
|
||||
await self.commandobj.create(ctx=ctx, command=command, response=text)
|
||||
await ctx.send(_("Custom command successfully added."))
|
||||
except AlreadyExists:
|
||||
await ctx.send(_(
|
||||
"This command already exists. Use "
|
||||
"`{}` to edit it.").format(
|
||||
await ctx.send(
|
||||
_("This command already exists. Use `{}` to edit it.").format(
|
||||
"{}customcom edit".format(ctx.prefix)
|
||||
))
|
||||
)
|
||||
)
|
||||
|
||||
@customcom.command(name="edit")
|
||||
@checks.mod_or_permissions(administrator=True)
|
||||
async def cc_edit(self,
|
||||
ctx,
|
||||
command: str,
|
||||
*,
|
||||
text=None):
|
||||
async def cc_edit(self, ctx, command: str, *, text=None):
|
||||
"""Edits a custom command
|
||||
|
||||
Example:
|
||||
[p]customcom edit yourcommand Text you want
|
||||
"""
|
||||
@@ -255,61 +226,55 @@ class CustomCommands:
|
||||
command = command.lower()
|
||||
|
||||
try:
|
||||
await self.commandobj.edit(ctx=ctx,
|
||||
command=command,
|
||||
response=text)
|
||||
await self.commandobj.edit(ctx=ctx, command=command, response=text)
|
||||
await ctx.send(_("Custom command successfully edited."))
|
||||
except NotFound:
|
||||
await ctx.send(_(
|
||||
"That command doesn't exist. Use "
|
||||
"`{}` to add it.").format(
|
||||
await ctx.send(
|
||||
_("That command doesn't exist. Use `{}` to add it.").format(
|
||||
"{}customcom add".format(ctx.prefix)
|
||||
))
|
||||
)
|
||||
)
|
||||
|
||||
@customcom.command(name="delete")
|
||||
@checks.mod_or_permissions(administrator=True)
|
||||
async def cc_delete(self,
|
||||
ctx,
|
||||
command: str):
|
||||
async def cc_delete(self, ctx, command: str):
|
||||
"""Deletes a custom command
|
||||
Example:
|
||||
[p]customcom delete yourcommand"""
|
||||
guild = ctx.message.guild
|
||||
command = command.lower()
|
||||
try:
|
||||
await self.commandobj.delete(ctx=ctx,
|
||||
command=command)
|
||||
await self.commandobj.delete(ctx=ctx, command=command)
|
||||
await ctx.send(_("Custom command successfully deleted."))
|
||||
except NotFound:
|
||||
await ctx.send(_("That command doesn't exist."))
|
||||
|
||||
@customcom.command(name="list")
|
||||
async def cc_list(self,
|
||||
ctx):
|
||||
async def cc_list(self, ctx):
|
||||
"""Shows custom commands list"""
|
||||
|
||||
response = await CommandObj.get_commands(self.config.guild(ctx.guild))
|
||||
|
||||
if not response:
|
||||
await ctx.send(_(
|
||||
"There are no custom commands in this server."
|
||||
" Use `{}` to start adding some.").format(
|
||||
"{}customcom add".format(ctx.prefix)
|
||||
))
|
||||
await ctx.send(
|
||||
_(
|
||||
"There are no custom commands in this server."
|
||||
" Use `{}` to start adding some."
|
||||
).format("{}customcom add".format(ctx.prefix))
|
||||
)
|
||||
return
|
||||
|
||||
results = []
|
||||
|
||||
for command, body in response.items():
|
||||
responses = body['response']
|
||||
responses = body["response"]
|
||||
if isinstance(responses, list):
|
||||
result = ", ".join(responses)
|
||||
elif isinstance(responses, str):
|
||||
result = responses
|
||||
else:
|
||||
continue
|
||||
results.append("{command:<15} : {result}".format(command=command,
|
||||
result=result))
|
||||
results.append("{command:<15} : {result}".format(command=command, result=result))
|
||||
|
||||
commands = "\n".join(results)
|
||||
|
||||
@@ -319,14 +284,13 @@ class CustomCommands:
|
||||
for page in pagify(commands, delims=[" ", "\n"]):
|
||||
await ctx.author.send(box(page))
|
||||
|
||||
async def on_message(self,
|
||||
message):
|
||||
async def on_message(self, message):
|
||||
is_private = isinstance(message.channel, discord.abc.PrivateChannel)
|
||||
if len(message.content) < 2 or is_private:
|
||||
return
|
||||
|
||||
guild = message.guild
|
||||
prefixes = await self.bot.db.guild(guild).get_raw('prefix', default=[])
|
||||
prefixes = await self.bot.db.guild(guild).get_raw("prefix", default=[])
|
||||
|
||||
if len(prefixes) < 1:
|
||||
def_prefixes = await self.bot.get_prefix(message)
|
||||
@@ -345,10 +309,9 @@ class CustomCommands:
|
||||
return
|
||||
|
||||
if user_allowed:
|
||||
cmd = message.content[len(prefix):]
|
||||
cmd = message.content[len(prefix) :]
|
||||
try:
|
||||
c = await self.commandobj.get(message=message,
|
||||
command=cmd)
|
||||
c = await self.commandobj.get(message=message, command=cmd)
|
||||
if isinstance(c, list):
|
||||
command = random.choice(c)
|
||||
elif isinstance(c, str):
|
||||
@@ -360,18 +323,14 @@ class CustomCommands:
|
||||
response = self.format_cc(command, message)
|
||||
await message.channel.send(response)
|
||||
|
||||
def format_cc(self,
|
||||
command,
|
||||
message) -> str:
|
||||
def format_cc(self, command, message) -> str:
|
||||
results = re.findall("\{([^}]+)\}", command)
|
||||
for result in results:
|
||||
param = self.transform_parameter(result, message)
|
||||
command = command.replace("{" + result + "}", param)
|
||||
return command
|
||||
|
||||
def transform_parameter(self,
|
||||
result,
|
||||
message) -> str:
|
||||
def transform_parameter(self, result, message) -> str:
|
||||
"""
|
||||
For security reasons only specific objects are allowed
|
||||
Internals are ignored
|
||||
@@ -382,7 +341,7 @@ class CustomCommands:
|
||||
"author": message.author,
|
||||
"channel": message.channel,
|
||||
"guild": message.guild,
|
||||
"server": message.guild
|
||||
"server": message.guild,
|
||||
}
|
||||
if result in objects:
|
||||
return str(objects[result])
|
||||
|
||||
@@ -1,15 +1,11 @@
|
||||
import subprocess
|
||||
|
||||
TO_TRANSLATE = [
|
||||
'../customcom.py'
|
||||
]
|
||||
TO_TRANSLATE = ["../customcom.py"]
|
||||
|
||||
|
||||
def regen_messages():
|
||||
subprocess.run(
|
||||
['pygettext', '-n'] + TO_TRANSLATE
|
||||
)
|
||||
subprocess.run(["pygettext", "-n"] + TO_TRANSLATE)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
regen_messages()
|
||||
regen_messages()
|
||||
|
||||
@@ -16,49 +16,49 @@ class SpecResolver(object):
|
||||
self.v2path = path
|
||||
self.resolved = set()
|
||||
self.available_core_conversions = {
|
||||
'Bank Accounts': {
|
||||
'cfg': ('Bank', None, 384734293238749),
|
||||
'file': self.v2path / 'data' / 'economy' / 'bank.json',
|
||||
'converter': self.bank_accounts_conv_spec
|
||||
"Bank Accounts": {
|
||||
"cfg": ("Bank", None, 384734293238749),
|
||||
"file": self.v2path / "data" / "economy" / "bank.json",
|
||||
"converter": self.bank_accounts_conv_spec,
|
||||
},
|
||||
'Economy Settings': {
|
||||
'cfg': ('Economy', 'config', 1256844281),
|
||||
'file': self.v2path / 'data' / 'economy' / 'settings.json',
|
||||
'converter': self.economy_conv_spec
|
||||
"Economy Settings": {
|
||||
"cfg": ("Economy", "config", 1256844281),
|
||||
"file": self.v2path / "data" / "economy" / "settings.json",
|
||||
"converter": self.economy_conv_spec,
|
||||
},
|
||||
'Mod Log Cases': {
|
||||
'cfg': ('ModLog', None, 1354799444),
|
||||
'file': self.v2path / 'data' / 'mod' / 'modlog.json',
|
||||
'converter': None # prevents from showing as available
|
||||
"Mod Log Cases": {
|
||||
"cfg": ("ModLog", None, 1354799444),
|
||||
"file": self.v2path / "data" / "mod" / "modlog.json",
|
||||
"converter": None, # prevents from showing as available
|
||||
},
|
||||
'Filter': {
|
||||
'cfg': ('Filter', 'settings', 4766951341),
|
||||
'file': self.v2path / 'data' / 'mod' / 'filter.json',
|
||||
'converter': self.filter_conv_spec
|
||||
"Filter": {
|
||||
"cfg": ("Filter", "settings", 4766951341),
|
||||
"file": self.v2path / "data" / "mod" / "filter.json",
|
||||
"converter": self.filter_conv_spec,
|
||||
},
|
||||
'Past Names': {
|
||||
'cfg': ('Mod', 'settings', 4961522000),
|
||||
'file': self.v2path / 'data' / 'mod' / 'past_names.json',
|
||||
'converter': self.past_names_conv_spec
|
||||
"Past Names": {
|
||||
"cfg": ("Mod", "settings", 4961522000),
|
||||
"file": self.v2path / "data" / "mod" / "past_names.json",
|
||||
"converter": self.past_names_conv_spec,
|
||||
},
|
||||
'Past Nicknames': {
|
||||
'cfg': ('Mod', 'settings', 4961522000),
|
||||
'file': self.v2path / 'data' / 'mod' / 'past_nicknames.json',
|
||||
'converter': self.past_nicknames_conv_spec
|
||||
"Past Nicknames": {
|
||||
"cfg": ("Mod", "settings", 4961522000),
|
||||
"file": self.v2path / "data" / "mod" / "past_nicknames.json",
|
||||
"converter": self.past_nicknames_conv_spec,
|
||||
},
|
||||
"Custom Commands": {
|
||||
"cfg": ("CustomCommands", "config", 414589031223512),
|
||||
"file": self.v2path / "data" / "customcom" / "commands.json",
|
||||
"converter": self.customcom_conv_spec,
|
||||
},
|
||||
'Custom Commands': {
|
||||
'cfg': ('CustomCommands', 'config', 414589031223512),
|
||||
'file': self.v2path / 'data' / 'customcom' / 'commands.json',
|
||||
'converter': self.customcom_conv_spec
|
||||
}
|
||||
}
|
||||
|
||||
@property
|
||||
def available(self):
|
||||
return sorted(
|
||||
k for k, v in self.available_core_conversions.items()
|
||||
if v['file'].is_file() and v['converter'] is not None
|
||||
and k not in self.resolved
|
||||
k
|
||||
for k, v in self.available_core_conversions.items()
|
||||
if v["file"].is_file() and v["converter"] is not None and k not in self.resolved
|
||||
)
|
||||
|
||||
def unpack(self, parent_key, parent_value):
|
||||
@@ -75,15 +75,8 @@ class SpecResolver(object):
|
||||
"""Flatten a nested dictionary structure"""
|
||||
dictionary = {(key,): value for key, value in dictionary.items()}
|
||||
while True:
|
||||
dictionary = dict(
|
||||
chain.from_iterable(
|
||||
starmap(self.unpack, dictionary.items())
|
||||
)
|
||||
)
|
||||
if not any(
|
||||
isinstance(value, dict)
|
||||
for value in dictionary.values()
|
||||
):
|
||||
dictionary = dict(chain.from_iterable(starmap(self.unpack, dictionary.items())))
|
||||
if not any(isinstance(value, dict) for value in dictionary.values()):
|
||||
break
|
||||
return dictionary
|
||||
|
||||
@@ -97,11 +90,8 @@ class SpecResolver(object):
|
||||
outerkey, innerkey = tuple(k[:-1]), (k[-1],)
|
||||
if outerkey not in ret:
|
||||
ret[outerkey] = {}
|
||||
if innerkey[0] == 'created_at':
|
||||
x = int(
|
||||
datetime.strptime(
|
||||
v, "%Y-%m-%d %H:%M:%S").timestamp()
|
||||
)
|
||||
if innerkey[0] == "created_at":
|
||||
x = int(datetime.strptime(v, "%Y-%m-%d %H:%M:%S").timestamp())
|
||||
ret[outerkey].update({innerkey: x})
|
||||
else:
|
||||
ret[outerkey].update({innerkey: v})
|
||||
@@ -121,60 +111,60 @@ class SpecResolver(object):
|
||||
raise NotImplementedError("This one isn't ready yet")
|
||||
|
||||
def filter_conv_spec(self, data: dict):
|
||||
return {
|
||||
(Config.GUILD, k): {('filter',): v}
|
||||
for k, v in data.items()
|
||||
}
|
||||
return {(Config.GUILD, k): {("filter",): v} for k, v in data.items()}
|
||||
|
||||
def past_names_conv_spec(self, data: dict):
|
||||
return {
|
||||
(Config.USER, k): {('past_names',): v}
|
||||
for k, v in data.items()
|
||||
}
|
||||
return {(Config.USER, k): {("past_names",): v} for k, v in data.items()}
|
||||
|
||||
def past_nicknames_conv_spec(self, data: dict):
|
||||
flatscoped = self.apply_scope(Config.MEMBER, self.flatten_dict(data))
|
||||
ret = {}
|
||||
for k, v in flatscoped.items():
|
||||
outerkey, innerkey = (*k[:-1],), (k[-1],)
|
||||
if outerkey not in ret:
|
||||
ret[outerkey] = {}
|
||||
ret[outerkey].update({innerkey: v})
|
||||
for config_identifiers, v2data in flatscoped.items():
|
||||
if config_identifiers not in ret:
|
||||
ret[config_identifiers] = {}
|
||||
ret[config_identifiers].update({("past_nicks",): v2data})
|
||||
return ret
|
||||
|
||||
def customcom_conv_spec(self, data: dict):
|
||||
flatscoped = self.apply_scope(Config.GUILD, self.flatten_dict(data))
|
||||
ret = {}
|
||||
for k, v in flatscoped.items():
|
||||
outerkey, innerkey = (*k[:-1],), ('commands', k[-1])
|
||||
outerkey, innerkey = (*k[:-1],), ("commands", k[-1])
|
||||
if outerkey not in ret:
|
||||
ret[outerkey] = {}
|
||||
|
||||
ccinfo = {
|
||||
'author': {
|
||||
'id': 42,
|
||||
'name': 'Converted from a v2 instance'
|
||||
},
|
||||
'command': k[-1],
|
||||
'created_at': '{:%d/%m/%Y %H:%M:%S}'.format(datetime.utcnow()),
|
||||
'editors': [],
|
||||
'response': v
|
||||
"author": {"id": 42, "name": "Converted from a v2 instance"},
|
||||
"command": k[-1],
|
||||
"created_at": "{:%d/%m/%Y %H:%M:%S}".format(datetime.utcnow()),
|
||||
"editors": [],
|
||||
"response": v,
|
||||
}
|
||||
ret[outerkey].update({innerkey: ccinfo})
|
||||
return ret
|
||||
|
||||
async def convert(self, bot: Red, prettyname: str):
|
||||
if prettyname not in self.available:
|
||||
raise NotImplementedError("No Conversion Specs for this")
|
||||
|
||||
info = self.available_core_conversions[prettyname]
|
||||
filepath, converter = info['file'], info['converter']
|
||||
(cogname, attr, _id) = info['cfg']
|
||||
def get_config_object(self, bot, cogname, attr, _id):
|
||||
try:
|
||||
config = getattr(bot.get_cog(cogname), attr)
|
||||
except (TypeError, AttributeError):
|
||||
config = Config.get_conf(None, _id, cog_name=cogname)
|
||||
|
||||
return config
|
||||
|
||||
def get_conversion_info(self, prettyname: str):
|
||||
info = self.available_core_conversions[prettyname]
|
||||
filepath, converter = info["file"], info["converter"]
|
||||
(cogname, attr, _id) = info["cfg"]
|
||||
return filepath, converter, cogname, attr, _id
|
||||
|
||||
async def convert(self, bot: Red, prettyname: str, config=None):
|
||||
if prettyname not in self.available:
|
||||
raise NotImplementedError("No Conversion Specs for this")
|
||||
|
||||
filepath, converter, cogname, attr, _id = self.get_conversion_info(prettyname)
|
||||
if config is None:
|
||||
config = self.get_config_object(bot, cogname, attr, _id)
|
||||
|
||||
try:
|
||||
items = converter(dc.json_load(filepath))
|
||||
await dc(config).dict_import(items)
|
||||
|
||||
@@ -1,17 +1,16 @@
|
||||
from pathlib import Path
|
||||
import asyncio
|
||||
|
||||
from discord.ext import commands
|
||||
|
||||
from redbot.core import checks, RedContext
|
||||
from redbot.core import checks, commands
|
||||
from redbot.core.bot import Red
|
||||
from redbot.core.i18n import CogI18n
|
||||
from redbot.core.i18n import Translator, cog_i18n
|
||||
from redbot.cogs.dataconverter.core_specs import SpecResolver
|
||||
from redbot.core.utils.chat_formatting import box
|
||||
|
||||
_ = CogI18n('DataConverter', __file__)
|
||||
_ = Translator("DataConverter", __file__)
|
||||
|
||||
|
||||
@cog_i18n(_)
|
||||
class DataConverter:
|
||||
"""
|
||||
Cog for importing Red v2 Data
|
||||
@@ -22,7 +21,7 @@ class DataConverter:
|
||||
|
||||
@checks.is_owner()
|
||||
@commands.command(name="convertdata")
|
||||
async def dataconversioncommand(self, ctx: RedContext, v2path: str):
|
||||
async def dataconversioncommand(self, ctx: commands.Context, v2path: str):
|
||||
"""
|
||||
Interactive prompt for importing data from Red v2
|
||||
|
||||
@@ -35,13 +34,14 @@ class DataConverter:
|
||||
|
||||
if not resolver.available:
|
||||
return await ctx.send(
|
||||
_("There don't seem to be any data files I know how to "
|
||||
"handle here. Are you sure you gave me the base "
|
||||
"installation path?")
|
||||
_(
|
||||
"There don't seem to be any data files I know how to "
|
||||
"handle here. Are you sure you gave me the base "
|
||||
"installation path?"
|
||||
)
|
||||
)
|
||||
while resolver.available:
|
||||
menu = _("Please select a set of data to import by number"
|
||||
", or 'exit' to exit")
|
||||
menu = _("Please select a set of data to import by number, or 'exit' to exit")
|
||||
for index, entry in enumerate(resolver.available, 1):
|
||||
menu += "\n{}. {}".format(index, entry)
|
||||
|
||||
@@ -51,24 +51,17 @@ class DataConverter:
|
||||
return m.channel == ctx.channel and m.author == ctx.author
|
||||
|
||||
try:
|
||||
message = await self.bot.wait_for(
|
||||
'message', check=pred, timeout=60
|
||||
)
|
||||
message = await self.bot.wait_for("message", check=pred, timeout=60)
|
||||
except asyncio.TimeoutError:
|
||||
return await ctx.send(
|
||||
_('Try this again when you are more ready'))
|
||||
return await ctx.send(_("Try this again when you are more ready"))
|
||||
else:
|
||||
if message.content.strip().lower() in [
|
||||
'quit', 'exit', '-1', 'q', 'cancel'
|
||||
]:
|
||||
if message.content.strip().lower() in ["quit", "exit", "-1", "q", "cancel"]:
|
||||
return await ctx.tick()
|
||||
try:
|
||||
message = int(message.content.strip())
|
||||
to_conv = resolver.available[message - 1]
|
||||
except (ValueError, IndexError):
|
||||
await ctx.send(
|
||||
_("That wasn't a valid choice.")
|
||||
)
|
||||
await ctx.send(_("That wasn't a valid choice."))
|
||||
continue
|
||||
else:
|
||||
async with ctx.typing():
|
||||
@@ -77,6 +70,8 @@ class DataConverter:
|
||||
await menu_message.delete()
|
||||
else:
|
||||
return await ctx.send(
|
||||
_("There isn't anything else I know how to convert here."
|
||||
"\nThere might be more things I can convert in the future.")
|
||||
_(
|
||||
"There isn't anything else I know how to convert here."
|
||||
"\nThere might be more things I can convert in the future."
|
||||
)
|
||||
)
|
||||
|
||||
@@ -1,14 +1,10 @@
|
||||
import subprocess
|
||||
|
||||
TO_TRANSLATE = [
|
||||
'../dataconverter.py'
|
||||
]
|
||||
TO_TRANSLATE = ["../dataconverter.py"]
|
||||
|
||||
|
||||
def regen_messages():
|
||||
subprocess.run(
|
||||
['pygettext', '-n'] + TO_TRANSLATE
|
||||
)
|
||||
subprocess.run(["pygettext", "-n"] + TO_TRANSLATE)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import asyncio
|
||||
|
||||
import discord
|
||||
from discord.ext import commands
|
||||
from redbot.core import commands
|
||||
|
||||
__all__ = ["install_agreement", ]
|
||||
__all__ = ["do_install_agreement"]
|
||||
|
||||
REPO_INSTALL_MSG = (
|
||||
"You're about to add a 3rd party repository. The creator of Red"
|
||||
@@ -16,30 +16,21 @@ REPO_INSTALL_MSG = (
|
||||
)
|
||||
|
||||
|
||||
def install_agreement():
|
||||
async def pred(ctx: commands.Context):
|
||||
downloader = ctx.command.instance
|
||||
if downloader is None:
|
||||
return True
|
||||
elif downloader.already_agreed:
|
||||
return True
|
||||
elif ctx.invoked_subcommand is None or \
|
||||
isinstance(ctx.invoked_subcommand, commands.Group):
|
||||
return True
|
||||
|
||||
def does_agree(msg: discord.Message):
|
||||
return ctx.author == msg.author and \
|
||||
ctx.channel == msg.channel and \
|
||||
msg.content == "I agree"
|
||||
|
||||
await ctx.send(REPO_INSTALL_MSG)
|
||||
|
||||
try:
|
||||
await ctx.bot.wait_for('message', check=does_agree, timeout=30)
|
||||
except asyncio.TimeoutError:
|
||||
await ctx.send("Your response has timed out, please try again.")
|
||||
return False
|
||||
|
||||
downloader.already_agreed = True
|
||||
async def do_install_agreement(ctx: commands.Context):
|
||||
downloader = ctx.cog
|
||||
if downloader is None or downloader.already_agreed:
|
||||
return True
|
||||
return commands.check(pred)
|
||||
|
||||
def does_agree(msg: discord.Message):
|
||||
return ctx.author == msg.author and ctx.channel == msg.channel and msg.content == "I agree"
|
||||
|
||||
await ctx.send(REPO_INSTALL_MSG)
|
||||
|
||||
try:
|
||||
await ctx.bot.wait_for("message", check=does_agree, timeout=30)
|
||||
except asyncio.TimeoutError:
|
||||
await ctx.send("Your response has timed out, please try again.")
|
||||
return False
|
||||
|
||||
downloader.already_agreed = True
|
||||
return True
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import discord
|
||||
from discord.ext import commands
|
||||
from .repo_manager import RepoManager
|
||||
from redbot.core import commands
|
||||
from .installable import Installable
|
||||
|
||||
|
||||
@@ -12,8 +11,6 @@ class InstalledCog(commands.Converter):
|
||||
|
||||
cog = discord.utils.get(await downloader.installed_cogs(), name=arg)
|
||||
if cog is None:
|
||||
raise commands.BadArgument(
|
||||
"That cog is not installed"
|
||||
)
|
||||
raise commands.BadArgument("That cog is not installed")
|
||||
|
||||
return cog
|
||||
|
||||
@@ -10,31 +10,29 @@ import sys
|
||||
from redbot.core import Config
|
||||
from redbot.core import checks
|
||||
from redbot.core.data_manager import cog_data_path
|
||||
from redbot.core.i18n import CogI18n
|
||||
from redbot.core.i18n import Translator, cog_i18n
|
||||
from redbot.core.utils.chat_formatting import box, pagify
|
||||
from discord.ext import commands
|
||||
from redbot.core import commands
|
||||
|
||||
from redbot.core.bot import Red
|
||||
from .checks import install_agreement
|
||||
from .checks import do_install_agreement
|
||||
from .converters import InstalledCog
|
||||
from .errors import CloningError, ExistingGitRepo
|
||||
from .installable import Installable
|
||||
from .log import log
|
||||
from .repo_manager import RepoManager, Repo
|
||||
|
||||
_ = CogI18n('Downloader', __file__)
|
||||
_ = Translator("Downloader", __file__)
|
||||
|
||||
|
||||
@cog_i18n(_)
|
||||
class Downloader:
|
||||
def __init__(self, bot: Red):
|
||||
self.bot = bot
|
||||
|
||||
self.conf = Config.get_conf(self, identifier=998240343,
|
||||
force_registration=True)
|
||||
self.conf = Config.get_conf(self, identifier=998240343, force_registration=True)
|
||||
|
||||
self.conf.register_global(
|
||||
installed=[]
|
||||
)
|
||||
self.conf.register_global(installed=[])
|
||||
|
||||
self.already_agreed = False
|
||||
|
||||
@@ -45,17 +43,17 @@ class Downloader:
|
||||
self.LIB_PATH.mkdir(parents=True, exist_ok=True)
|
||||
self.SHAREDLIB_PATH.mkdir(parents=True, exist_ok=True)
|
||||
if not self.SHAREDLIB_INIT.exists():
|
||||
with self.SHAREDLIB_INIT.open(mode='w', encoding='utf-8') as _:
|
||||
with self.SHAREDLIB_INIT.open(mode="w", encoding="utf-8") as _:
|
||||
pass
|
||||
|
||||
if str(self.LIB_PATH) not in syspath:
|
||||
syspath.insert(1, str(self.LIB_PATH))
|
||||
|
||||
self._repo_manager = RepoManager(self.conf)
|
||||
self._repo_manager = RepoManager()
|
||||
|
||||
async def cog_install_path(self):
|
||||
"""Get the current cog install path.
|
||||
|
||||
|
||||
Returns
|
||||
-------
|
||||
pathlib.Path
|
||||
@@ -66,7 +64,7 @@ class Downloader:
|
||||
|
||||
async def installed_cogs(self) -> Tuple[Installable]:
|
||||
"""Get info on installed cogs.
|
||||
|
||||
|
||||
Returns
|
||||
-------
|
||||
`tuple` of `Installable`
|
||||
@@ -79,7 +77,7 @@ class Downloader:
|
||||
|
||||
async def _add_to_installed(self, cog: Installable):
|
||||
"""Mark a cog as installed.
|
||||
|
||||
|
||||
Parameters
|
||||
----------
|
||||
cog : Installable
|
||||
@@ -95,7 +93,7 @@ class Downloader:
|
||||
|
||||
async def _remove_from_installed(self, cog: Installable):
|
||||
"""Remove a cog from the saved list of installed cogs.
|
||||
|
||||
|
||||
Parameters
|
||||
----------
|
||||
cog : Installable
|
||||
@@ -169,7 +167,7 @@ class Downloader:
|
||||
for repo, reqs in has_reqs:
|
||||
for req in reqs:
|
||||
# noinspection PyTypeChecker
|
||||
ret = ret and await repo.install_raw_requirements([req, ], self.LIB_PATH)
|
||||
ret = ret and await repo.install_raw_requirements([req], self.LIB_PATH)
|
||||
return ret
|
||||
|
||||
@staticmethod
|
||||
@@ -199,8 +197,12 @@ class Downloader:
|
||||
if success:
|
||||
await ctx.send(_("Libraries installed."))
|
||||
else:
|
||||
await ctx.send(_("Some libraries failed to install. Please check"
|
||||
" your logs for a complete list."))
|
||||
await ctx.send(
|
||||
_(
|
||||
"Some libraries failed to install. Please check"
|
||||
" your logs for a complete list."
|
||||
)
|
||||
)
|
||||
|
||||
@commands.group()
|
||||
@checks.is_owner()
|
||||
@@ -208,25 +210,22 @@ class Downloader:
|
||||
"""
|
||||
Command group for managing Downloader repos.
|
||||
"""
|
||||
if ctx.invoked_subcommand is None:
|
||||
await ctx.send_help()
|
||||
pass
|
||||
|
||||
@repo.command(name="add")
|
||||
@install_agreement()
|
||||
async def _repo_add(self, ctx, name: str, repo_url: str, branch: str=None):
|
||||
async def _repo_add(self, ctx, name: str, repo_url: str, branch: str = None):
|
||||
"""
|
||||
Add a new repo to Downloader.
|
||||
|
||||
Name can only contain characters A-z, numbers and underscore
|
||||
Branch will default to master if not specified
|
||||
"""
|
||||
agreed = await do_install_agreement(ctx)
|
||||
if not agreed:
|
||||
return
|
||||
try:
|
||||
# noinspection PyTypeChecker
|
||||
repo = await self._repo_manager.add_repo(
|
||||
name=name,
|
||||
url=repo_url,
|
||||
branch=branch
|
||||
)
|
||||
repo = await self._repo_manager.add_repo(name=name, url=repo_url, branch=branch)
|
||||
except ExistingGitRepo:
|
||||
await ctx.send(_("That git repo has already been added under another name."))
|
||||
except CloningError:
|
||||
@@ -235,7 +234,7 @@ class Downloader:
|
||||
else:
|
||||
await ctx.send(_("Repo `{}` successfully added.").format(name))
|
||||
if repo.install_msg is not None:
|
||||
await ctx.send(repo.install_msg)
|
||||
await ctx.send(repo.install_msg.replace("[p]", ctx.prefix))
|
||||
|
||||
@repo.command(name="delete")
|
||||
async def _repo_del(self, ctx, repo_name: Repo):
|
||||
@@ -253,19 +252,33 @@ class Downloader:
|
||||
"""
|
||||
repos = self._repo_manager.get_all_repo_names()
|
||||
repos = sorted(repos, key=str.lower)
|
||||
joined = _("Installed Repos:\n") + "\n".join(["+ " + r for r in repos])
|
||||
joined = _("Installed Repos:\n\n")
|
||||
for repo_name in repos:
|
||||
repo = self._repo_manager.get_repo(repo_name)
|
||||
joined += "+ {}: {}\n".format(repo.name, repo.short or "")
|
||||
|
||||
for page in pagify(joined, ["\n"], shorten_by=16):
|
||||
await ctx.send(box(page.lstrip(" "), lang="diff"))
|
||||
|
||||
@repo.command(name="info")
|
||||
async def _repo_info(self, ctx, repo_name: Repo):
|
||||
"""
|
||||
Lists information about a single repo
|
||||
"""
|
||||
if repo_name is None:
|
||||
await ctx.send(_("There is no repo `{}`").format(repo_name.name))
|
||||
return
|
||||
|
||||
msg = _("Information on {}:\n{}").format(repo_name.name, repo_name.description or "")
|
||||
await ctx.send(box(msg))
|
||||
|
||||
@commands.group()
|
||||
@checks.is_owner()
|
||||
async def cog(self, ctx):
|
||||
"""
|
||||
Command group for managing installable Cogs.
|
||||
"""
|
||||
if ctx.invoked_subcommand is None:
|
||||
await ctx.send_help()
|
||||
pass
|
||||
|
||||
@cog.command(name="install")
|
||||
async def _cog_install(self, ctx, repo_name: Repo, cog_name: str):
|
||||
@@ -274,20 +287,28 @@ class Downloader:
|
||||
"""
|
||||
cog = discord.utils.get(repo_name.available_cogs, name=cog_name) # type: Installable
|
||||
if cog is None:
|
||||
await ctx.send(_("Error, there is no cog by the name of"
|
||||
" `{}` in the `{}` repo.").format(cog_name, repo_name.name))
|
||||
await ctx.send(
|
||||
_("Error, there is no cog by the name of `{}` in the `{}` repo.").format(
|
||||
cog_name, repo_name.name
|
||||
)
|
||||
)
|
||||
return
|
||||
elif cog.min_python_version > sys.version_info:
|
||||
await ctx.send(_(
|
||||
"This cog requires at least python version {}, aborting install.".format(
|
||||
'.'.join([str(n) for n in cog.min_python_version])
|
||||
await ctx.send(
|
||||
_(
|
||||
"This cog requires at least python version {}, aborting install.".format(
|
||||
".".join([str(n) for n in cog.min_python_version])
|
||||
)
|
||||
)
|
||||
))
|
||||
)
|
||||
return
|
||||
|
||||
if not await repo_name.install_requirements(cog, self.LIB_PATH):
|
||||
await ctx.send(_("Failed to install the required libraries for"
|
||||
" `{}`: `{}`").format(cog.name, cog.requirements))
|
||||
await ctx.send(
|
||||
_("Failed to install the required libraries for `{}`: `{}`").format(
|
||||
cog.name, cog.requirements
|
||||
)
|
||||
)
|
||||
return
|
||||
|
||||
await repo_name.install_cog(cog, await self.cog_install_path())
|
||||
@@ -298,7 +319,7 @@ class Downloader:
|
||||
|
||||
await ctx.send(_("`{}` cog successfully installed.").format(cog_name))
|
||||
if cog.install_msg is not None:
|
||||
await ctx.send(cog.install_msg)
|
||||
await ctx.send(cog.install_msg.replace("[p]", ctx.prefix))
|
||||
|
||||
@cog.command(name="uninstall")
|
||||
async def _cog_uninstall(self, ctx, cog_name: InstalledCog):
|
||||
@@ -316,12 +337,16 @@ class Downloader:
|
||||
await self._remove_from_installed(cog_name)
|
||||
await ctx.send(_("`{}` was successfully removed.").format(real_name))
|
||||
else:
|
||||
await ctx.send(_("That cog was installed but can no longer"
|
||||
" be located. You may need to remove it's"
|
||||
" files manually if it is still usable."))
|
||||
await ctx.send(
|
||||
_(
|
||||
"That cog was installed but can no longer"
|
||||
" be located. You may need to remove it's"
|
||||
" files manually if it is still usable."
|
||||
)
|
||||
)
|
||||
|
||||
@cog.command(name="update")
|
||||
async def _cog_update(self, ctx, cog_name: InstalledCog=None):
|
||||
async def _cog_update(self, ctx, cog_name: InstalledCog = None):
|
||||
"""
|
||||
Updates all cogs or one of your choosing.
|
||||
"""
|
||||
@@ -355,11 +380,27 @@ class Downloader:
|
||||
"""
|
||||
Lists all available cogs from a single repo.
|
||||
"""
|
||||
installed = await self.installed_cogs()
|
||||
installed_str = ""
|
||||
if installed:
|
||||
installed_str = _("Installed Cogs:\n") + "\n".join(
|
||||
[
|
||||
"- {}{}".format(i.name, ": {}".format(i.short) if i.short else "")
|
||||
for i in installed
|
||||
if i.repo_name == repo_name.name
|
||||
]
|
||||
)
|
||||
cogs = repo_name.available_cogs
|
||||
cogs = _("Available Cogs:\n") + "\n".join(
|
||||
["+ {}: {}".format(c.name, c.short or "") for c in cogs])
|
||||
|
||||
await ctx.send(box(cogs, lang="diff"))
|
||||
[
|
||||
"+ {}: {}".format(c.name, c.short or "")
|
||||
for c in cogs
|
||||
if not (c.hidden or c in installed)
|
||||
]
|
||||
)
|
||||
cogs = cogs + "\n\n" + installed_str
|
||||
for page in pagify(cogs, ["\n"], shorten_by=16):
|
||||
await ctx.send(box(page.lstrip(" "), lang="diff"))
|
||||
|
||||
@cog.command(name="info")
|
||||
async def _cog_info(self, ctx, repo_name: Repo, cog_name: str):
|
||||
@@ -368,15 +409,19 @@ class Downloader:
|
||||
"""
|
||||
cog = discord.utils.get(repo_name.available_cogs, name=cog_name)
|
||||
if cog is None:
|
||||
await ctx.send(_("There is no cog `{}` in the repo `{}`").format(
|
||||
cog_name, repo_name.name
|
||||
))
|
||||
await ctx.send(
|
||||
_("There is no cog `{}` in the repo `{}`").format(cog_name, repo_name.name)
|
||||
)
|
||||
return
|
||||
|
||||
msg = _("Information on {}:\n{}").format(cog.name, cog.description or "")
|
||||
msg = _("Information on {}:\n{}\n\nRequirements: {}").format(
|
||||
cog.name, cog.description or "", ", ".join(cog.requirements) or "None"
|
||||
)
|
||||
await ctx.send(box(msg))
|
||||
|
||||
async def is_installed(self, cog_name: str) -> (bool, Union[Installable, None]):
|
||||
async def is_installed(
|
||||
self, cog_name: str
|
||||
) -> Union[Tuple[bool, Installable], Tuple[bool, None]]:
|
||||
"""Check to see if a cog has been installed through Downloader.
|
||||
|
||||
Parameters
|
||||
@@ -396,8 +441,9 @@ class Downloader:
|
||||
return True, installable
|
||||
return False, None
|
||||
|
||||
def format_findcog_info(self, command_name: str,
|
||||
cog_installable: Union[Installable, object]=None) -> str:
|
||||
def format_findcog_info(
|
||||
self, command_name: str, cog_installable: Union[Installable, object] = None
|
||||
) -> str:
|
||||
"""Format a cog's info for output to discord.
|
||||
|
||||
Parameters
|
||||
@@ -406,7 +452,7 @@ class Downloader:
|
||||
Name of the command which belongs to the cog.
|
||||
cog_installable : `Installable` or `object`
|
||||
Can be an `Installable` instance or a Cog instance.
|
||||
|
||||
|
||||
Returns
|
||||
-------
|
||||
str
|
||||
@@ -420,7 +466,7 @@ class Downloader:
|
||||
cog_name = cog_installable.name
|
||||
else:
|
||||
made_by = "26 & co."
|
||||
repo_url = "https://github.com/Twentysix26/Red-DiscordBot"
|
||||
repo_url = "https://github.com/Cog-Creators/Red-DiscordBot"
|
||||
cog_name = cog_installable.__class__.__name__
|
||||
|
||||
msg = _("Command: {}\nMade by: {}\nRepo: {}\nCog name: {}")
|
||||
@@ -431,19 +477,19 @@ class Downloader:
|
||||
"""Determines the cog name that Downloader knows from the cog instance.
|
||||
|
||||
Probably.
|
||||
|
||||
|
||||
Parameters
|
||||
----------
|
||||
instance : object
|
||||
The cog instance.
|
||||
|
||||
|
||||
Returns
|
||||
-------
|
||||
str
|
||||
The name of the cog according to Downloader..
|
||||
|
||||
"""
|
||||
splitted = instance.__module__.split('.')
|
||||
splitted = instance.__module__.split(".")
|
||||
return splitted[-2]
|
||||
|
||||
@commands.command()
|
||||
|
||||
@@ -1,12 +1,23 @@
|
||||
__all__ = ["DownloaderException", "GitException", "InvalidRepoName", "ExistingGitRepo",
|
||||
"MissingGitRepo", "CloningError", "CurrentHashError", "HardResetError",
|
||||
"UpdateError", "GitDiffError", "PipError"]
|
||||
__all__ = [
|
||||
"DownloaderException",
|
||||
"GitException",
|
||||
"InvalidRepoName",
|
||||
"ExistingGitRepo",
|
||||
"MissingGitRepo",
|
||||
"CloningError",
|
||||
"CurrentHashError",
|
||||
"HardResetError",
|
||||
"UpdateError",
|
||||
"GitDiffError",
|
||||
"PipError",
|
||||
]
|
||||
|
||||
|
||||
class DownloaderException(Exception):
|
||||
"""
|
||||
Base class for Downloader exceptions.
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
@@ -21,6 +32,7 @@ class InvalidRepoName(DownloaderException):
|
||||
Throw when a repo name is invalid. Check
|
||||
the message for a more detailed reason.
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
@@ -29,6 +41,7 @@ class ExistingGitRepo(DownloaderException):
|
||||
Thrown when trying to clone into a folder where a
|
||||
git repo already exists.
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
@@ -37,6 +50,7 @@ class MissingGitRepo(DownloaderException):
|
||||
Thrown when a git repo is expected to exist but
|
||||
does not.
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
@@ -44,6 +58,7 @@ class CloningError(GitException):
|
||||
"""
|
||||
Thrown when git clone returns a non zero exit code.
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
@@ -52,6 +67,7 @@ class CurrentHashError(GitException):
|
||||
Thrown when git returns a non zero exit code attempting
|
||||
to determine the current commit hash.
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
@@ -60,6 +76,7 @@ class HardResetError(GitException):
|
||||
Thrown when there is an issue trying to execute a hard reset
|
||||
(usually prior to a repo update).
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
@@ -67,6 +84,7 @@ class UpdateError(GitException):
|
||||
"""
|
||||
Thrown when git pull returns a non zero error code.
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
@@ -74,6 +92,7 @@ class GitDiffError(GitException):
|
||||
"""
|
||||
Thrown when a git diff fails.
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
@@ -81,4 +100,5 @@ class PipError(DownloaderException):
|
||||
"""
|
||||
Thrown when pip returns a non-zero return code.
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
@@ -3,9 +3,8 @@ import distutils.dir_util
|
||||
import shutil
|
||||
from enum import Enum
|
||||
from pathlib import Path
|
||||
from typing import MutableMapping, Any
|
||||
from typing import MutableMapping, Any, TYPE_CHECKING
|
||||
|
||||
from redbot.core.utils import TYPE_CHECKING
|
||||
from .log import log
|
||||
from .json_mixins import RepoJSONMixin
|
||||
|
||||
@@ -25,7 +24,7 @@ class Installable(RepoJSONMixin):
|
||||
- Modules
|
||||
- Repo Libraries
|
||||
- Other stuff?
|
||||
|
||||
|
||||
The attributes of this class will mostly come from the installation's
|
||||
info.json.
|
||||
|
||||
@@ -56,6 +55,7 @@ class Installable(RepoJSONMixin):
|
||||
:class:`InstallationType`.
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, location: Path):
|
||||
"""Base installable initializer.
|
||||
|
||||
@@ -75,6 +75,7 @@ class Installable(RepoJSONMixin):
|
||||
self.bot_version = (3, 0, 0)
|
||||
self.min_python_version = (3, 5, 1)
|
||||
self.hidden = False
|
||||
self.disabled = False
|
||||
self.required_cogs = {} # Cog name -> repo URL
|
||||
self.requirements = ()
|
||||
self.tags = ()
|
||||
@@ -114,13 +115,9 @@ class Installable(RepoJSONMixin):
|
||||
|
||||
# noinspection PyBroadException
|
||||
try:
|
||||
copy_func(
|
||||
src=str(self._location),
|
||||
dst=str(target_dir / self._location.stem)
|
||||
)
|
||||
copy_func(src=str(self._location), dst=str(target_dir / self._location.stem))
|
||||
except:
|
||||
log.exception("Error occurred when copying path:"
|
||||
" {}".format(self._location))
|
||||
log.exception("Error occurred when copying path: {}".format(self._location))
|
||||
return False
|
||||
return True
|
||||
|
||||
@@ -130,7 +127,7 @@ class Installable(RepoJSONMixin):
|
||||
if self._info_file.exists():
|
||||
self._process_info_file()
|
||||
|
||||
def _process_info_file(self, info_file_path: Path=None) -> MutableMapping[str, Any]:
|
||||
def _process_info_file(self, info_file_path: Path = None) -> MutableMapping[str, Any]:
|
||||
"""
|
||||
Processes an information file. Loads dependencies among other
|
||||
information into this object.
|
||||
@@ -144,13 +141,12 @@ class Installable(RepoJSONMixin):
|
||||
raise ValueError("No valid information file path was found.")
|
||||
|
||||
info = {}
|
||||
with info_file_path.open(encoding='utf-8') as f:
|
||||
with info_file_path.open(encoding="utf-8") as f:
|
||||
try:
|
||||
info = json.load(f)
|
||||
except json.JSONDecodeError:
|
||||
info = {}
|
||||
log.exception("Invalid JSON information file at path:"
|
||||
" {}".format(info_file_path))
|
||||
log.exception("Invalid JSON information file at path: {}".format(info_file_path))
|
||||
else:
|
||||
self._info = info
|
||||
|
||||
@@ -167,7 +163,7 @@ class Installable(RepoJSONMixin):
|
||||
self.bot_version = bot_version
|
||||
|
||||
try:
|
||||
min_python_version = tuple(info.get('min_python_version', [3, 5, 1]))
|
||||
min_python_version = tuple(info.get("min_python_version", [3, 5, 1]))
|
||||
except ValueError:
|
||||
min_python_version = self.min_python_version
|
||||
self.min_python_version = min_python_version
|
||||
@@ -178,6 +174,12 @@ class Installable(RepoJSONMixin):
|
||||
hidden = False
|
||||
self.hidden = hidden
|
||||
|
||||
try:
|
||||
disabled = bool(info.get("disabled", False))
|
||||
except ValueError:
|
||||
disabled = False
|
||||
self.disabled = disabled
|
||||
|
||||
self.required_cogs = info.get("required_cogs", {})
|
||||
|
||||
self.requirements = info.get("requirements", ())
|
||||
@@ -200,15 +202,12 @@ class Installable(RepoJSONMixin):
|
||||
return info
|
||||
|
||||
def to_json(self):
|
||||
return {
|
||||
"repo_name": self.repo_name,
|
||||
"cog_name": self.name
|
||||
}
|
||||
return {"repo_name": self.repo_name, "cog_name": self.name}
|
||||
|
||||
@classmethod
|
||||
def from_json(cls, data: dict, repo_mgr: "RepoManager"):
|
||||
repo_name = data['repo_name']
|
||||
cog_name = data['cog_name']
|
||||
repo_name = data["repo_name"]
|
||||
cog_name = data["cog_name"]
|
||||
|
||||
repo = repo_mgr.get_repo(repo_name)
|
||||
if repo is not None:
|
||||
|
||||
@@ -24,7 +24,7 @@ class RepoJSONMixin:
|
||||
return
|
||||
|
||||
try:
|
||||
with self._info_file.open(encoding='utf-8') as f:
|
||||
with self._info_file.open(encoding="utf-8") as f:
|
||||
info = json.load(f)
|
||||
except json.JSONDecodeError:
|
||||
return
|
||||
@@ -34,4 +34,4 @@ class RepoJSONMixin:
|
||||
self.author = info.get("author")
|
||||
self.install_msg = info.get("install_msg")
|
||||
self.short = info.get("short")
|
||||
self.description = info.get("description")
|
||||
self.description = info.get("description")
|
||||
|
||||
@@ -1,15 +1,11 @@
|
||||
import subprocess
|
||||
|
||||
TO_TRANSLATE = [
|
||||
'../downloader.py'
|
||||
]
|
||||
TO_TRANSLATE = ["../downloader.py"]
|
||||
|
||||
|
||||
def regen_messages():
|
||||
subprocess.run(
|
||||
['pygettext', '-n'] + TO_TRANSLATE
|
||||
)
|
||||
subprocess.run(["pygettext", "-n"] + TO_TRANSLATE)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
regen_messages()
|
||||
regen_messages()
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
import logging
|
||||
|
||||
log = logging.getLogger("red.downloader")
|
||||
log = logging.getLogger("red.downloader")
|
||||
|
||||
@@ -2,17 +2,13 @@ import asyncio
|
||||
import functools
|
||||
import os
|
||||
import pkgutil
|
||||
import shutil
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
from pathlib import Path
|
||||
from subprocess import run as sp_run, PIPE
|
||||
from sys import executable
|
||||
from typing import Tuple, MutableMapping, Union
|
||||
|
||||
from discord.ext import commands
|
||||
|
||||
from redbot.core import Config
|
||||
from redbot.core import data_manager
|
||||
from redbot.core import data_manager, commands
|
||||
from redbot.core.utils import safe_delete
|
||||
from .errors import *
|
||||
from .installable import Installable, InstallableType
|
||||
@@ -27,16 +23,21 @@ class Repo(RepoJSONMixin):
|
||||
GIT_LATEST_COMMIT = "git -C {path} rev-parse {branch}"
|
||||
GIT_HARD_RESET = "git -C {path} reset --hard origin/{branch} -q"
|
||||
GIT_PULL = "git -C {path} pull -q --ff-only"
|
||||
GIT_DIFF_FILE_STATUS = ("git -C {path} diff --no-commit-id --name-status"
|
||||
" {old_hash} {new_hash}")
|
||||
GIT_LOG = ("git -C {path} log --relative-date --reverse {old_hash}.."
|
||||
" {relative_file_path}")
|
||||
GIT_DIFF_FILE_STATUS = "git -C {path} diff --no-commit-id --name-status {old_hash} {new_hash}"
|
||||
GIT_LOG = "git -C {path} log --relative-date --reverse {old_hash}.. {relative_file_path}"
|
||||
GIT_DISCOVER_REMOTE_URL = "git -C {path} config --get remote.origin.url"
|
||||
|
||||
PIP_INSTALL = "{python} -m pip install -U -t {target_dir} {reqs}"
|
||||
|
||||
def __init__(self, name: str, url: str, branch: str, folder_path: Path,
|
||||
available_modules: Tuple[Installable]=(), loop: asyncio.AbstractEventLoop=None):
|
||||
def __init__(
|
||||
self,
|
||||
name: str,
|
||||
url: str,
|
||||
branch: str,
|
||||
folder_path: Path,
|
||||
available_modules: Tuple[Installable] = (),
|
||||
loop: asyncio.AbstractEventLoop = None,
|
||||
):
|
||||
self.url = url
|
||||
self.branch = branch
|
||||
|
||||
@@ -71,11 +72,12 @@ class Repo(RepoJSONMixin):
|
||||
return poss_repo
|
||||
|
||||
def _existing_git_repo(self) -> (bool, Path):
|
||||
git_path = self.folder_path / '.git'
|
||||
git_path = self.folder_path / ".git"
|
||||
return git_path.exists(), git_path
|
||||
|
||||
async def _get_file_update_statuses(
|
||||
self, old_hash: str, new_hash: str) -> MutableMapping[str, str]:
|
||||
self, old_hash: str, new_hash: str
|
||||
) -> MutableMapping[str, str]:
|
||||
"""
|
||||
Gets the file update status letters for each changed file between
|
||||
the two hashes.
|
||||
@@ -85,29 +87,25 @@ class Repo(RepoJSONMixin):
|
||||
"""
|
||||
p = await self._run(
|
||||
self.GIT_DIFF_FILE_STATUS.format(
|
||||
path=self.folder_path,
|
||||
old_hash=old_hash,
|
||||
new_hash=new_hash
|
||||
path=self.folder_path, old_hash=old_hash, new_hash=new_hash
|
||||
)
|
||||
)
|
||||
|
||||
if p.returncode != 0:
|
||||
raise GitDiffError("Git diff failed for repo at path:"
|
||||
" {}".format(self.folder_path))
|
||||
raise GitDiffError("Git diff failed for repo at path: {}".format(self.folder_path))
|
||||
|
||||
stdout = p.stdout.strip().decode().split('\n')
|
||||
stdout = p.stdout.strip().decode().split("\n")
|
||||
|
||||
ret = {}
|
||||
|
||||
for filename in stdout:
|
||||
# TODO: filter these filenames by ones in self.available_modules
|
||||
status, _, filepath = filename.partition('\t')
|
||||
status, _, filepath = filename.partition("\t")
|
||||
ret[filepath] = status
|
||||
|
||||
return ret
|
||||
|
||||
async def _get_commit_notes(self, old_commit_hash: str,
|
||||
relative_file_path: str) -> str:
|
||||
async def _get_commit_notes(self, old_commit_hash: str, relative_file_path: str) -> str:
|
||||
"""
|
||||
Gets the commit notes from git log.
|
||||
:param old_commit_hash: Point in time to start getting messages
|
||||
@@ -119,13 +117,15 @@ class Repo(RepoJSONMixin):
|
||||
self.GIT_LOG.format(
|
||||
path=self.folder_path,
|
||||
old_hash=old_commit_hash,
|
||||
relative_file_path=relative_file_path
|
||||
relative_file_path=relative_file_path,
|
||||
)
|
||||
)
|
||||
|
||||
if p.returncode != 0:
|
||||
raise GitException("An exception occurred while executing git log on"
|
||||
" this repo: {}".format(self.folder_path))
|
||||
raise GitException(
|
||||
"An exception occurred while executing git log on"
|
||||
" this repo: {}".format(self.folder_path)
|
||||
)
|
||||
|
||||
return p.stdout.decode().strip()
|
||||
|
||||
@@ -146,10 +146,11 @@ class Repo(RepoJSONMixin):
|
||||
Installable(location=name)
|
||||
)
|
||||
"""
|
||||
for file_finder, name, is_pkg in pkgutil.walk_packages(path=[str(self.folder_path), ]):
|
||||
curr_modules.append(
|
||||
Installable(location=self.folder_path / name)
|
||||
)
|
||||
for file_finder, name, is_pkg in pkgutil.walk_packages(
|
||||
path=[str(self.folder_path)], onerror=lambda name: None
|
||||
):
|
||||
if is_pkg:
|
||||
curr_modules.append(Installable(location=self.folder_path / name))
|
||||
self.available_modules = curr_modules
|
||||
|
||||
# noinspection PyTypeChecker
|
||||
@@ -157,12 +158,11 @@ class Repo(RepoJSONMixin):
|
||||
|
||||
async def _run(self, *args, **kwargs):
|
||||
env = os.environ.copy()
|
||||
env['GIT_TERMINAL_PROMPT'] = '0'
|
||||
kwargs['env'] = env
|
||||
env["GIT_TERMINAL_PROMPT"] = "0"
|
||||
kwargs["env"] = env
|
||||
async with self._repo_lock:
|
||||
return await self._loop.run_in_executor(
|
||||
self._executor,
|
||||
functools.partial(sp_run, *args, stdout=PIPE, **kwargs)
|
||||
self._executor, functools.partial(sp_run, *args, stdout=PIPE, **kwargs)
|
||||
)
|
||||
|
||||
async def clone(self) -> Tuple[str]:
|
||||
@@ -176,24 +176,17 @@ class Repo(RepoJSONMixin):
|
||||
"""
|
||||
exists, path = self._existing_git_repo()
|
||||
if exists:
|
||||
raise ExistingGitRepo(
|
||||
"A git repo already exists at path: {}".format(path)
|
||||
)
|
||||
raise ExistingGitRepo("A git repo already exists at path: {}".format(path))
|
||||
|
||||
if self.branch is not None:
|
||||
p = await self._run(
|
||||
self.GIT_CLONE.format(
|
||||
branch=self.branch,
|
||||
url=self.url,
|
||||
folder=self.folder_path
|
||||
branch=self.branch, url=self.url, folder=self.folder_path
|
||||
).split()
|
||||
)
|
||||
else:
|
||||
p = await self._run(
|
||||
self.GIT_CLONE_NO_BRANCH.format(
|
||||
url=self.url,
|
||||
folder=self.folder_path
|
||||
).split()
|
||||
self.GIT_CLONE_NO_BRANCH.format(url=self.url, folder=self.folder_path).split()
|
||||
)
|
||||
|
||||
if p.returncode != 0:
|
||||
@@ -217,30 +210,25 @@ class Repo(RepoJSONMixin):
|
||||
"""
|
||||
exists, _ = self._existing_git_repo()
|
||||
if not exists:
|
||||
raise MissingGitRepo(
|
||||
"A git repo does not exist at path: {}".format(self.folder_path)
|
||||
)
|
||||
raise MissingGitRepo("A git repo does not exist at path: {}".format(self.folder_path))
|
||||
|
||||
p = await self._run(
|
||||
self.GIT_CURRENT_BRANCH.format(
|
||||
path=self.folder_path
|
||||
).split()
|
||||
)
|
||||
p = await self._run(self.GIT_CURRENT_BRANCH.format(path=self.folder_path).split())
|
||||
|
||||
if p.returncode != 0:
|
||||
raise GitException("Could not determine current branch"
|
||||
" at path: {}".format(self.folder_path))
|
||||
raise GitException(
|
||||
"Could not determine current branch at path: {}".format(self.folder_path)
|
||||
)
|
||||
|
||||
return p.stdout.decode().strip()
|
||||
|
||||
async def current_commit(self, branch: str=None) -> str:
|
||||
async def current_commit(self, branch: str = None) -> str:
|
||||
"""Determine the current commit hash of the repo.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
branch : `str`, optional
|
||||
Override for repo's branch attribute.
|
||||
|
||||
|
||||
Returns
|
||||
-------
|
||||
str
|
||||
@@ -252,15 +240,10 @@ class Repo(RepoJSONMixin):
|
||||
|
||||
exists, _ = self._existing_git_repo()
|
||||
if not exists:
|
||||
raise MissingGitRepo(
|
||||
"A git repo does not exist at path: {}".format(self.folder_path)
|
||||
)
|
||||
raise MissingGitRepo("A git repo does not exist at path: {}".format(self.folder_path))
|
||||
|
||||
p = await self._run(
|
||||
self.GIT_LATEST_COMMIT.format(
|
||||
path=self.folder_path,
|
||||
branch=branch
|
||||
).split()
|
||||
self.GIT_LATEST_COMMIT.format(path=self.folder_path, branch=branch).split()
|
||||
)
|
||||
|
||||
if p.returncode != 0:
|
||||
@@ -268,7 +251,7 @@ class Repo(RepoJSONMixin):
|
||||
|
||||
return p.stdout.decode().strip()
|
||||
|
||||
async def current_url(self, folder: Path=None) -> str:
|
||||
async def current_url(self, folder: Path = None) -> str:
|
||||
"""
|
||||
Discovers the FETCH URL for a Git repo.
|
||||
|
||||
@@ -290,18 +273,14 @@ class Repo(RepoJSONMixin):
|
||||
if folder is None:
|
||||
folder = self.folder_path
|
||||
|
||||
p = await self._run(
|
||||
Repo.GIT_DISCOVER_REMOTE_URL.format(
|
||||
path=folder
|
||||
).split()
|
||||
)
|
||||
p = await self._run(Repo.GIT_DISCOVER_REMOTE_URL.format(path=folder).split())
|
||||
|
||||
if p.returncode != 0:
|
||||
raise RuntimeError("Unable to discover a repo URL.")
|
||||
|
||||
return p.stdout.decode().strip()
|
||||
|
||||
async def hard_reset(self, branch: str=None) -> None:
|
||||
async def hard_reset(self, branch: str = None) -> None:
|
||||
"""Perform a hard reset on the current repo.
|
||||
|
||||
Parameters
|
||||
@@ -315,21 +294,18 @@ class Repo(RepoJSONMixin):
|
||||
|
||||
exists, _ = self._existing_git_repo()
|
||||
if not exists:
|
||||
raise MissingGitRepo(
|
||||
"A git repo does not exist at path: {}".format(self.folder_path)
|
||||
)
|
||||
raise MissingGitRepo("A git repo does not exist at path: {}".format(self.folder_path))
|
||||
|
||||
p = await self._run(
|
||||
self.GIT_HARD_RESET.format(
|
||||
path=self.folder_path,
|
||||
branch=branch
|
||||
).split()
|
||||
self.GIT_HARD_RESET.format(path=self.folder_path, branch=branch).split()
|
||||
)
|
||||
|
||||
if p.returncode != 0:
|
||||
raise HardResetError("Some error occurred when trying to"
|
||||
" execute a hard reset on the repo at"
|
||||
" the following path: {}".format(self.folder_path))
|
||||
raise HardResetError(
|
||||
"Some error occurred when trying to"
|
||||
" execute a hard reset on the repo at"
|
||||
" the following path: {}".format(self.folder_path)
|
||||
)
|
||||
|
||||
async def update(self) -> (str, str):
|
||||
"""Update the current branch of this repo.
|
||||
@@ -345,15 +321,13 @@ class Repo(RepoJSONMixin):
|
||||
|
||||
await self.hard_reset(branch=curr_branch)
|
||||
|
||||
p = await self._run(
|
||||
self.GIT_PULL.format(
|
||||
path=self.folder_path
|
||||
).split()
|
||||
)
|
||||
p = await self._run(self.GIT_PULL.format(path=self.folder_path).split())
|
||||
|
||||
if p.returncode != 0:
|
||||
raise UpdateError("Git pull returned a non zero exit code"
|
||||
" for the repo located at path: {}".format(self.folder_path))
|
||||
raise UpdateError(
|
||||
"Git pull returned a non zero exit code"
|
||||
" for the repo located at path: {}".format(self.folder_path)
|
||||
)
|
||||
|
||||
new_commit = await self.current_commit(branch=curr_branch)
|
||||
|
||||
@@ -389,7 +363,9 @@ class Repo(RepoJSONMixin):
|
||||
|
||||
return await cog.copy_to(target_dir=target_dir)
|
||||
|
||||
async def install_libraries(self, target_dir: Path, libraries: Tuple[Installable]=()) -> bool:
|
||||
async def install_libraries(
|
||||
self, target_dir: Path, libraries: Tuple[Installable] = ()
|
||||
) -> bool:
|
||||
"""Install shared libraries to the target directory.
|
||||
|
||||
If :code:`libraries` is not specified, all shared libraries in the repo
|
||||
@@ -401,7 +377,7 @@ class Repo(RepoJSONMixin):
|
||||
Directory to install shared libraries to.
|
||||
libraries : `tuple` of `Installable`
|
||||
A subset of available libraries.
|
||||
|
||||
|
||||
Returns
|
||||
-------
|
||||
bool
|
||||
@@ -423,7 +399,7 @@ class Repo(RepoJSONMixin):
|
||||
|
||||
async def install_requirements(self, cog: Installable, target_dir: Path) -> bool:
|
||||
"""Install a cog's requirements.
|
||||
|
||||
|
||||
Requirements will be installed via pip directly into
|
||||
:code:`target_dir`.
|
||||
|
||||
@@ -469,29 +445,28 @@ class Repo(RepoJSONMixin):
|
||||
|
||||
p = await self._run(
|
||||
self.PIP_INSTALL.format(
|
||||
python=executable,
|
||||
target_dir=target_dir,
|
||||
reqs=" ".join(requirements)
|
||||
python=executable, target_dir=target_dir, reqs=" ".join(requirements)
|
||||
).split()
|
||||
)
|
||||
|
||||
if p.returncode != 0:
|
||||
log.error("Something went wrong when installing"
|
||||
" the following requirements:"
|
||||
" {}".format(", ".join(requirements)))
|
||||
log.error(
|
||||
"Something went wrong when installing"
|
||||
" the following requirements:"
|
||||
" {}".format(", ".join(requirements))
|
||||
)
|
||||
return False
|
||||
return True
|
||||
|
||||
@property
|
||||
def available_cogs(self) -> Tuple[Installable]:
|
||||
"""`tuple` of `installable` : All available cogs in this Repo.
|
||||
|
||||
|
||||
This excludes hidden or shared packages.
|
||||
"""
|
||||
# noinspection PyTypeChecker
|
||||
return tuple(
|
||||
[m for m in self.available_modules
|
||||
if m.type == InstallableType.COG and not m.hidden]
|
||||
[m for m in self.available_modules if m.type == InstallableType.COG and not m.disabled]
|
||||
)
|
||||
|
||||
@property
|
||||
@@ -501,8 +476,7 @@ class Repo(RepoJSONMixin):
|
||||
"""
|
||||
# noinspection PyTypeChecker
|
||||
return tuple(
|
||||
[m for m in self.available_modules
|
||||
if m.type == InstallableType.SHARED_LIBRARY]
|
||||
[m for m in self.available_modules if m.type == InstallableType.SHARED_LIBRARY]
|
||||
)
|
||||
|
||||
@classmethod
|
||||
@@ -515,8 +489,7 @@ class Repo(RepoJSONMixin):
|
||||
|
||||
|
||||
class RepoManager:
|
||||
def __init__(self, downloader_config: Config):
|
||||
self.downloader_config = downloader_config
|
||||
def __init__(self):
|
||||
|
||||
self._repos = {}
|
||||
|
||||
@@ -526,7 +499,7 @@ class RepoManager:
|
||||
@property
|
||||
def repos_folder(self) -> Path:
|
||||
data_folder = data_manager.cog_data_path(self)
|
||||
return data_folder / 'repos'
|
||||
return data_folder / "repos"
|
||||
|
||||
def does_repo_exist(self, name: str) -> bool:
|
||||
return name in self._repos
|
||||
@@ -537,7 +510,7 @@ class RepoManager:
|
||||
raise InvalidRepoName("Not a valid Python variable name.")
|
||||
return name.lower()
|
||||
|
||||
async def add_repo(self, url: str, name: str, branch: str="master") -> Repo:
|
||||
async def add_repo(self, url: str, name: str, branch: str = "master") -> Repo:
|
||||
"""Add and clone a git repository.
|
||||
|
||||
Parameters
|
||||
@@ -556,14 +529,12 @@ class RepoManager:
|
||||
|
||||
"""
|
||||
if self.does_repo_exist(name):
|
||||
raise InvalidRepoName(
|
||||
"That repo name you provided already exists."
|
||||
" Please choose another."
|
||||
raise ExistingGitRepo(
|
||||
"That repo name you provided already exists. Please choose another."
|
||||
)
|
||||
|
||||
# noinspection PyTypeChecker
|
||||
r = Repo(url=url, name=name, branch=branch,
|
||||
folder_path=self.repos_folder / name)
|
||||
r = Repo(url=url, name=name, branch=branch, folder_path=self.repos_folder / name)
|
||||
await r.clone()
|
||||
|
||||
self._repos[name] = r
|
||||
|
||||
@@ -7,14 +7,14 @@ from enum import Enum
|
||||
import discord
|
||||
|
||||
from redbot.cogs.bank import check_global_setting_guildowner, check_global_setting_admin
|
||||
from redbot.core import Config, bank
|
||||
from redbot.core.i18n import CogI18n
|
||||
from redbot.core.utils.chat_formatting import pagify, box
|
||||
from discord.ext import commands
|
||||
from redbot.core import Config, bank, commands
|
||||
from redbot.core.i18n import Translator, cog_i18n
|
||||
from redbot.core.utils.chat_formatting import box
|
||||
from redbot.core.utils.menus import menu, DEFAULT_CONTROLS
|
||||
|
||||
from redbot.core.bot import Red
|
||||
|
||||
_ = CogI18n("Economy", __file__)
|
||||
_ = Translator("Economy", __file__)
|
||||
|
||||
logger = logging.getLogger("red.economy")
|
||||
|
||||
@@ -37,42 +37,41 @@ class SMReel(Enum):
|
||||
PAYOUTS = {
|
||||
(SMReel.two, SMReel.two, SMReel.six): {
|
||||
"payout": lambda x: x * 2500 + x,
|
||||
"phrase": _("JACKPOT! 226! Your bid has been multiplied * 2500!")
|
||||
"phrase": _("JACKPOT! 226! Your bid has been multiplied * 2500!"),
|
||||
},
|
||||
(SMReel.flc, SMReel.flc, SMReel.flc): {
|
||||
"payout": lambda x: x + 1000,
|
||||
"phrase": _("4LC! +1000!")
|
||||
"phrase": _("4LC! +1000!"),
|
||||
},
|
||||
(SMReel.cherries, SMReel.cherries, SMReel.cherries): {
|
||||
"payout": lambda x: x + 800,
|
||||
"phrase": _("Three cherries! +800!")
|
||||
"phrase": _("Three cherries! +800!"),
|
||||
},
|
||||
(SMReel.two, SMReel.six): {
|
||||
"payout": lambda x: x * 4 + x,
|
||||
"phrase": _("2 6! Your bid has been multiplied * 4!")
|
||||
"phrase": _("2 6! Your bid has been multiplied * 4!"),
|
||||
},
|
||||
(SMReel.cherries, SMReel.cherries): {
|
||||
"payout": lambda x: x * 3 + x,
|
||||
"phrase": _("Two cherries! Your bid has been multiplied * 3!")
|
||||
},
|
||||
"3 symbols": {
|
||||
"payout": lambda x: x + 500,
|
||||
"phrase": _("Three symbols! +500!")
|
||||
"phrase": _("Two cherries! Your bid has been multiplied * 3!"),
|
||||
},
|
||||
"3 symbols": {"payout": lambda x: x + 500, "phrase": _("Three symbols! +500!")},
|
||||
"2 symbols": {
|
||||
"payout": lambda x: x * 2 + x,
|
||||
"phrase": _("Two consecutive symbols! Your bid has been multiplied * 2!")
|
||||
"phrase": _("Two consecutive symbols! Your bid has been multiplied * 2!"),
|
||||
},
|
||||
}
|
||||
|
||||
SLOT_PAYOUTS_MSG = _("Slot machine payouts:\n"
|
||||
"{two.value} {two.value} {six.value} Bet * 2500\n"
|
||||
"{flc.value} {flc.value} {flc.value} +1000\n"
|
||||
"{cherries.value} {cherries.value} {cherries.value} +800\n"
|
||||
"{two.value} {six.value} Bet * 4\n"
|
||||
"{cherries.value} {cherries.value} Bet * 3\n\n"
|
||||
"Three symbols: +500\n"
|
||||
"Two symbols: Bet * 2").format(**SMReel.__dict__)
|
||||
SLOT_PAYOUTS_MSG = _(
|
||||
"Slot machine payouts:\n"
|
||||
"{two.value} {two.value} {six.value} Bet * 2500\n"
|
||||
"{flc.value} {flc.value} {flc.value} +1000\n"
|
||||
"{cherries.value} {cherries.value} {cherries.value} +800\n"
|
||||
"{two.value} {six.value} Bet * 4\n"
|
||||
"{cherries.value} {cherries.value} Bet * 3\n\n"
|
||||
"Three symbols: +500\n"
|
||||
"Two symbols: Bet * 2"
|
||||
).format(**SMReel.__dict__)
|
||||
|
||||
|
||||
def guild_only_check():
|
||||
@@ -83,6 +82,7 @@ def guild_only_check():
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
return commands.check(pred)
|
||||
|
||||
|
||||
@@ -104,6 +104,7 @@ class SetParser:
|
||||
raise RuntimeError
|
||||
|
||||
|
||||
@cog_i18n(_)
|
||||
class Economy:
|
||||
"""Economy
|
||||
|
||||
@@ -115,19 +116,14 @@ class Economy:
|
||||
"SLOT_MIN": 5,
|
||||
"SLOT_MAX": 100,
|
||||
"SLOT_TIME": 0,
|
||||
"REGISTER_CREDITS": 0
|
||||
"REGISTER_CREDITS": 0,
|
||||
}
|
||||
|
||||
default_global_settings = default_guild_settings
|
||||
|
||||
default_member_settings = {
|
||||
"next_payday": 0,
|
||||
"last_slot": 0
|
||||
}
|
||||
default_member_settings = {"next_payday": 0, "last_slot": 0}
|
||||
|
||||
default_role_settings = {
|
||||
"PAYDAY_CREDITS": 0
|
||||
}
|
||||
default_role_settings = {"PAYDAY_CREDITS": 0}
|
||||
|
||||
default_user_settings = default_member_settings
|
||||
|
||||
@@ -142,11 +138,11 @@ class Economy:
|
||||
self.config.register_role(**self.default_role_settings)
|
||||
self.slot_register = defaultdict(dict)
|
||||
|
||||
@guild_only_check()
|
||||
@commands.group(name="bank")
|
||||
async def _bank(self, ctx: commands.Context):
|
||||
"""Bank operations"""
|
||||
if ctx.invoked_subcommand is None:
|
||||
await ctx.send_help()
|
||||
pass
|
||||
|
||||
@_bank.command()
|
||||
async def balance(self, ctx: commands.Context, user: discord.Member = None):
|
||||
@@ -159,8 +155,7 @@ class Economy:
|
||||
bal = await bank.get_balance(user)
|
||||
currency = await bank.get_currency_name(ctx.guild)
|
||||
|
||||
await ctx.send(_("{}'s balance is {} {}").format(
|
||||
user.display_name, bal, currency))
|
||||
await ctx.send(_("{}'s balance is {} {}").format(user.display_name, bal, currency))
|
||||
|
||||
@_bank.command()
|
||||
async def transfer(self, ctx: commands.Context, to: discord.Member, amount: int):
|
||||
@@ -171,11 +166,13 @@ class Economy:
|
||||
try:
|
||||
await bank.transfer_credits(from_, to, amount)
|
||||
except ValueError as e:
|
||||
await ctx.send(str(e))
|
||||
return await ctx.send(str(e))
|
||||
|
||||
await ctx.send(_("{} transferred {} {} to {}").format(
|
||||
from_.display_name, amount, currency, to.display_name
|
||||
))
|
||||
await ctx.send(
|
||||
_("{} transferred {} {} to {}").format(
|
||||
from_.display_name, amount, currency, to.display_name
|
||||
)
|
||||
)
|
||||
|
||||
@_bank.command(name="set")
|
||||
@check_global_setting_admin()
|
||||
@@ -193,43 +190,49 @@ class Economy:
|
||||
|
||||
if creds.operation == "deposit":
|
||||
await bank.deposit_credits(to, creds.sum)
|
||||
await ctx.send(_("{} added {} {} to {}'s account.").format(
|
||||
author.display_name, creds.sum, currency, to.display_name
|
||||
))
|
||||
await ctx.send(
|
||||
_("{} added {} {} to {}'s account.").format(
|
||||
author.display_name, creds.sum, currency, to.display_name
|
||||
)
|
||||
)
|
||||
elif creds.operation == "withdraw":
|
||||
await bank.withdraw_credits(to, creds.sum)
|
||||
await ctx.send(_("{} removed {} {} from {}'s account.").format(
|
||||
author.display_name, creds.sum, currency, to.display_name
|
||||
))
|
||||
await ctx.send(
|
||||
_("{} removed {} {} from {}'s account.").format(
|
||||
author.display_name, creds.sum, currency, to.display_name
|
||||
)
|
||||
)
|
||||
else:
|
||||
await bank.set_balance(to, creds.sum)
|
||||
await ctx.send(_("{} set {}'s account to {} {}.").format(
|
||||
author.display_name, to.display_name, creds.sum, currency
|
||||
))
|
||||
await ctx.send(
|
||||
_("{} set {}'s account to {} {}.").format(
|
||||
author.display_name, to.display_name, creds.sum, currency
|
||||
)
|
||||
)
|
||||
|
||||
@_bank.command()
|
||||
@guild_only_check()
|
||||
@check_global_setting_guildowner()
|
||||
async def reset(self, ctx, confirmation: bool = False):
|
||||
"""Deletes bank accounts"""
|
||||
if confirmation is False:
|
||||
await ctx.send(
|
||||
_("This will delete all bank accounts for {}.\nIf you're sure, type "
|
||||
"`{}bank reset yes`").format(
|
||||
self.bot.user.name if await bank.is_global() else "this server",
|
||||
ctx.prefix
|
||||
_(
|
||||
"This will delete all bank accounts for {}.\nIf you're sure, type "
|
||||
"`{}bank reset yes`"
|
||||
).format(
|
||||
self.bot.user.name if await bank.is_global() else "this server", ctx.prefix
|
||||
)
|
||||
)
|
||||
else:
|
||||
await bank.wipe_bank()
|
||||
await ctx.send(_("All bank accounts for {} have been "
|
||||
"deleted.").format(
|
||||
self.bot.user.name if await bank.is_global() else "this server"
|
||||
)
|
||||
)
|
||||
await ctx.send(
|
||||
_("All bank accounts for {} have been deleted.").format(
|
||||
self.bot.user.name if await bank.is_global() else "this server"
|
||||
)
|
||||
)
|
||||
|
||||
@commands.command()
|
||||
@guild_only_check()
|
||||
@commands.command()
|
||||
async def payday(self, ctx: commands.Context):
|
||||
"""Get some free currency"""
|
||||
author = ctx.author
|
||||
@@ -245,77 +248,98 @@ class Economy:
|
||||
await self.config.user(author).next_payday.set(next_payday)
|
||||
|
||||
pos = await bank.get_leaderboard_position(author)
|
||||
await ctx.send(_(
|
||||
"{0.mention} Here, take some {1}. Enjoy! (+{2} {1}!)\n\n"
|
||||
"You currently have {3} {1}.\n\n"
|
||||
"You are currently #{4} on the leaderboard!"
|
||||
).format(
|
||||
author, credits_name, str(await self.config.PAYDAY_CREDITS()),
|
||||
str(await bank.get_balance(author)), pos
|
||||
))
|
||||
await ctx.send(
|
||||
_(
|
||||
"{0.mention} Here, take some {1}. Enjoy! (+{2} {1}!)\n\n"
|
||||
"You currently have {3} {1}.\n\n"
|
||||
"You are currently #{4} on the global leaderboard!"
|
||||
).format(
|
||||
author,
|
||||
credits_name,
|
||||
str(await self.config.PAYDAY_CREDITS()),
|
||||
str(await bank.get_balance(author)),
|
||||
pos,
|
||||
)
|
||||
)
|
||||
|
||||
else:
|
||||
dtime = self.display_time(next_payday - cur_time)
|
||||
await ctx.send(
|
||||
_("{} Too soon. For your next payday you have to"
|
||||
" wait {}.").format(author.mention, dtime)
|
||||
_("{} Too soon. For your next payday you have to wait {}.").format(
|
||||
author.mention, dtime
|
||||
)
|
||||
)
|
||||
else:
|
||||
next_payday = await self.config.member(author).next_payday()
|
||||
if cur_time >= next_payday:
|
||||
credit_amount = await self.config.guild(guild).PAYDAY_CREDITS()
|
||||
for role in author.roles:
|
||||
role_credits = await self.config.role(role).PAYDAY_CREDITS() # Nice variable name
|
||||
role_credits = await self.config.role(
|
||||
role
|
||||
).PAYDAY_CREDITS() # Nice variable name
|
||||
if role_credits > credit_amount:
|
||||
credit_amount = role_credits
|
||||
await bank.deposit_credits(author, credit_amount)
|
||||
next_payday = cur_time + await self.config.guild(guild).PAYDAY_TIME()
|
||||
await self.config.member(author).next_payday.set(next_payday)
|
||||
pos = await bank.get_leaderboard_position(author)
|
||||
await ctx.send(_(
|
||||
"{0.mention} Here, take some {1}. Enjoy! (+{2} {1}!)\n\n"
|
||||
"You currently have {3} {1}.\n\n"
|
||||
"You are currently #{4} on the leaderboard!"
|
||||
).format(
|
||||
author, credits_name, credit_amount,
|
||||
str(await bank.get_balance(author)), pos
|
||||
))
|
||||
await ctx.send(
|
||||
_(
|
||||
"{0.mention} Here, take some {1}. Enjoy! (+{2} {1}!)\n\n"
|
||||
"You currently have {3} {1}.\n\n"
|
||||
"You are currently #{4} on the leaderboard!"
|
||||
).format(
|
||||
author,
|
||||
credits_name,
|
||||
credit_amount,
|
||||
str(await bank.get_balance(author)),
|
||||
pos,
|
||||
)
|
||||
)
|
||||
else:
|
||||
dtime = self.display_time(next_payday - cur_time)
|
||||
await ctx.send(
|
||||
_("{} Too soon. For your next payday you have to"
|
||||
" wait {}.").format(author.mention, dtime))
|
||||
_("{} Too soon. For your next payday you have to wait {}.").format(
|
||||
author.mention, dtime
|
||||
)
|
||||
)
|
||||
|
||||
@commands.command()
|
||||
@guild_only_check()
|
||||
async def leaderboard(self, ctx: commands.Context, top: int = 10, show_global: bool=False):
|
||||
async def leaderboard(self, ctx: commands.Context, top: int = 10, show_global: bool = False):
|
||||
"""Prints out the leaderboard
|
||||
|
||||
Defaults to top 10"""
|
||||
# Originally coded by Airenkun - edited by irdumb, rewritten by Palm__ for v3
|
||||
guild = ctx.guild
|
||||
author = ctx.author
|
||||
if top < 1:
|
||||
top = 10
|
||||
if await bank.is_global() and show_global: # show_global is only applicable if bank is global
|
||||
if (
|
||||
await bank.is_global() and show_global
|
||||
): # show_global is only applicable if bank is global
|
||||
guild = None
|
||||
bank_sorted = await bank.get_leaderboard(positions=top, guild=guild)
|
||||
if len(bank_sorted) < top:
|
||||
top = len(bank_sorted)
|
||||
highscore = ""
|
||||
for pos, acc in enumerate(bank_sorted, 1):
|
||||
pos = pos
|
||||
poswidth = 2
|
||||
name = acc[1]["name"]
|
||||
namewidth = 35
|
||||
balance = acc[1]["balance"]
|
||||
balwidth = 2
|
||||
highscore += "{pos: <{poswidth}} {name: <{namewidth}s} {balance: >{balwidth}}\n".format(
|
||||
pos=pos, poswidth=poswidth, name=name, namewidth=namewidth,
|
||||
balance=balance, balwidth=balwidth
|
||||
header = f"{f'#':4}{f'Name':36}{f'Score':2}\n"
|
||||
highscores = [
|
||||
(
|
||||
f"{f'{pos}.': <{3 if pos < 10 else 2}} {acc[1]['name']: <{35}s} "
|
||||
f"{acc[1]['balance']: >{2 if pos < 10 else 1}}\n"
|
||||
)
|
||||
if highscore != "":
|
||||
for page in pagify(highscore, shorten_by=12):
|
||||
await ctx.send(box(page, lang="py"))
|
||||
if acc[0] != author.id
|
||||
else (
|
||||
f"{f'{pos}.': <{3 if pos < 10 else 2}} <<{acc[1]['name'] + '>>': <{33}s} "
|
||||
f"{acc[1]['balance']: >{2 if pos < 10 else 1}}\n"
|
||||
)
|
||||
for pos, acc in enumerate(bank_sorted, 1)
|
||||
]
|
||||
if highscores:
|
||||
pages = [
|
||||
f"```md\n{header}{''.join(''.join(highscores[x:x + 10]))}```"
|
||||
for x in range(0, len(highscores), 10)
|
||||
]
|
||||
await menu(ctx, pages, DEFAULT_CONTROLS)
|
||||
else:
|
||||
await ctx.send(_("There are no accounts in the bank."))
|
||||
|
||||
@@ -337,7 +361,11 @@ class Economy:
|
||||
slot_time = await self.config.SLOT_TIME()
|
||||
last_slot = await self.config.user(author).last_slot()
|
||||
else:
|
||||
valid_bid = await self.config.guild(guild).SLOT_MIN() <= bid <= await self.config.guild(guild).SLOT_MAX()
|
||||
valid_bid = (
|
||||
await self.config.guild(guild).SLOT_MIN()
|
||||
<= bid
|
||||
<= await self.config.guild(guild).SLOT_MAX()
|
||||
)
|
||||
slot_time = await self.config.guild(guild).SLOT_TIME()
|
||||
last_slot = await self.config.member(author).last_slot()
|
||||
now = calendar.timegm(ctx.message.created_at.utctimetuple())
|
||||
@@ -364,9 +392,11 @@ class Economy:
|
||||
default_reel.rotate(random.randint(-999, 999)) # weeeeee
|
||||
new_reel = deque(default_reel, maxlen=3) # we need only 3 symbols
|
||||
reels.append(new_reel) # for each reel
|
||||
rows = ((reels[0][0], reels[1][0], reels[2][0]),
|
||||
(reels[0][1], reels[1][1], reels[2][1]),
|
||||
(reels[0][2], reels[1][2], reels[2][2]))
|
||||
rows = (
|
||||
(reels[0][0], reels[1][0], reels[2][0]),
|
||||
(reels[0][1], reels[1][1], reels[2][1]),
|
||||
(reels[0][2], reels[1][2], reels[2][2]),
|
||||
)
|
||||
|
||||
slot = "~~\n~~" # Mobile friendly
|
||||
for i, row in enumerate(rows): # Let's build the slot to show
|
||||
@@ -378,8 +408,7 @@ class Economy:
|
||||
payout = PAYOUTS.get(rows[1])
|
||||
if not payout:
|
||||
# Checks for two-consecutive-symbols special rewards
|
||||
payout = PAYOUTS.get((rows[1][0], rows[1][1]),
|
||||
PAYOUTS.get((rows[1][1], rows[1][2])))
|
||||
payout = PAYOUTS.get((rows[1][0], rows[1][1]), PAYOUTS.get((rows[1][1], rows[1][2])))
|
||||
if not payout:
|
||||
# Still nothing. Let's check for 3 generic same symbols
|
||||
# or 2 consecutive symbols
|
||||
@@ -395,15 +424,20 @@ class Economy:
|
||||
pay = payout["payout"](bid)
|
||||
now = then - bid + pay
|
||||
await bank.set_balance(author, now)
|
||||
await channel.send(_("{}\n{} {}\n\nYour bid: {}\n{} → {}!"
|
||||
"").format(slot, author.mention,
|
||||
payout["phrase"], bid, then, now))
|
||||
await channel.send(
|
||||
_("{}\n{} {}\n\nYour bid: {}\n{} → {}!").format(
|
||||
slot, author.mention, payout["phrase"], bid, then, now
|
||||
)
|
||||
)
|
||||
else:
|
||||
then = await bank.get_balance(author)
|
||||
await bank.withdraw_credits(author, bid)
|
||||
now = then - bid
|
||||
await channel.send(_("{}\n{} Nothing!\nYour bid: {}\n{} → {}!"
|
||||
"").format(slot, author.mention, bid, then, now))
|
||||
await channel.send(
|
||||
_("{}\n{} Nothing!\nYour bid: {}\n{} → {}!").format(
|
||||
slot, author.mention, bid, then, now
|
||||
)
|
||||
)
|
||||
|
||||
@commands.group()
|
||||
@guild_only_check()
|
||||
@@ -412,7 +446,6 @@ class Economy:
|
||||
"""Changes economy module settings"""
|
||||
guild = ctx.guild
|
||||
if ctx.invoked_subcommand is None:
|
||||
await ctx.send_help()
|
||||
if await bank.is_global():
|
||||
slot_min = await self.config.SLOT_MIN()
|
||||
slot_max = await self.config.SLOT_MAX()
|
||||
@@ -427,17 +460,18 @@ class Economy:
|
||||
payday_amount = await self.config.guild(guild).PAYDAY_CREDITS()
|
||||
register_amount = await bank.get_default_balance(guild)
|
||||
msg = box(
|
||||
_("Minimum slot bid: {}\n"
|
||||
"Maximum slot bid: {}\n"
|
||||
"Slot cooldown: {}\n"
|
||||
"Payday amount: {}\n"
|
||||
"Payday cooldown: {}\n"
|
||||
"Amount given at account registration: {}"
|
||||
"").format(
|
||||
slot_min, slot_max, slot_time,
|
||||
payday_amount, payday_time, register_amount
|
||||
_(
|
||||
"Minimum slot bid: {}\n"
|
||||
"Maximum slot bid: {}\n"
|
||||
"Slot cooldown: {}\n"
|
||||
"Payday amount: {}\n"
|
||||
"Payday cooldown: {}\n"
|
||||
"Amount given at account registration: {}"
|
||||
""
|
||||
).format(
|
||||
slot_min, slot_max, slot_time, payday_amount, payday_time, register_amount
|
||||
),
|
||||
_("Current Economy settings:")
|
||||
_("Current Economy settings:"),
|
||||
)
|
||||
await ctx.send(msg)
|
||||
|
||||
@@ -445,7 +479,7 @@ class Economy:
|
||||
async def slotmin(self, ctx: commands.Context, bid: int):
|
||||
"""Minimum slot machine bid"""
|
||||
if bid < 1:
|
||||
await ctx.send(_('Invalid min bid amount.'))
|
||||
await ctx.send(_("Invalid min bid amount."))
|
||||
return
|
||||
guild = ctx.guild
|
||||
if await bank.is_global():
|
||||
@@ -460,8 +494,7 @@ class Economy:
|
||||
"""Maximum slot machine bid"""
|
||||
slot_min = await self.config.SLOT_MIN()
|
||||
if bid < 1 or bid < slot_min:
|
||||
await ctx.send(_('Invalid slotmax bid amount. Must be greater'
|
||||
' than slotmin.'))
|
||||
await ctx.send(_("Invalid slotmax bid amount. Must be greater than slotmin."))
|
||||
return
|
||||
guild = ctx.guild
|
||||
credits_name = await bank.get_currency_name(guild)
|
||||
@@ -489,8 +522,9 @@ class Economy:
|
||||
await self.config.PAYDAY_TIME.set(seconds)
|
||||
else:
|
||||
await self.config.guild(guild).PAYDAY_TIME.set(seconds)
|
||||
await ctx.send(_("Value modified. At least {} seconds must pass "
|
||||
"between each payday.").format(seconds))
|
||||
await ctx.send(
|
||||
_("Value modified. At least {} seconds must pass between each payday.").format(seconds)
|
||||
)
|
||||
|
||||
@economyset.command()
|
||||
async def paydayamount(self, ctx: commands.Context, creds: int):
|
||||
@@ -504,8 +538,7 @@ class Economy:
|
||||
await self.config.PAYDAY_CREDITS.set(creds)
|
||||
else:
|
||||
await self.config.guild(guild).PAYDAY_CREDITS.set(creds)
|
||||
await ctx.send(_("Every payday will now give {} {}."
|
||||
"").format(creds, credits_name))
|
||||
await ctx.send(_("Every payday will now give {} {}.").format(creds, credits_name))
|
||||
|
||||
@economyset.command()
|
||||
async def rolepaydayamount(self, ctx: commands.Context, role: discord.Role, creds: int):
|
||||
@@ -516,8 +549,11 @@ class Economy:
|
||||
await ctx.send("The bank must be per-server for per-role paydays to work.")
|
||||
else:
|
||||
await self.config.role(role).PAYDAY_CREDITS.set(creds)
|
||||
await ctx.send(_("Every payday will now give {} {} to people with the role {}."
|
||||
"").format(creds, credits_name, role.name))
|
||||
await ctx.send(
|
||||
_("Every payday will now give {} {} to people with the role {}.").format(
|
||||
creds, credits_name, role.name
|
||||
)
|
||||
)
|
||||
|
||||
@economyset.command()
|
||||
async def registeramount(self, ctx: commands.Context, creds: int):
|
||||
@@ -527,17 +563,18 @@ class Economy:
|
||||
creds = 0
|
||||
credits_name = await bank.get_currency_name(guild)
|
||||
await bank.set_default_balance(creds, guild)
|
||||
await ctx.send(_("Registering an account will now give {} {}."
|
||||
"").format(creds, credits_name))
|
||||
await ctx.send(
|
||||
_("Registering an account will now give {} {}.").format(creds, credits_name)
|
||||
)
|
||||
|
||||
# What would I ever do without stackoverflow?
|
||||
def display_time(self, seconds, granularity=2):
|
||||
intervals = ( # Source: http://stackoverflow.com/a/24542445
|
||||
(_('weeks'), 604800), # 60 * 60 * 24 * 7
|
||||
(_('days'), 86400), # 60 * 60 * 24
|
||||
(_('hours'), 3600), # 60 * 60
|
||||
(_('minutes'), 60),
|
||||
(_('seconds'), 1),
|
||||
(_("weeks"), 604800), # 60 * 60 * 24 * 7
|
||||
(_("days"), 86400), # 60 * 60 * 24
|
||||
(_("hours"), 3600), # 60 * 60
|
||||
(_("minutes"), 60),
|
||||
(_("seconds"), 1),
|
||||
)
|
||||
|
||||
result = []
|
||||
@@ -547,6 +584,6 @@ class Economy:
|
||||
if value:
|
||||
seconds -= value * count
|
||||
if value == 1:
|
||||
name = name.rstrip('s')
|
||||
name = name.rstrip("s")
|
||||
result.append("{} {}".format(value, name))
|
||||
return ', '.join(result[:granularity])
|
||||
return ", ".join(result[:granularity])
|
||||
|
||||
@@ -1,15 +1,11 @@
|
||||
import subprocess
|
||||
|
||||
TO_TRANSLATE = [
|
||||
'../economy.py'
|
||||
]
|
||||
TO_TRANSLATE = ["../economy.py"]
|
||||
|
||||
|
||||
def regen_messages():
|
||||
subprocess.run(
|
||||
['pygettext', '-n'] + TO_TRANSLATE
|
||||
)
|
||||
subprocess.run(["pygettext", "-n"] + TO_TRANSLATE)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
regen_messages()
|
||||
regen_messages()
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
import discord
|
||||
from discord.ext import commands
|
||||
|
||||
from redbot.core import checks, Config, modlog, RedContext
|
||||
from redbot.core import checks, Config, modlog, commands
|
||||
from redbot.core.bot import Red
|
||||
from redbot.core.i18n import CogI18n
|
||||
from redbot.core.i18n import Translator, cog_i18n
|
||||
from redbot.core.utils.chat_formatting import pagify
|
||||
from redbot.core.utils.mod import is_mod_or_superior
|
||||
|
||||
_ = CogI18n("Filter", __file__)
|
||||
_ = Translator("Filter", __file__)
|
||||
|
||||
|
||||
@cog_i18n(_)
|
||||
class Filter:
|
||||
"""Filter-related commands"""
|
||||
|
||||
@@ -21,12 +21,9 @@ class Filter:
|
||||
"filterban_count": 0,
|
||||
"filterban_time": 0,
|
||||
"filter_names": False,
|
||||
"filter_default_name": "John Doe"
|
||||
}
|
||||
default_member_settings = {
|
||||
"filter_count": 0,
|
||||
"next_reset_time": 0
|
||||
"filter_default_name": "John Doe",
|
||||
}
|
||||
default_member_settings = {"filter_count": 0, "next_reset_time": 0}
|
||||
self.settings.register_guild(**default_guild_settings)
|
||||
self.settings.register_member(**default_member_settings)
|
||||
self.register_task = self.bot.loop.create_task(self.register_filterban())
|
||||
@@ -37,8 +34,7 @@ class Filter:
|
||||
async def register_filterban(self):
|
||||
try:
|
||||
await modlog.register_casetype(
|
||||
"filterban", False, ":filing_cabinet: :hammer:",
|
||||
"Filter ban", "ban"
|
||||
"filterban", False, ":filing_cabinet: :hammer:", "Filter ban", "ban"
|
||||
)
|
||||
except RuntimeError:
|
||||
pass
|
||||
@@ -46,14 +42,13 @@ class Filter:
|
||||
@commands.group(name="filter")
|
||||
@commands.guild_only()
|
||||
@checks.mod_or_permissions(manage_messages=True)
|
||||
async def _filter(self, ctx: RedContext):
|
||||
async def _filter(self, ctx: commands.Context):
|
||||
"""Adds/removes words from filter
|
||||
|
||||
Use double quotes to add/remove sentences
|
||||
Using this command with no subcommands will send
|
||||
the list of the server's filtered words."""
|
||||
if ctx.invoked_subcommand is None:
|
||||
await ctx.send_help()
|
||||
server = ctx.guild
|
||||
author = ctx.author
|
||||
word_list = await self.settings.guild(server).filter()
|
||||
@@ -79,13 +74,12 @@ class Filter:
|
||||
word_list = []
|
||||
tmp = ""
|
||||
for word in split_words:
|
||||
if not word.startswith("\"")\
|
||||
and not word.endswith("\"") and not tmp:
|
||||
if not word.startswith('"') and not word.endswith('"') and not tmp:
|
||||
word_list.append(word)
|
||||
else:
|
||||
if word.startswith("\""):
|
||||
if word.startswith('"'):
|
||||
tmp += word[1:]
|
||||
elif word.endswith("\""):
|
||||
elif word.endswith('"'):
|
||||
tmp += word[:-1]
|
||||
word_list.append(tmp)
|
||||
tmp = ""
|
||||
@@ -110,13 +104,12 @@ class Filter:
|
||||
word_list = []
|
||||
tmp = ""
|
||||
for word in split_words:
|
||||
if not word.startswith("\"")\
|
||||
and not word.endswith("\"") and not tmp:
|
||||
if not word.startswith('"') and not word.endswith('"') and not tmp:
|
||||
word_list.append(word)
|
||||
else:
|
||||
if word.startswith("\""):
|
||||
if word.startswith('"'):
|
||||
tmp += word[1:]
|
||||
elif word.endswith("\""):
|
||||
elif word.endswith('"'):
|
||||
tmp += word[:-1]
|
||||
word_list.append(tmp)
|
||||
tmp = ""
|
||||
@@ -129,48 +122,43 @@ class Filter:
|
||||
await ctx.send(_("Those words weren't in the filter."))
|
||||
|
||||
@_filter.command(name="names")
|
||||
async def filter_names(self, ctx: RedContext):
|
||||
"""
|
||||
Toggles whether or not to check names and nicknames against the filter
|
||||
async def filter_names(self, ctx: commands.Context):
|
||||
"""Toggles whether or not to check names and nicknames against the filter
|
||||
|
||||
This is disabled by default
|
||||
"""
|
||||
guild = ctx.guild
|
||||
current_setting = await self.settings.guild(guild).filter_names()
|
||||
await self.settings.guild(guild).filter_names.set(not current_setting)
|
||||
if current_setting:
|
||||
await ctx.send(
|
||||
_("Names and nicknames will no longer be "
|
||||
"checked against the filter")
|
||||
)
|
||||
await ctx.send(_("Names and nicknames will no longer be checked against the filter."))
|
||||
else:
|
||||
await ctx.send(
|
||||
_("Names and nicknames will now be checked against "
|
||||
"the filter")
|
||||
)
|
||||
await ctx.send(_("Names and nicknames will now be checked against the filter."))
|
||||
|
||||
@_filter.command(name="defaultname")
|
||||
async def filter_default_name(self, ctx: RedContext, name: str):
|
||||
"""
|
||||
Sets the default name to use if filtering names is enabled
|
||||
async def filter_default_name(self, ctx: commands.Context, name: str):
|
||||
"""Sets the default name to use if filtering names is enabled
|
||||
|
||||
Note that this has no effect if filtering names is disabled
|
||||
|
||||
The default name used is John Doe
|
||||
"""
|
||||
guild = ctx.guild
|
||||
await self.settings.guild(guild).filter_default_name.set(name)
|
||||
await ctx.send(_("The name to use on filtered names has been set"))
|
||||
await ctx.send(_("The name to use on filtered names has been set."))
|
||||
|
||||
@_filter.command(name="ban")
|
||||
async def filter_ban(
|
||||
self, ctx: commands.Context, count: int, timeframe: int):
|
||||
"""
|
||||
Sets up an autoban if the specified number of messages are
|
||||
filtered in the specified amount of time (in seconds)
|
||||
async def filter_ban(self, ctx: commands.Context, count: int, timeframe: int):
|
||||
"""Autobans if the specified number of messages are filtered in the timeframe
|
||||
|
||||
The timeframe is represented by seconds.
|
||||
"""
|
||||
if (count <= 0) != (timeframe <= 0):
|
||||
await ctx.send(
|
||||
_("Count and timeframe either both need to be 0 "
|
||||
"or both need to be greater than 0!"
|
||||
)
|
||||
_(
|
||||
"Count and timeframe either both need to be 0 "
|
||||
"or both need to be greater than 0!"
|
||||
)
|
||||
)
|
||||
return
|
||||
elif count == 0 and timeframe == 0:
|
||||
@@ -213,9 +201,7 @@ class Filter:
|
||||
if filter_count > 0 and filter_time > 0:
|
||||
if message.created_at.timestamp() >= next_reset_time:
|
||||
next_reset_time = message.created_at.timestamp() + filter_time
|
||||
await self.settings.member(author).next_reset_time.set(
|
||||
next_reset_time
|
||||
)
|
||||
await self.settings.member(author).next_reset_time.set(next_reset_time)
|
||||
if user_count > 0:
|
||||
user_count = 0
|
||||
await self.settings.member(author).filter_count.set(user_count)
|
||||
@@ -231,17 +217,24 @@ class Filter:
|
||||
if filter_count > 0 and filter_time > 0:
|
||||
user_count += 1
|
||||
await self.settings.member(author).filter_count.set(user_count)
|
||||
if user_count >= filter_count and \
|
||||
message.created_at.timestamp() < next_reset_time:
|
||||
reason = "Autoban (too many filtered messages)"
|
||||
if (
|
||||
user_count >= filter_count
|
||||
and message.created_at.timestamp() < next_reset_time
|
||||
):
|
||||
reason = "Autoban (too many filtered messages.)"
|
||||
try:
|
||||
await server.ban(author, reason=reason)
|
||||
except:
|
||||
pass
|
||||
else:
|
||||
await modlog.create_case(
|
||||
self.bot, server, message.created_at,
|
||||
"filterban", author, server.me, reason
|
||||
self.bot,
|
||||
server,
|
||||
message.created_at,
|
||||
"filterban",
|
||||
author,
|
||||
server.me,
|
||||
reason,
|
||||
)
|
||||
|
||||
async def on_message(self, message: discord.Message):
|
||||
@@ -251,7 +244,7 @@ class Filter:
|
||||
valid_user = isinstance(author, discord.Member) and not author.bot
|
||||
if not valid_user:
|
||||
return
|
||||
|
||||
|
||||
# Bots and mods or superior are ignored from the filter
|
||||
mod_or_superior = await is_mod_or_superior(self.bot, obj=author)
|
||||
if mod_or_superior:
|
||||
@@ -266,7 +259,7 @@ class Filter:
|
||||
valid_user = isinstance(author, discord.Member) and not author.bot
|
||||
if not valid_user:
|
||||
return
|
||||
|
||||
|
||||
# Bots and mods or superior are ignored from the filter
|
||||
mod_or_superior = await is_mod_or_superior(self.bot, obj=author)
|
||||
if mod_or_superior:
|
||||
@@ -323,4 +316,3 @@ class Filter:
|
||||
except:
|
||||
pass
|
||||
break
|
||||
|
||||
|
||||
@@ -1,15 +1,11 @@
|
||||
import subprocess
|
||||
|
||||
TO_TRANSLATE = [
|
||||
'../filter.py'
|
||||
]
|
||||
TO_TRANSLATE = ["../filter.py"]
|
||||
|
||||
|
||||
def regen_messages():
|
||||
subprocess.run(
|
||||
['pygettext', '-n'] + TO_TRANSLATE
|
||||
)
|
||||
subprocess.run(["pygettext", "-n"] + TO_TRANSLATE)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
regen_messages()
|
||||
regen_messages()
|
||||
|
||||
@@ -6,17 +6,17 @@ from urllib.parse import quote_plus
|
||||
|
||||
import aiohttp
|
||||
import discord
|
||||
from redbot.core.i18n import CogI18n
|
||||
from discord.ext import commands
|
||||
from redbot.core import commands
|
||||
from redbot.core.i18n import Translator, cog_i18n
|
||||
|
||||
from redbot.core.utils.chat_formatting import escape, italics, pagify
|
||||
|
||||
_ = CogI18n("General", __file__)
|
||||
_ = Translator("General", __file__)
|
||||
|
||||
|
||||
class RPS(Enum):
|
||||
rock = "\N{MOYAI}"
|
||||
paper = "\N{PAGE FACING UP}"
|
||||
rock = "\N{MOYAI}"
|
||||
paper = "\N{PAGE FACING UP}"
|
||||
scissors = "\N{BLACK SCISSORS}"
|
||||
|
||||
|
||||
@@ -33,26 +33,35 @@ class RPSParser:
|
||||
raise
|
||||
|
||||
|
||||
@cog_i18n(_)
|
||||
class General:
|
||||
"""General commands."""
|
||||
|
||||
def __init__(self):
|
||||
self.stopwatches = {}
|
||||
self.ball = [
|
||||
_("As I see it, yes"), _("It is certain"), _("It is decidedly so"),
|
||||
_("Most likely"), _("Outlook good"), _("Signs point to yes"),
|
||||
_("Without a doubt"), _("Yes"), _("Yes – definitely"), _("You may rely on it"),
|
||||
_("Reply hazy, try again"), _("Ask again later"),
|
||||
_("Better not tell you now"), _("Cannot predict now"),
|
||||
_("Concentrate and ask again"), _("Don't count on it"), _("My reply is no"),
|
||||
_("My sources say no"), _("Outlook not so good"), _("Very doubtful")
|
||||
_("As I see it, yes"),
|
||||
_("It is certain"),
|
||||
_("It is decidedly so"),
|
||||
_("Most likely"),
|
||||
_("Outlook good"),
|
||||
_("Signs point to yes"),
|
||||
_("Without a doubt"),
|
||||
_("Yes"),
|
||||
_("Yes – definitely"),
|
||||
_("You may rely on it"),
|
||||
_("Reply hazy, try again"),
|
||||
_("Ask again later"),
|
||||
_("Better not tell you now"),
|
||||
_("Cannot predict now"),
|
||||
_("Concentrate and ask again"),
|
||||
_("Don't count on it"),
|
||||
_("My reply is no"),
|
||||
_("My sources say no"),
|
||||
_("Outlook not so good"),
|
||||
_("Very doubtful"),
|
||||
]
|
||||
|
||||
@commands.command(hidden=True)
|
||||
async def ping(self, ctx):
|
||||
"""Pong."""
|
||||
await ctx.send("Pong.")
|
||||
|
||||
@commands.command()
|
||||
async def choose(self, ctx, *choices):
|
||||
"""Chooses between multiple choices.
|
||||
@@ -61,12 +70,12 @@ class General:
|
||||
"""
|
||||
choices = [escape(c, mass_mentions=True) for c in choices]
|
||||
if len(choices) < 2:
|
||||
await ctx.send(_('Not enough choices to pick from.'))
|
||||
await ctx.send(_("Not enough choices to pick from."))
|
||||
else:
|
||||
await ctx.send(choice(choices))
|
||||
|
||||
@commands.command()
|
||||
async def roll(self, ctx, number : int = 100):
|
||||
async def roll(self, ctx, number: int = 100):
|
||||
"""Rolls random number (between 1 and user choice)
|
||||
|
||||
Defaults to 100.
|
||||
@@ -74,14 +83,12 @@ class General:
|
||||
author = ctx.author
|
||||
if number > 1:
|
||||
n = randint(1, number)
|
||||
await ctx.send(
|
||||
_("{} :game_die: {} :game_die:").format(author.mention, n)
|
||||
)
|
||||
await ctx.send(_("{} :game_die: {} :game_die:").format(author.mention, n))
|
||||
else:
|
||||
await ctx.send(_("{} Maybe higher than 1? ;P").format(author.mention))
|
||||
|
||||
@commands.command()
|
||||
async def flip(self, ctx, user: discord.Member=None):
|
||||
async def flip(self, ctx, user: discord.Member = None):
|
||||
"""Flips a coin... or a user.
|
||||
|
||||
Defaults to coin.
|
||||
@@ -90,8 +97,7 @@ class General:
|
||||
msg = ""
|
||||
if user.id == ctx.bot.user.id:
|
||||
user = ctx.author
|
||||
msg = _("Nice try. You think this is funny?\n"
|
||||
"How about *this* instead:\n\n")
|
||||
msg = _("Nice try. You think this is funny?\n How about *this* instead:\n\n")
|
||||
char = "abcdefghijklmnopqrstuvwxyz"
|
||||
tran = "ɐqɔpǝɟƃɥᴉɾʞlɯuodbɹsʇnʌʍxʎz"
|
||||
table = str.maketrans(char, tran)
|
||||
@@ -102,45 +108,37 @@ class General:
|
||||
name = name.translate(table)
|
||||
await ctx.send(msg + "(╯°□°)╯︵ " + name[::-1])
|
||||
else:
|
||||
await ctx.send(
|
||||
_("*flips a coin and... ") + choice([_("HEADS!*"), _("TAILS!*")])
|
||||
)
|
||||
await ctx.send(_("*flips a coin and... ") + choice([_("HEADS!*"), _("TAILS!*")]))
|
||||
|
||||
@commands.command()
|
||||
async def rps(self, ctx, your_choice : RPSParser):
|
||||
async def rps(self, ctx, your_choice: RPSParser):
|
||||
"""Play rock paper scissors"""
|
||||
author = ctx.author
|
||||
player_choice = your_choice.choice
|
||||
red_choice = choice((RPS.rock, RPS.paper, RPS.scissors))
|
||||
cond = {
|
||||
(RPS.rock, RPS.paper) : False,
|
||||
(RPS.rock, RPS.scissors) : True,
|
||||
(RPS.paper, RPS.rock) : True,
|
||||
(RPS.paper, RPS.scissors) : False,
|
||||
(RPS.scissors, RPS.rock) : False,
|
||||
(RPS.scissors, RPS.paper) : True
|
||||
}
|
||||
(RPS.rock, RPS.paper): False,
|
||||
(RPS.rock, RPS.scissors): True,
|
||||
(RPS.paper, RPS.rock): True,
|
||||
(RPS.paper, RPS.scissors): False,
|
||||
(RPS.scissors, RPS.rock): False,
|
||||
(RPS.scissors, RPS.paper): True,
|
||||
}
|
||||
|
||||
if red_choice == player_choice:
|
||||
outcome = None # Tie
|
||||
outcome = None # Tie
|
||||
else:
|
||||
outcome = cond[(player_choice, red_choice)]
|
||||
|
||||
if outcome is True:
|
||||
await ctx.send(_("{} You win {}!").format(
|
||||
red_choice.value, author.mention
|
||||
))
|
||||
await ctx.send(_("{} You win {}!").format(red_choice.value, author.mention))
|
||||
elif outcome is False:
|
||||
await ctx.send(_("{} You lose {}!").format(
|
||||
red_choice.value, author.mention
|
||||
))
|
||||
await ctx.send(_("{} You lose {}!").format(red_choice.value, author.mention))
|
||||
else:
|
||||
await ctx.send(_("{} We're square {}!").format(
|
||||
red_choice.value, author.mention
|
||||
))
|
||||
await ctx.send(_("{} We're square {}!").format(red_choice.value, author.mention))
|
||||
|
||||
@commands.command(name="8", aliases=["8ball"])
|
||||
async def _8ball(self, ctx, *, question : str):
|
||||
async def _8ball(self, ctx, *, question: str):
|
||||
"""Ask 8 ball a question
|
||||
|
||||
Question must end with a question mark.
|
||||
@@ -164,14 +162,14 @@ class General:
|
||||
self.stopwatches.pop(author.id, None)
|
||||
|
||||
@commands.command()
|
||||
async def lmgtfy(self, ctx, *, search_terms : str):
|
||||
async def lmgtfy(self, ctx, *, search_terms: str):
|
||||
"""Creates a lmgtfy link"""
|
||||
search_terms = escape(search_terms.replace(" ", "+"), mass_mentions=True)
|
||||
await ctx.send("https://lmgtfy.com/?q={}".format(search_terms))
|
||||
|
||||
@commands.command(hidden=True)
|
||||
@commands.guild_only()
|
||||
async def hug(self, ctx, user : discord.Member, intensity : int=1):
|
||||
async def hug(self, ctx, user: discord.Member, intensity: int = 1):
|
||||
"""Because everyone likes hugs
|
||||
|
||||
Up to 10 intensity levels."""
|
||||
@@ -188,97 +186,24 @@ class General:
|
||||
msg = "(づ ̄ ³ ̄)づ{} ⊂(´・ω・`⊂)".format(name)
|
||||
await ctx.send(msg)
|
||||
|
||||
@commands.command()
|
||||
@commands.guild_only()
|
||||
async def userinfo(self, ctx, *, user: discord.Member=None):
|
||||
"""Shows users's informations"""
|
||||
author = ctx.author
|
||||
guild = ctx.guild
|
||||
|
||||
if not user:
|
||||
user = author
|
||||
|
||||
# A special case for a special someone :^)
|
||||
special_date = datetime.datetime(2016, 1, 10, 6, 8, 4, 443000)
|
||||
is_special = (user.id == 96130341705637888 and
|
||||
guild.id == 133049272517001216)
|
||||
|
||||
roles = sorted(user.roles)[1:]
|
||||
|
||||
joined_at = user.joined_at if not is_special else special_date
|
||||
since_created = (ctx.message.created_at - user.created_at).days
|
||||
since_joined = (ctx.message.created_at - joined_at).days
|
||||
user_joined = joined_at.strftime("%d %b %Y %H:%M")
|
||||
user_created = user.created_at.strftime("%d %b %Y %H:%M")
|
||||
member_number = sorted(guild.members,
|
||||
key=lambda m: m.joined_at).index(user) + 1
|
||||
|
||||
created_on = _("{}\n({} days ago)").format(user_created, since_created)
|
||||
joined_on = _("{}\n({} days ago)").format(user_joined, since_joined)
|
||||
|
||||
activity = _("Chilling in {} status").format(user.status)
|
||||
if user.activity is None: # Default status
|
||||
pass
|
||||
elif user.activity.type == discord.ActivityType.playing:
|
||||
activity = _("Playing {}").format(user.activity.name)
|
||||
elif user.activity.type == discord.ActivityType.streaming:
|
||||
activity = _("Streaming [{}]({})").format(user.activity.name, user.activity.url)
|
||||
elif user.activity.type == discord.ActivityType.listening:
|
||||
activity = _("Listening to {}").format(user.activity.name)
|
||||
elif user.activity.type == discord.ActivityType.watching:
|
||||
activity = _("Watching {}").format(user.activity.name)
|
||||
|
||||
if roles:
|
||||
roles = ", ".join([x.name for x in roles])
|
||||
else:
|
||||
roles = _("None")
|
||||
|
||||
data = discord.Embed(description=activity, colour=user.colour)
|
||||
data.add_field(name=_("Joined Discord on"), value=created_on)
|
||||
data.add_field(name=_("Joined this server on"), value=joined_on)
|
||||
data.add_field(name=_("Roles"), value=roles, inline=False)
|
||||
data.set_footer(text=_("Member #{} | User ID: {}"
|
||||
"").format(member_number, user.id))
|
||||
|
||||
name = str(user)
|
||||
name = " ~ ".join((name, user.nick)) if user.nick else name
|
||||
|
||||
if user.avatar:
|
||||
avatar = user.avatar_url
|
||||
avatar = avatar.replace('webp', 'png')
|
||||
data.set_author(name=name, url=avatar)
|
||||
data.set_thumbnail(url=avatar)
|
||||
else:
|
||||
data.set_author(name=name)
|
||||
|
||||
try:
|
||||
await ctx.send(embed=data)
|
||||
except discord.HTTPException:
|
||||
await ctx.send(_("I need the `Embed links` permission "
|
||||
"to send this."))
|
||||
|
||||
@commands.command()
|
||||
@commands.guild_only()
|
||||
async def serverinfo(self, ctx):
|
||||
"""Shows server's informations"""
|
||||
guild = ctx.guild
|
||||
online = len([m.status for m in guild.members
|
||||
if m.status == discord.Status.online or
|
||||
m.status == discord.Status.idle])
|
||||
online = len([m.status for m in guild.members if m.status != discord.Status.offline])
|
||||
total_users = len(guild.members)
|
||||
text_channels = len(guild.text_channels)
|
||||
voice_channels = len(guild.voice_channels)
|
||||
passed = (ctx.message.created_at - guild.created_at).days
|
||||
created_at = (_("Since {}. That's over {} days ago!"
|
||||
"").format(guild.created_at.strftime("%d %b %Y %H:%M"),
|
||||
passed))
|
||||
created_at = _("Since {}. That's over {} days ago!").format(
|
||||
guild.created_at.strftime("%d %b %Y %H:%M"), passed
|
||||
)
|
||||
|
||||
colour = ''.join([choice('0123456789ABCDEF') for x in range(6)])
|
||||
colour = "".join([choice("0123456789ABCDEF") for x in range(6)])
|
||||
colour = randint(0, 0xFFFFFF)
|
||||
|
||||
data = discord.Embed(
|
||||
description=created_at,
|
||||
colour=discord.Colour(value=colour))
|
||||
data = discord.Embed(description=created_at, colour=discord.Colour(value=colour))
|
||||
data.add_field(name=_("Region"), value=str(guild.region))
|
||||
data.add_field(name=_("Users"), value="{}/{}".format(online, total_users))
|
||||
data.add_field(name=_("Text Channels"), value=text_channels)
|
||||
@@ -296,16 +221,16 @@ class General:
|
||||
try:
|
||||
await ctx.send(embed=data)
|
||||
except discord.HTTPException:
|
||||
await ctx.send(_("I need the `Embed links` permission "
|
||||
"to send this."))
|
||||
await ctx.send(_("I need the `Embed links` permission to send this."))
|
||||
|
||||
@commands.command()
|
||||
async def urban(self, ctx, *, search_terms: str, definition_number: int=1):
|
||||
async def urban(self, ctx, *, search_terms: str, definition_number: int = 1):
|
||||
"""Urban Dictionary search
|
||||
|
||||
Definition number must be between 1 and 10"""
|
||||
|
||||
def encode(s):
|
||||
return quote_plus(s, encoding='utf-8', errors='replace')
|
||||
return quote_plus(s, encoding="utf-8", errors="replace")
|
||||
|
||||
# definition_number is just there to show up in the help
|
||||
# all this mess is to avoid forcing double quotes on the user
|
||||
@@ -317,8 +242,8 @@ class General:
|
||||
search_terms = search_terms[:-1]
|
||||
else:
|
||||
pos = 0
|
||||
if pos not in range(0, 11): # API only provides the
|
||||
pos = 0 # top 10 definitions
|
||||
if pos not in range(0, 11): # API only provides the
|
||||
pos = 0 # top 10 definitions
|
||||
except ValueError:
|
||||
pos = 0
|
||||
|
||||
@@ -330,18 +255,18 @@ class General:
|
||||
result = await r.json()
|
||||
item_list = result["list"]
|
||||
if item_list:
|
||||
definition = item_list[pos]['definition']
|
||||
example = item_list[pos]['example']
|
||||
definition = item_list[pos]["definition"]
|
||||
example = item_list[pos]["example"]
|
||||
defs = len(item_list)
|
||||
msg = ("**Definition #{} out of {}:\n**{}\n\n"
|
||||
"**Example:\n**{}".format(pos+1, defs, definition,
|
||||
example))
|
||||
msg = "**Definition #{} out of {}:\n**{}\n\n**Example:\n**{}".format(
|
||||
pos + 1, defs, definition, example
|
||||
)
|
||||
msg = pagify(msg, ["\n"])
|
||||
for page in msg:
|
||||
await ctx.send(page)
|
||||
else:
|
||||
await ctx.send(_("Your search terms gave no results."))
|
||||
except IndexError:
|
||||
await ctx.send(_("There is no definition #{}").format(pos+1))
|
||||
await ctx.send(_("There is no definition #{}").format(pos + 1))
|
||||
except:
|
||||
await ctx.send(_("Error."))
|
||||
|
||||
@@ -1,15 +1,11 @@
|
||||
import subprocess
|
||||
|
||||
TO_TRANSLATE = [
|
||||
'../general.py'
|
||||
]
|
||||
TO_TRANSLATE = ["../general.py"]
|
||||
|
||||
|
||||
def regen_messages():
|
||||
subprocess.run(
|
||||
['pygettext', '-n'] + TO_TRANSLATE
|
||||
)
|
||||
subprocess.run(["pygettext", "-n"] + TO_TRANSLATE)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
regen_messages()
|
||||
regen_messages()
|
||||
|
||||
@@ -1,21 +1,20 @@
|
||||
from random import shuffle
|
||||
|
||||
import aiohttp
|
||||
from discord.ext import commands
|
||||
|
||||
from redbot.core.i18n import CogI18n
|
||||
from redbot.core import checks, Config
|
||||
from redbot.core.i18n import Translator, cog_i18n
|
||||
from redbot.core import checks, Config, commands
|
||||
|
||||
_ = CogI18n("Image", __file__)
|
||||
_ = Translator("Image", __file__)
|
||||
|
||||
GIPHY_API_KEY = "dc6zaTOxFJmzC"
|
||||
|
||||
|
||||
@cog_i18n(_)
|
||||
class Image:
|
||||
"""Image related commands."""
|
||||
default_global = {
|
||||
"imgur_client_id": None
|
||||
}
|
||||
|
||||
default_global = {"imgur_client_id": None}
|
||||
|
||||
def __init__(self, bot):
|
||||
self.bot = bot
|
||||
@@ -33,8 +32,7 @@ class Image:
|
||||
|
||||
Make sure to set the client ID using
|
||||
[p]imgurcreds"""
|
||||
if ctx.invoked_subcommand is None:
|
||||
await ctx.send_help()
|
||||
pass
|
||||
|
||||
@_imgur.command(name="search")
|
||||
async def imgur_search(self, ctx, *, term: str):
|
||||
@@ -44,8 +42,10 @@ class Image:
|
||||
imgur_client_id = await self.settings.imgur_client_id()
|
||||
if not imgur_client_id:
|
||||
await ctx.send(
|
||||
_("A client ID has not been set! Please set one with {}").format(
|
||||
"`{}imgurcreds`".format(ctx.prefix)))
|
||||
_("A client ID has not been set! Please set one with {}.").format(
|
||||
"`{}imgurcreds`".format(ctx.prefix)
|
||||
)
|
||||
)
|
||||
return
|
||||
headers = {"Authorization": "Client-ID {}".format(imgur_client_id)}
|
||||
async with self.session.get(url, headers=headers, params=params) as search_get:
|
||||
@@ -54,7 +54,7 @@ class Image:
|
||||
if data["success"]:
|
||||
results = data["data"]
|
||||
if not results:
|
||||
await ctx.send(_("Your search returned no results"))
|
||||
await ctx.send(_("Your search returned no results."))
|
||||
return
|
||||
shuffle(results)
|
||||
msg = _("Search results...\n")
|
||||
@@ -63,10 +63,12 @@ class Image:
|
||||
msg += "\n"
|
||||
await ctx.send(msg)
|
||||
else:
|
||||
await ctx.send(_("Something went wrong. Error code is {}").format(data["status"]))
|
||||
await ctx.send(_("Something went wrong. Error code is {}.").format(data["status"]))
|
||||
|
||||
@_imgur.command(name="subreddit")
|
||||
async def imgur_subreddit(self, ctx, subreddit: str, sort_type: str="top", window: str="day"):
|
||||
async def imgur_subreddit(
|
||||
self, ctx, subreddit: str, sort_type: str = "top", window: str = "day"
|
||||
):
|
||||
"""Gets images from the specified subreddit section
|
||||
|
||||
Sort types: new, top
|
||||
@@ -89,8 +91,10 @@ class Image:
|
||||
imgur_client_id = await self.settings.imgur_client_id()
|
||||
if not imgur_client_id:
|
||||
await ctx.send(
|
||||
_("A client ID has not been set! Please set one with {}").format(
|
||||
"`{}imgurcreds`".format(ctx.prefix)))
|
||||
_("A client ID has not been set! Please set one with {}.").format(
|
||||
"`{}imgurcreds`".format(ctx.prefix)
|
||||
)
|
||||
)
|
||||
return
|
||||
|
||||
links = []
|
||||
@@ -112,12 +116,13 @@ class Image:
|
||||
else:
|
||||
await ctx.send(_("No results found."))
|
||||
else:
|
||||
await ctx.send(_("Something went wrong. Error code is {}").format(data["status"]))
|
||||
await ctx.send(_("Something went wrong. Error code is {}.").format(data["status"]))
|
||||
|
||||
@checks.is_owner()
|
||||
@commands.command()
|
||||
async def imgurcreds(self, ctx, imgur_client_id: str):
|
||||
"""Sets the imgur client id
|
||||
|
||||
You will need an account on Imgur to get this
|
||||
|
||||
You can get these by visiting https://api.imgur.com/oauth2/addclient
|
||||
@@ -126,7 +131,7 @@ class Image:
|
||||
set the authorization callback url to 'https://localhost'
|
||||
leave the app website blank, enter a valid email address, and
|
||||
enter a description. Check the box for the captcha, then click Next.
|
||||
Your client ID will be on the page that loads"""
|
||||
Your client ID will be on the page that loads."""
|
||||
await self.settings.imgur_client_id.set(imgur_client_id)
|
||||
await ctx.send(_("Set the imgur client id!"))
|
||||
|
||||
@@ -139,8 +144,9 @@ class Image:
|
||||
await ctx.send_help()
|
||||
return
|
||||
|
||||
url = ("http://api.giphy.com/v1/gifs/search?&api_key={}&q={}"
|
||||
"".format(GIPHY_API_KEY, keywords))
|
||||
url = "http://api.giphy.com/v1/gifs/search?&api_key={}&q={}".format(
|
||||
GIPHY_API_KEY, keywords
|
||||
)
|
||||
|
||||
async with self.session.get(url) as r:
|
||||
result = await r.json()
|
||||
@@ -150,7 +156,7 @@ class Image:
|
||||
else:
|
||||
await ctx.send(_("No results found."))
|
||||
else:
|
||||
await ctx.send(_("Error contacting the API"))
|
||||
await ctx.send(_("Error contacting the API."))
|
||||
|
||||
@commands.command(pass_context=True, no_pm=True)
|
||||
async def gifr(self, ctx, *keywords):
|
||||
@@ -161,8 +167,9 @@ class Image:
|
||||
await ctx.send_help()
|
||||
return
|
||||
|
||||
url = ("http://api.giphy.com/v1/gifs/random?&api_key={}&tag={}"
|
||||
"".format(GIPHY_API_KEY, keywords))
|
||||
url = "http://api.giphy.com/v1/gifs/random?&api_key={}&tag={}".format(
|
||||
GIPHY_API_KEY, keywords
|
||||
)
|
||||
|
||||
async with self.session.get(url) as r:
|
||||
result = await r.json()
|
||||
@@ -172,4 +179,4 @@ class Image:
|
||||
else:
|
||||
await ctx.send(_("No results found."))
|
||||
else:
|
||||
await ctx.send(_("Error contacting the API"))
|
||||
await ctx.send(_("Error contacting the API."))
|
||||
|
||||
@@ -1,15 +1,11 @@
|
||||
import subprocess
|
||||
|
||||
TO_TRANSLATE = [
|
||||
'../image.py'
|
||||
]
|
||||
TO_TRANSLATE = ["../image.py"]
|
||||
|
||||
|
||||
def regen_messages():
|
||||
subprocess.run(
|
||||
['pygettext', '-n'] + TO_TRANSLATE
|
||||
)
|
||||
subprocess.run(["pygettext", "-n"] + TO_TRANSLATE)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
regen_messages()
|
||||
regen_messages()
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
from discord.ext import commands
|
||||
from redbot.core import commands
|
||||
import discord
|
||||
|
||||
|
||||
@@ -18,11 +18,14 @@ def mod_or_voice_permissions(**perms):
|
||||
|
||||
for vc in guild.voice_channels:
|
||||
resolved = vc.permissions_for(author)
|
||||
good = all(getattr(resolved, name, None) == value for name, value in perms.items())
|
||||
good = resolved.administrator or all(
|
||||
getattr(resolved, name, None) == value for name, value in perms.items()
|
||||
)
|
||||
if not good:
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
|
||||
return commands.check(pred)
|
||||
|
||||
|
||||
@@ -37,11 +40,14 @@ def admin_or_voice_permissions(**perms):
|
||||
return True
|
||||
for vc in guild.voice_channels:
|
||||
resolved = vc.permissions_for(author)
|
||||
good = all(getattr(resolved, name, None) == value for name, value in perms.items())
|
||||
good = resolved.administrator or all(
|
||||
getattr(resolved, name, None) == value for name, value in perms.items()
|
||||
)
|
||||
if not good:
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
|
||||
return commands.check(pred)
|
||||
|
||||
|
||||
@@ -50,9 +56,12 @@ def bot_has_voice_permissions(**perms):
|
||||
guild = ctx.guild
|
||||
for vc in guild.voice_channels:
|
||||
resolved = vc.permissions_for(guild.me)
|
||||
good = all(getattr(resolved, name, None) == value for name, value in perms.items())
|
||||
good = resolved.administrator or all(
|
||||
getattr(resolved, name, None) == value for name, value in perms.items()
|
||||
)
|
||||
if not good:
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
|
||||
return commands.check(pred)
|
||||
|
||||
@@ -1,15 +1,11 @@
|
||||
import subprocess
|
||||
|
||||
TO_TRANSLATE = [
|
||||
'../mod.py'
|
||||
]
|
||||
TO_TRANSLATE = ["../mod.py"]
|
||||
|
||||
|
||||
def regen_messages():
|
||||
subprocess.run(
|
||||
['pygettext', '-n'] + TO_TRANSLATE
|
||||
)
|
||||
subprocess.run(["pygettext", "-n"] + TO_TRANSLATE)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
regen_messages()
|
||||
regen_messages()
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,15 +1,11 @@
|
||||
import subprocess
|
||||
|
||||
TO_TRANSLATE = [
|
||||
'../modlog.py'
|
||||
]
|
||||
TO_TRANSLATE = ["../modlog.py"]
|
||||
|
||||
|
||||
def regen_messages():
|
||||
subprocess.run(
|
||||
['pygettext', '-n'] + TO_TRANSLATE
|
||||
)
|
||||
subprocess.run(["pygettext", "-n"] + TO_TRANSLATE)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
regen_messages()
|
||||
regen_messages()
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import discord
|
||||
from discord.ext import commands
|
||||
|
||||
from redbot.core import checks, modlog, RedContext
|
||||
from redbot.core import checks, modlog, commands
|
||||
from redbot.core.bot import Red
|
||||
from redbot.core.i18n import CogI18n
|
||||
from redbot.core.i18n import Translator, cog_i18n
|
||||
from redbot.core.utils.chat_formatting import box
|
||||
|
||||
_ = CogI18n('ModLog', __file__)
|
||||
_ = Translator("ModLog", __file__)
|
||||
|
||||
|
||||
@cog_i18n(_)
|
||||
class ModLog:
|
||||
"""Log for mod actions"""
|
||||
|
||||
@@ -17,14 +17,13 @@ class ModLog:
|
||||
|
||||
@commands.group()
|
||||
@checks.guildowner_or_permissions(administrator=True)
|
||||
async def modlogset(self, ctx: RedContext):
|
||||
async def modlogset(self, ctx: commands.Context):
|
||||
"""Settings for the mod log"""
|
||||
if ctx.invoked_subcommand is None:
|
||||
await ctx.send_help()
|
||||
pass
|
||||
|
||||
@modlogset.command()
|
||||
@commands.guild_only()
|
||||
async def modlog(self, ctx: RedContext, channel: discord.TextChannel = None):
|
||||
async def modlog(self, ctx: commands.Context, channel: discord.TextChannel = None):
|
||||
"""Sets a channel as mod log
|
||||
|
||||
Leaving the channel parameter empty will deactivate it"""
|
||||
@@ -32,15 +31,10 @@ class ModLog:
|
||||
if channel:
|
||||
if channel.permissions_for(guild.me).send_messages:
|
||||
await modlog.set_modlog_channel(guild, channel)
|
||||
await ctx.send(
|
||||
_("Mod events will be sent to {}").format(
|
||||
channel.mention
|
||||
)
|
||||
)
|
||||
await ctx.send(_("Mod events will be sent to {}").format(channel.mention))
|
||||
else:
|
||||
await ctx.send(
|
||||
_("I do not have permissions to "
|
||||
"send messages in {}!").format(channel.mention)
|
||||
_("I do not have permissions to send messages in {}!").format(channel.mention)
|
||||
)
|
||||
else:
|
||||
try:
|
||||
@@ -51,9 +45,9 @@ class ModLog:
|
||||
await modlog.set_modlog_channel(guild, None)
|
||||
await ctx.send(_("Mod log deactivated."))
|
||||
|
||||
@modlogset.command(name='cases')
|
||||
@modlogset.command(name="cases")
|
||||
@commands.guild_only()
|
||||
async def set_cases(self, ctx: RedContext, action: str = None):
|
||||
async def set_cases(self, ctx: commands.Context, action: str = None):
|
||||
"""Enables or disables case creation for each type of mod action"""
|
||||
guild = ctx.guild
|
||||
|
||||
@@ -64,8 +58,8 @@ class ModLog:
|
||||
msg = ""
|
||||
for ct in casetypes:
|
||||
enabled = await ct.is_enabled()
|
||||
value = 'enabled' if enabled else 'disabled'
|
||||
msg += '%s : %s\n' % (ct.name, value)
|
||||
value = "enabled" if enabled else "disabled"
|
||||
msg += "%s : %s\n" % (ct.name, value)
|
||||
|
||||
msg = title + "\n" + box(msg)
|
||||
await ctx.send(msg)
|
||||
@@ -78,16 +72,14 @@ class ModLog:
|
||||
enabled = await casetype.is_enabled()
|
||||
await casetype.set_enabled(True if not enabled else False)
|
||||
|
||||
msg = (
|
||||
_('Case creation for {} actions is now {}.').format(
|
||||
action, 'enabled' if not enabled else 'disabled'
|
||||
)
|
||||
msg = _("Case creation for {} actions is now {}.").format(
|
||||
action, "enabled" if not enabled else "disabled"
|
||||
)
|
||||
await ctx.send(msg)
|
||||
|
||||
@modlogset.command()
|
||||
@commands.guild_only()
|
||||
async def resetcases(self, ctx: RedContext):
|
||||
async def resetcases(self, ctx: commands.Context):
|
||||
"""Resets modlog's cases"""
|
||||
guild = ctx.guild
|
||||
await modlog.reset_cases(guild)
|
||||
@@ -95,7 +87,7 @@ class ModLog:
|
||||
|
||||
@commands.command()
|
||||
@commands.guild_only()
|
||||
async def case(self, ctx: RedContext, number: int):
|
||||
async def case(self, ctx: commands.Context, number: int):
|
||||
"""Shows the specified case"""
|
||||
try:
|
||||
case = await modlog.get_case(number, ctx.guild, self.bot)
|
||||
@@ -107,7 +99,7 @@ class ModLog:
|
||||
|
||||
@commands.command()
|
||||
@commands.guild_only()
|
||||
async def reason(self, ctx: RedContext, case: int, *, reason: str = ""):
|
||||
async def reason(self, ctx: commands.Context, case: int, *, reason: str = ""):
|
||||
"""Lets you specify a reason for mod-log's cases
|
||||
Please note that you can only edit cases you are
|
||||
the owner of unless you are a mod/admin or the server owner"""
|
||||
@@ -133,8 +125,10 @@ class ModLog:
|
||||
if audit_type:
|
||||
audit_case = None
|
||||
async for entry in guild.audit_logs(action=audit_type):
|
||||
if entry.target.id == case_before.user.id and \
|
||||
entry.user.id == case_before.moderator.id:
|
||||
if (
|
||||
entry.target.id == case_before.user.id
|
||||
and entry.action == audit_type
|
||||
):
|
||||
audit_case = entry
|
||||
break
|
||||
if audit_case:
|
||||
@@ -145,11 +139,9 @@ class ModLog:
|
||||
if not (is_guild_owner or is_case_author or author_is_mod):
|
||||
await ctx.send(_("You are not authorized to modify that case!"))
|
||||
return
|
||||
to_modify = {
|
||||
"reason": reason,
|
||||
}
|
||||
to_modify = {"reason": reason}
|
||||
if case_before.moderator != author:
|
||||
to_modify["amended_by"] = author
|
||||
to_modify["modified_at"] = ctx.message.created_at.timestamp()
|
||||
await case_before.edit(self.bot, to_modify)
|
||||
await case_before.edit(to_modify)
|
||||
await ctx.send(_("Reason has been updated."))
|
||||
|
||||
5
redbot/cogs/permissions/__init__.py
Normal file
5
redbot/cogs/permissions/__init__.py
Normal file
@@ -0,0 +1,5 @@
|
||||
from .permissions import Permissions
|
||||
|
||||
|
||||
def setup(bot):
|
||||
bot.add_cog(Permissions(bot))
|
||||
44
redbot/cogs/permissions/converters.py
Normal file
44
redbot/cogs/permissions/converters.py
Normal file
@@ -0,0 +1,44 @@
|
||||
from redbot.core import commands
|
||||
from typing import Tuple
|
||||
|
||||
|
||||
class CogOrCommand(commands.Converter):
|
||||
async def convert(self, ctx: commands.Context, arg: str) -> Tuple[str]:
|
||||
ret = ctx.bot.get_cog(arg)
|
||||
if ret:
|
||||
return "cogs", ret.__class__.__name__
|
||||
ret = ctx.bot.get_command(arg)
|
||||
if ret:
|
||||
return "commands", ret.qualified_name
|
||||
|
||||
raise commands.BadArgument(
|
||||
'Cog or command "{arg}" not found. Please note that this is case sensitive.'
|
||||
"".format(arg=arg)
|
||||
)
|
||||
|
||||
|
||||
class RuleType(commands.Converter):
|
||||
async def convert(self, ctx: commands.Context, arg: str) -> str:
|
||||
if arg.lower() in ("allow", "whitelist", "allowed"):
|
||||
return "allow"
|
||||
if arg.lower() in ("deny", "blacklist", "denied"):
|
||||
return "deny"
|
||||
|
||||
raise commands.BadArgument(
|
||||
'"{arg}" is not a valid rule. Valid rules are "allow" or "deny"'.format(arg=arg)
|
||||
)
|
||||
|
||||
|
||||
class ClearableRuleType(commands.Converter):
|
||||
async def convert(self, ctx: commands.Context, arg: str) -> str:
|
||||
if arg.lower() in ("allow", "whitelist", "allowed"):
|
||||
return "allow"
|
||||
if arg.lower() in ("deny", "blacklist", "denied"):
|
||||
return "deny"
|
||||
if arg.lower() in ("clear", "reset"):
|
||||
return "clear"
|
||||
|
||||
raise commands.BadArgument(
|
||||
'"{arg}" is not a valid rule. Valid rules are "allow" or "deny", or "clear" to remove the rule'
|
||||
"".format(arg=arg)
|
||||
)
|
||||
102
redbot/cogs/permissions/mass_resolution.py
Normal file
102
redbot/cogs/permissions/mass_resolution.py
Normal file
@@ -0,0 +1,102 @@
|
||||
from redbot.core import commands
|
||||
from redbot.core.config import Config
|
||||
from .resolvers import entries_from_ctx, resolve_lists
|
||||
|
||||
# This has optimizations in it that may not hold True if other parts of the permission
|
||||
# model are changed from the state they are in currently.
|
||||
# (commit hash ~ 3bcf375204c22271ad3ed1fc059b598b751aa03f)
|
||||
#
|
||||
# This is primarily to help with the performance of the help formatter
|
||||
|
||||
# This is less efficient if only checking one command,
|
||||
# but is much faster for checking all of them.
|
||||
|
||||
|
||||
async def mass_resolve(*, ctx: commands.Context, config: Config):
|
||||
"""
|
||||
Get's all the permission cog interactions for all loaded commands
|
||||
in the given context.
|
||||
"""
|
||||
|
||||
owner_settings = await config.owner_models()
|
||||
guild_owner_settings = await config.guild(ctx.guild).owner_models() if ctx.guild else None
|
||||
|
||||
ret = {"allowed": [], "denied": [], "default": []}
|
||||
|
||||
for cogname, cog in ctx.bot.cogs.items():
|
||||
|
||||
cog_setting = resolve_cog_or_command(
|
||||
objname=cogname, models=owner_settings, ctx=ctx, typ="cogs"
|
||||
)
|
||||
if cog_setting is None and guild_owner_settings:
|
||||
cog_setting = resolve_cog_or_command(
|
||||
objname=cogname, models=guild_owner_settings, ctx=ctx, typ="cogs"
|
||||
)
|
||||
|
||||
for command in [c for c in ctx.bot.all_commands.values() if c.instance is cog]:
|
||||
resolution = recursively_resolve(
|
||||
com_or_group=command,
|
||||
o_models=owner_settings,
|
||||
g_models=guild_owner_settings,
|
||||
ctx=ctx,
|
||||
)
|
||||
|
||||
for com, resolved in resolution:
|
||||
if resolved is None:
|
||||
resolved = cog_setting
|
||||
if resolved is True:
|
||||
ret["allowed"].append(com)
|
||||
elif resolved is False:
|
||||
ret["denied"].append(com)
|
||||
else:
|
||||
ret["default"].append(com)
|
||||
|
||||
ret = {k: set(v) for k, v in ret.items()}
|
||||
|
||||
return ret
|
||||
|
||||
|
||||
def recursively_resolve(*, com_or_group, o_models, g_models, ctx, override=False):
|
||||
ret = []
|
||||
if override:
|
||||
current = False
|
||||
else:
|
||||
current = resolve_cog_or_command(
|
||||
typ="commands", objname=com_or_group.qualified_name, ctx=ctx, models=o_models
|
||||
)
|
||||
if current is None and g_models:
|
||||
current = resolve_cog_or_command(
|
||||
typ="commands", objname=com_or_group.qualified_name, ctx=ctx, models=o_models
|
||||
)
|
||||
ret.append((com_or_group, current))
|
||||
if isinstance(com_or_group, commands.Group):
|
||||
for com in com_or_group.commands:
|
||||
ret.extend(
|
||||
recursively_resolve(
|
||||
com_or_group=com,
|
||||
o_models=o_models,
|
||||
g_models=g_models,
|
||||
ctx=ctx,
|
||||
override=(current is False),
|
||||
)
|
||||
)
|
||||
return ret
|
||||
|
||||
|
||||
def resolve_cog_or_command(*, typ, ctx, objname, models: dict) -> bool:
|
||||
"""
|
||||
Resolves models in order.
|
||||
"""
|
||||
|
||||
resolved = None
|
||||
|
||||
if objname in models.get(typ, {}):
|
||||
blacklist = models[typ][objname].get("deny", [])
|
||||
whitelist = models[typ][objname].get("allow", [])
|
||||
resolved = resolve_lists(ctx=ctx, whitelist=whitelist, blacklist=blacklist)
|
||||
if resolved is not None:
|
||||
return resolved
|
||||
resolved = models[typ][objname].get("default", None)
|
||||
if resolved is not None:
|
||||
return resolved
|
||||
return None
|
||||
669
redbot/cogs/permissions/permissions.py
Normal file
669
redbot/cogs/permissions/permissions.py
Normal file
@@ -0,0 +1,669 @@
|
||||
from copy import copy
|
||||
import contextlib
|
||||
import asyncio
|
||||
import discord
|
||||
from redbot.core import commands
|
||||
from redbot.core.bot import Red
|
||||
from redbot.core import checks
|
||||
from redbot.core.config import Config
|
||||
from redbot.core.i18n import Translator, cog_i18n
|
||||
from redbot.core.utils.caching import LRUDict
|
||||
|
||||
from .resolvers import val_if_check_is_valid, resolve_models, entries_from_ctx
|
||||
from .yaml_handler import yamlset_acl, yamlget_acl
|
||||
from .converters import CogOrCommand, RuleType, ClearableRuleType
|
||||
from .mass_resolution import mass_resolve
|
||||
|
||||
_models = ["owner", "guildowner", "admin", "mod", "all"]
|
||||
|
||||
_ = Translator("Permissions", __file__)
|
||||
|
||||
REACTS = {"\N{WHITE HEAVY CHECK MARK}": True, "\N{NEGATIVE SQUARED CROSS MARK}": False}
|
||||
Y_OR_N = {"y": True, "yes": True, "n": False, "no": False}
|
||||
|
||||
|
||||
@cog_i18n(_)
|
||||
class Permissions:
|
||||
"""
|
||||
A high level permission model
|
||||
"""
|
||||
|
||||
# Not sure if we will use admin or mod models in core red
|
||||
# but they are explicitly supported
|
||||
resolution_order = {k: _models[:i] for i, k in enumerate(_models, 1)}
|
||||
|
||||
def __init__(self, bot: Red):
|
||||
self.bot = bot
|
||||
self.config = Config.get_conf(self, identifier=78631113035100160, force_registration=True)
|
||||
self.config.register_global(owner_models={})
|
||||
self.config.register_guild(owner_models={})
|
||||
self.cache = LRUDict(size=25000) # This can be tuned later
|
||||
|
||||
async def get_user_ctx_overrides(self, ctx: commands.Context) -> dict:
|
||||
"""
|
||||
This takes a context object, and returns a dict of
|
||||
|
||||
allowed: list of commands
|
||||
denied: list of commands
|
||||
default: list of commands
|
||||
|
||||
representing how permissions interacts with the
|
||||
user, channel, guild, and (possibly) voice channel
|
||||
for all commands on the bot (not just the one in the context object)
|
||||
|
||||
This mainly exists for use by the help formatter,
|
||||
but others may find it useful
|
||||
|
||||
Unlike the rest of the permission system, if other models are added later,
|
||||
due to optimizations made for this, this needs to be adjusted accordingly
|
||||
|
||||
This does not account for before and after permission hooks,
|
||||
these need to be checked seperately
|
||||
"""
|
||||
return await mass_resolve(ctx=ctx, config=self.config)
|
||||
|
||||
async def __global_check(self, ctx: commands.Context) -> bool:
|
||||
"""
|
||||
Yes, this is needed on top of hooking into checks.py
|
||||
to ensure that unchecked commands can still be managed by permissions
|
||||
This should return True in the case of no overrides
|
||||
defering to check logic
|
||||
This works since all checks must be True to run
|
||||
"""
|
||||
v = await self.check_overrides(ctx, "all")
|
||||
|
||||
if v is False:
|
||||
return False
|
||||
return True
|
||||
|
||||
async def check_overrides(self, ctx: commands.Context, level: str) -> bool:
|
||||
"""
|
||||
This checks for any overrides in the permission model
|
||||
|
||||
Parameters
|
||||
----------
|
||||
ctx: `redbot.core.context.commands.Context`
|
||||
The context of the command
|
||||
level: `str`
|
||||
One of 'owner', 'guildowner', 'admin', 'mod', 'all'
|
||||
|
||||
Returns
|
||||
-------
|
||||
bool
|
||||
a trinary value using None + bool to resolve permissions for
|
||||
checks.py
|
||||
"""
|
||||
if await ctx.bot.is_owner(ctx.author):
|
||||
return True
|
||||
|
||||
before = [
|
||||
getattr(cog, "_{0.__class__.__name__}__red_permissions_before".format(cog), None)
|
||||
for cog in ctx.bot.cogs.values()
|
||||
]
|
||||
for check in before:
|
||||
if check is None:
|
||||
continue
|
||||
override = await val_if_check_is_valid(check=check, ctx=ctx, level=level)
|
||||
if override is not None:
|
||||
return override
|
||||
|
||||
# checked ids + configureable to be checked against
|
||||
cache_tup = entries_from_ctx(ctx) + (
|
||||
ctx.cog.__class__.__name__,
|
||||
ctx.command.qualified_name,
|
||||
)
|
||||
if cache_tup in self.cache:
|
||||
override = self.cache[cache_tup]
|
||||
if override is not None:
|
||||
return override
|
||||
else:
|
||||
for model in self.resolution_order[level]:
|
||||
if ctx.guild is None and model != "owner":
|
||||
break
|
||||
override_model = getattr(self, model + "_model", None)
|
||||
override = await override_model(ctx) if override_model else None
|
||||
if override is not None:
|
||||
self.cache[cache_tup] = override
|
||||
return override
|
||||
# This is intentional not being in an else block
|
||||
self.cache[cache_tup] = None
|
||||
|
||||
after = [
|
||||
getattr(cog, "_{0.__class__.__name__}__red_permissions_after".format(cog), None)
|
||||
for cog in ctx.bot.cogs.values()
|
||||
]
|
||||
for check in after:
|
||||
override = await val_if_check_is_valid(check=check, ctx=ctx, level=level)
|
||||
if override is not None:
|
||||
return override
|
||||
|
||||
return None
|
||||
|
||||
async def owner_model(self, ctx: commands.Context) -> bool:
|
||||
"""
|
||||
Handles owner level overrides
|
||||
"""
|
||||
|
||||
async with self.config.owner_models() as models:
|
||||
return resolve_models(ctx=ctx, models=models)
|
||||
|
||||
async def guildowner_model(self, ctx: commands.Context) -> bool:
|
||||
"""
|
||||
Handles guild level overrides
|
||||
"""
|
||||
if ctx.guild is None:
|
||||
return None
|
||||
async with self.config.guild(ctx.guild).owner_models() as models:
|
||||
return resolve_models(ctx=ctx, models=models)
|
||||
|
||||
# Either of the below function signatures could be used
|
||||
# without any other modifications required at a later date
|
||||
#
|
||||
# async def admin_model(self, ctx: commands.Context) -> bool:
|
||||
# async def mod_model(self, ctx: commands.Context) -> bool:
|
||||
|
||||
@commands.group(aliases=["p"])
|
||||
async def permissions(self, ctx: commands.Context):
|
||||
"""
|
||||
Permission management tools
|
||||
"""
|
||||
pass
|
||||
|
||||
@permissions.command()
|
||||
async def explain(self, ctx: commands.Context):
|
||||
"""
|
||||
Provides a detailed explanation of how the permission model functions
|
||||
"""
|
||||
# Apologies in advance for the translators out there...
|
||||
|
||||
message = _(
|
||||
"This cog extends the default permission model of the bot. "
|
||||
"By default, many commands are restricted based on what "
|
||||
"the command can do."
|
||||
"\n"
|
||||
"Any command that could impact the host machine, "
|
||||
"is generally owner only."
|
||||
"\n"
|
||||
"Commands that take administrative or moderator "
|
||||
"actions in servers generally require a mod or an admin."
|
||||
"\n"
|
||||
"This cog allows you to refine some of those settings. "
|
||||
"You can allow wider or narrower "
|
||||
"access to most commands using it."
|
||||
"\n\n"
|
||||
"When additional rules are set using this cog, "
|
||||
"those rules will be checked prior to "
|
||||
"checking for the default restrictions of the command. "
|
||||
"\n"
|
||||
"Rules set globally (by the owner) are checked first, "
|
||||
"then rules set for guilds. If multiple global or guild "
|
||||
"rules apply to the case, the order they are checked is:"
|
||||
"\n"
|
||||
"1. Rules about a user.\n"
|
||||
"2. Rules about the voice channel a user is in.\n"
|
||||
"3. Rules about the text channel a command was issued in.\n"
|
||||
"4. Rules about a role the user has "
|
||||
"(The highest role they have with a rule will be used).\n"
|
||||
"5. Rules about the guild a user is in (Owner level only)."
|
||||
"\n\nFor more details, please read the official documentation."
|
||||
)
|
||||
|
||||
await ctx.maybe_send_embed(message)
|
||||
|
||||
@permissions.command(name="canrun")
|
||||
async def _test_permission_model(
|
||||
self, ctx: commands.Context, user: discord.Member, *, command: str
|
||||
):
|
||||
"""
|
||||
This checks if someone can run a command in the current location
|
||||
"""
|
||||
|
||||
if not command:
|
||||
return await ctx.send_help()
|
||||
|
||||
message = copy(ctx.message)
|
||||
message.author = user
|
||||
message.content = "{}{}".format(ctx.prefix, command)
|
||||
|
||||
com = self.bot.get_command(command)
|
||||
if com is None:
|
||||
out = _("No such command")
|
||||
else:
|
||||
try:
|
||||
testcontext = await self.bot.get_context(message, cls=commands.Context)
|
||||
can = await com.can_run(testcontext) and all(
|
||||
[await p.can_run(testcontext) for p in com.parents]
|
||||
)
|
||||
except commands.CheckFailure:
|
||||
can = False
|
||||
|
||||
out = (
|
||||
_("That user can run the specified command.")
|
||||
if can
|
||||
else _("That user can not run the specified command.")
|
||||
)
|
||||
await ctx.send(out)
|
||||
|
||||
@checks.is_owner()
|
||||
@permissions.command(name="setglobalacl")
|
||||
async def owner_set_acl(self, ctx: commands.Context):
|
||||
"""
|
||||
Take a YAML file upload to set permissions from
|
||||
"""
|
||||
if not ctx.message.attachments:
|
||||
return await ctx.send(_("You must upload a file."))
|
||||
|
||||
try:
|
||||
await yamlset_acl(ctx, config=self.config.owner_models, update=False)
|
||||
except Exception as e:
|
||||
print(e)
|
||||
return await ctx.send(_("Invalid syntax."))
|
||||
else:
|
||||
await ctx.send(_("Rules set."))
|
||||
self.invalidate_cache()
|
||||
|
||||
@checks.is_owner()
|
||||
@permissions.command(name="getglobalacl")
|
||||
async def owner_get_acl(self, ctx: commands.Context):
|
||||
"""
|
||||
Dumps a YAML file with the current owner level permissions
|
||||
"""
|
||||
await yamlget_acl(ctx, config=self.config.owner_models)
|
||||
|
||||
@commands.guild_only()
|
||||
@checks.guildowner_or_permissions(administrator=True)
|
||||
@permissions.command(name="setguildacl")
|
||||
async def guild_set_acl(self, ctx: commands.Context):
|
||||
"""
|
||||
Take a YAML file upload to set permissions from
|
||||
"""
|
||||
if not ctx.message.attachments:
|
||||
return await ctx.send(_("You must upload a file."))
|
||||
|
||||
try:
|
||||
await yamlset_acl(ctx, config=self.config.guild(ctx.guild).owner_models, update=False)
|
||||
except Exception as e:
|
||||
print(e)
|
||||
return await ctx.send(_("Invalid syntax."))
|
||||
else:
|
||||
await ctx.send(_("Rules set."))
|
||||
self.invalidate_cache(ctx.guild.id)
|
||||
|
||||
@commands.guild_only()
|
||||
@checks.guildowner_or_permissions(administrator=True)
|
||||
@permissions.command(name="getguildacl")
|
||||
async def guild_get_acl(self, ctx: commands.Context):
|
||||
"""
|
||||
Dumps a YAML file with the current owner level permissions
|
||||
"""
|
||||
await yamlget_acl(ctx, config=self.config.guild(ctx.guild).owner_models)
|
||||
|
||||
@commands.guild_only()
|
||||
@checks.guildowner_or_permissions(administrator=True)
|
||||
@permissions.command(name="updateguildacl")
|
||||
async def guild_update_acl(self, ctx: commands.Context):
|
||||
"""
|
||||
Take a YAML file upload to update permissions from
|
||||
|
||||
Use this to not lose existing rules
|
||||
"""
|
||||
if not ctx.message.attachments:
|
||||
return await ctx.send(_("You must upload a file."))
|
||||
|
||||
try:
|
||||
await yamlset_acl(ctx, config=self.config.guild(ctx.guild).owner_models, update=True)
|
||||
except Exception as e:
|
||||
print(e)
|
||||
return await ctx.send(_("Invalid syntax."))
|
||||
else:
|
||||
await ctx.send(_("Rules set."))
|
||||
self.invalidate_cache(ctx.guild.id)
|
||||
|
||||
@checks.is_owner()
|
||||
@permissions.command(name="updateglobalacl")
|
||||
async def owner_update_acl(self, ctx: commands.Context):
|
||||
"""
|
||||
Take a YAML file upload to update permissions from
|
||||
|
||||
Use this to not lose existing rules
|
||||
"""
|
||||
if not ctx.message.attachments:
|
||||
return await ctx.send(_("You must upload a file."))
|
||||
|
||||
try:
|
||||
await yamlset_acl(ctx, config=self.config.owner_models, update=True)
|
||||
except Exception as e:
|
||||
print(e)
|
||||
return await ctx.send(_("Invalid syntax."))
|
||||
else:
|
||||
await ctx.send(_("Rules set."))
|
||||
self.invalidate_cache()
|
||||
|
||||
@checks.is_owner()
|
||||
@permissions.command(name="addglobalrule")
|
||||
async def add_to_global_rule(
|
||||
self,
|
||||
ctx: commands.Context,
|
||||
allow_or_deny: RuleType,
|
||||
cog_or_command: CogOrCommand,
|
||||
who_or_what: str,
|
||||
):
|
||||
"""
|
||||
Adds something to the rules
|
||||
|
||||
allow_or_deny: "allow" or "deny", depending on the rule to modify
|
||||
|
||||
cog_or_command: case sensitive cog or command name
|
||||
nested commands should be space seperated, but enclosed in quotes
|
||||
|
||||
who_or_what: what to add to the rule list.
|
||||
For best results, use an ID or mention
|
||||
The bot will try to uniquely match even without,
|
||||
but a failure to do so will raise an error
|
||||
This can be a user, role, channel, or guild
|
||||
"""
|
||||
obj = self.find_object_uniquely(who_or_what)
|
||||
if not obj:
|
||||
return await ctx.send(_("No unique matches. Try using an ID or mention."))
|
||||
model_type, type_name = cog_or_command
|
||||
async with self.config.owner_models() as models:
|
||||
data = {k: v for k, v in models.items()}
|
||||
if model_type not in data:
|
||||
data[model_type] = {}
|
||||
if type_name not in data[model_type]:
|
||||
data[model_type][type_name] = {}
|
||||
if allow_or_deny not in data[model_type][type_name]:
|
||||
data[model_type][type_name][allow_or_deny] = []
|
||||
|
||||
if obj in data[model_type][type_name][allow_or_deny]:
|
||||
return await ctx.send(_("That rule already exists."))
|
||||
|
||||
data[model_type][type_name][allow_or_deny].append(obj)
|
||||
models.update(data)
|
||||
await ctx.send(_("Rule added."))
|
||||
self.invalidate_cache(type_name, obj)
|
||||
|
||||
@commands.guild_only()
|
||||
@checks.guildowner_or_permissions(administrator=True)
|
||||
@permissions.command(name="addguildrule")
|
||||
async def add_to_guild_rule(
|
||||
self,
|
||||
ctx: commands.Context,
|
||||
allow_or_deny: RuleType,
|
||||
cog_or_command: CogOrCommand,
|
||||
who_or_what: str,
|
||||
):
|
||||
"""
|
||||
Adds something to the rules
|
||||
|
||||
allow_or_deny: "allow" or "deny", depending on the rule to modify
|
||||
|
||||
cog_or_command: case sensitive cog or command name
|
||||
nested commands should be space seperated, but enclosed in quotes
|
||||
|
||||
who_or_what: what to add to the rule list.
|
||||
For best results, use an ID or mention
|
||||
The bot will try to uniquely match even without,
|
||||
but a failure to do so will raise an error
|
||||
This can be a user, role, channel, or guild
|
||||
"""
|
||||
obj = self.find_object_uniquely(who_or_what)
|
||||
if not obj:
|
||||
return await ctx.send(_("No unique matches. Try using an ID or mention."))
|
||||
model_type, type_name = cog_or_command
|
||||
async with self.config.guild(ctx.guild).owner_models() as models:
|
||||
data = {k: v for k, v in models.items()}
|
||||
if model_type not in data:
|
||||
data[model_type] = {}
|
||||
if type_name not in data[model_type]:
|
||||
data[model_type][type_name] = {}
|
||||
if allow_or_deny not in data[model_type][type_name]:
|
||||
data[model_type][type_name][allow_or_deny] = []
|
||||
|
||||
if obj in data[model_type][type_name][allow_or_deny]:
|
||||
return await ctx.send(_("That rule already exists."))
|
||||
|
||||
data[model_type][type_name][allow_or_deny].append(obj)
|
||||
models.update(data)
|
||||
await ctx.send(_("Rule added."))
|
||||
self.invalidate_cache(type_name, obj)
|
||||
|
||||
@checks.is_owner()
|
||||
@permissions.command(name="removeglobalrule")
|
||||
async def rem_from_global_rule(
|
||||
self,
|
||||
ctx: commands.Context,
|
||||
allow_or_deny: RuleType,
|
||||
cog_or_command: CogOrCommand,
|
||||
who_or_what: str,
|
||||
):
|
||||
"""
|
||||
removes something from the rules
|
||||
|
||||
allow_or_deny: "allow" or "deny", depending on the rule to modify
|
||||
|
||||
cog_or_command: case sensitive cog or command name
|
||||
nested commands should be space seperated, but enclosed in quotes
|
||||
|
||||
who_or_what: what to add to the rule list.
|
||||
For best results, use an ID or mention
|
||||
The bot will try to uniquely match even without,
|
||||
but a failure to do so will raise an error
|
||||
This can be a user, role, channel, or guild
|
||||
"""
|
||||
obj = self.find_object_uniquely(who_or_what)
|
||||
if not obj:
|
||||
return await ctx.send(_("No unique matches. Try using an ID or mention."))
|
||||
model_type, type_name = cog_or_command
|
||||
async with self.config.owner_models() as models:
|
||||
data = {k: v for k, v in models.items()}
|
||||
if model_type not in data:
|
||||
data[model_type] = {}
|
||||
if type_name not in data[model_type]:
|
||||
data[model_type][type_name] = {}
|
||||
if allow_or_deny not in data[model_type][type_name]:
|
||||
data[model_type][type_name][allow_or_deny] = []
|
||||
|
||||
if obj not in data[model_type][type_name][allow_or_deny]:
|
||||
return await ctx.send(_("That rule doesn't exist."))
|
||||
|
||||
data[model_type][type_name][allow_or_deny].remove(obj)
|
||||
models.update(data)
|
||||
await ctx.send(_("Rule removed."))
|
||||
self.invalidate_cache(obj, type_name)
|
||||
|
||||
@commands.guild_only()
|
||||
@checks.guildowner_or_permissions(administrator=True)
|
||||
@permissions.command(name="removeguildrule")
|
||||
async def rem_from_guild_rule(
|
||||
self,
|
||||
ctx: commands.Context,
|
||||
allow_or_deny: RuleType,
|
||||
cog_or_command: CogOrCommand,
|
||||
who_or_what: str,
|
||||
):
|
||||
"""
|
||||
removes something from the rules
|
||||
|
||||
allow_or_deny: "allow" or "deny", depending on the rule to modify
|
||||
|
||||
cog_or_command: case sensitive cog or command name
|
||||
nested commands should be space seperated, but enclosed in quotes
|
||||
|
||||
who_or_what: what to add to the rule list.
|
||||
For best results, use an ID or mention
|
||||
The bot will try to uniquely match even without,
|
||||
but a failure to do so will raise an error
|
||||
This can be a user, role, channel, or guild
|
||||
"""
|
||||
obj = self.find_object_uniquely(who_or_what)
|
||||
if not obj:
|
||||
return await ctx.send(_("No unique matches. Try using an ID or mention."))
|
||||
model_type, type_name = cog_or_command
|
||||
async with self.config.guild(ctx.guild).owner_models() as models:
|
||||
data = {k: v for k, v in models.items()}
|
||||
if model_type not in data:
|
||||
data[model_type] = {}
|
||||
if type_name not in data[model_type]:
|
||||
data[model_type][type_name] = {}
|
||||
if allow_or_deny not in data[model_type][type_name]:
|
||||
data[model_type][type_name][allow_or_deny] = []
|
||||
|
||||
if obj not in data[model_type][type_name][allow_or_deny]:
|
||||
return await ctx.send(_("That rule doesn't exist."))
|
||||
|
||||
data[model_type][type_name][allow_or_deny].remove(obj)
|
||||
models.update(data)
|
||||
await ctx.send(_("Rule removed."))
|
||||
self.invalidate_cache(obj, type_name)
|
||||
|
||||
@commands.guild_only()
|
||||
@checks.guildowner_or_permissions(administrator=True)
|
||||
@permissions.command(name="setdefaultguildrule")
|
||||
async def set_default_guild_rule(
|
||||
self, ctx: commands.Context, allow_or_deny: ClearableRuleType, cog_or_command: CogOrCommand
|
||||
):
|
||||
"""
|
||||
Sets the default behavior for a cog or command if no rule is set
|
||||
"""
|
||||
val_to_set = {"allow": True, "deny": False, "clear": None}.get(allow_or_deny)
|
||||
|
||||
model_type, type_name = cog_or_command
|
||||
async with self.config.guild(ctx.guild).owner_models() as models:
|
||||
data = {k: v for k, v in models.items()}
|
||||
if model_type not in data:
|
||||
data[model_type] = {}
|
||||
if type_name not in data[model_type]:
|
||||
data[model_type][type_name] = {}
|
||||
|
||||
data[model_type][type_name]["default"] = val_to_set
|
||||
|
||||
models.update(data)
|
||||
await ctx.send(_("Default set."))
|
||||
self.invalidate_cache(type_name)
|
||||
|
||||
@checks.is_owner()
|
||||
@permissions.command(name="setdefaultglobalrule")
|
||||
async def set_default_global_rule(
|
||||
self, ctx: commands.Context, allow_or_deny: ClearableRuleType, cog_or_command: CogOrCommand
|
||||
):
|
||||
"""
|
||||
Sets the default behavior for a cog or command if no rule is set
|
||||
"""
|
||||
val_to_set = {"allow": True, "deny": False, "clear": None}.get(allow_or_deny)
|
||||
|
||||
model_type, type_name = cog_or_command
|
||||
async with self.config.owner_models() as models:
|
||||
data = {k: v for k, v in models.items()}
|
||||
if model_type not in data:
|
||||
data[model_type] = {}
|
||||
if type_name not in data[model_type]:
|
||||
data[model_type][type_name] = {}
|
||||
|
||||
data[model_type][type_name]["default"] = val_to_set
|
||||
|
||||
models.update(data)
|
||||
await ctx.send(_("Default set."))
|
||||
self.invalidate_cache(type_name)
|
||||
|
||||
@checks.is_owner()
|
||||
@permissions.command(name="clearglobalsettings")
|
||||
async def clear_globals(self, ctx: commands.Context):
|
||||
"""
|
||||
Clears all global rules.
|
||||
"""
|
||||
await self._confirm_then_clear_rules(ctx, is_guild=False)
|
||||
self.invalidate_cache()
|
||||
|
||||
@commands.guild_only()
|
||||
@checks.guildowner_or_permissions(administrator=True)
|
||||
@permissions.command(name="clearguildsettings")
|
||||
async def clear_guild_settings(self, ctx: commands.Context):
|
||||
"""
|
||||
Clears all guild rules.
|
||||
"""
|
||||
await self._confirm_then_clear_rules(ctx, is_guild=True)
|
||||
self.invalidate_cache(ctx.guild.id)
|
||||
|
||||
async def _confirm_then_clear_rules(self, ctx: commands.Context, is_guild: bool):
|
||||
if ctx.guild.me.permissions_in(ctx.channel).add_reactions:
|
||||
m = await ctx.send(_("Are you sure?"))
|
||||
for r in REACTS.keys():
|
||||
await m.add_reaction(r)
|
||||
try:
|
||||
reaction, user = await self.bot.wait_for(
|
||||
"reaction_add",
|
||||
check=lambda r, u: u == ctx.author and str(r) in REACTS,
|
||||
timeout=30,
|
||||
)
|
||||
except asyncio.TimeoutError:
|
||||
return await ctx.send(_("Ok, try responding with an emoji next time."))
|
||||
|
||||
agreed = REACTS.get(str(reaction))
|
||||
else:
|
||||
await ctx.send(_("Are you sure? (y/n)"))
|
||||
try:
|
||||
message = await self.bot.wait_for(
|
||||
"message",
|
||||
check=lambda m: m.author == ctx.author and m.content in Y_OR_N,
|
||||
timeout=30,
|
||||
)
|
||||
except asyncio.TimeoutError:
|
||||
return await ctx.send(_("Ok, try responding with yes or no next time."))
|
||||
|
||||
agreed = Y_OR_N.get(message.content.lower())
|
||||
|
||||
if agreed:
|
||||
if is_guild:
|
||||
await self.config.guild(ctx.guild).owner_models.clear()
|
||||
await ctx.send(_("Guild settings cleared."))
|
||||
else:
|
||||
await self.config.owner_models.clear()
|
||||
await ctx.send(_("Global settings cleared."))
|
||||
else:
|
||||
await ctx.send(_("Okay."))
|
||||
|
||||
def invalidate_cache(self, *to_invalidate):
|
||||
"""
|
||||
Either invalidates the entire cache (if given no objects)
|
||||
or does a partial invalidation based on passed objects
|
||||
"""
|
||||
if len(to_invalidate) == 0:
|
||||
self.cache.clear()
|
||||
return
|
||||
# LRUDict inherits from ordered dict, hence the syntax below
|
||||
stil_valid = [
|
||||
(k, v) for k, v in self.cache.items() if not any(obj in k for obj in to_invalidate)
|
||||
]
|
||||
self.cache = LRUDict(*stil_valid, size=self.cache.size)
|
||||
|
||||
def find_object_uniquely(self, info: str) -> int:
|
||||
"""
|
||||
Finds an object uniquely, returns it's id or returns None
|
||||
"""
|
||||
if info is None:
|
||||
return None
|
||||
objs = []
|
||||
|
||||
objs.extend(self.bot.users)
|
||||
for guild in self.bot.guilds:
|
||||
objs.extend(guild.roles)
|
||||
objs.extend(guild.channels)
|
||||
|
||||
try:
|
||||
_id = int(info)
|
||||
except ValueError:
|
||||
_id = None
|
||||
|
||||
for function in (
|
||||
lambda x: x.id == _id,
|
||||
lambda x: x.mention == info,
|
||||
lambda x: str(x) == info,
|
||||
lambda x: x.name == info,
|
||||
lambda x: (x.nick if hasattr(x, "nick") else None) == info,
|
||||
):
|
||||
canidates = list(filter(function, objs))
|
||||
if len(canidates) == 1:
|
||||
return canidates[0].id
|
||||
|
||||
return None
|
||||
81
redbot/cogs/permissions/resolvers.py
Normal file
81
redbot/cogs/permissions/resolvers.py
Normal file
@@ -0,0 +1,81 @@
|
||||
import types
|
||||
import contextlib
|
||||
import asyncio
|
||||
import logging
|
||||
from redbot.core import commands
|
||||
|
||||
log = logging.getLogger("redbot.cogs.permissions.resolvers")
|
||||
|
||||
|
||||
def entries_from_ctx(ctx: commands.Context) -> tuple:
|
||||
voice_channel = None
|
||||
with contextlib.suppress(Exception):
|
||||
voice_channel = ctx.author.voice.voice_channel
|
||||
entries = [x.id for x in (ctx.author, voice_channel, ctx.channel) if x]
|
||||
roles = sorted(ctx.author.roles, reverse=True) if ctx.guild else []
|
||||
entries.extend([x.id for x in roles])
|
||||
# entries now contains the following (in order) (if applicable)
|
||||
# author.id
|
||||
# author.voice.voice_channel.id
|
||||
# channel.id
|
||||
# role.id for each role (highest to lowest)
|
||||
# (implicitly) guild.id because
|
||||
# the @everyone role shares an id with the guild
|
||||
return tuple(entries)
|
||||
|
||||
|
||||
async def val_if_check_is_valid(*, ctx: commands.Context, check: object, level: str) -> bool:
|
||||
"""
|
||||
Returns the value from a check if it is valid
|
||||
"""
|
||||
|
||||
val = None
|
||||
# let's not spam the console with improperly made 3rd party checks
|
||||
try:
|
||||
if asyncio.iscoroutinefunction(check):
|
||||
val = await check(ctx, level=level)
|
||||
else:
|
||||
val = check(ctx, level=level)
|
||||
except Exception as e:
|
||||
# but still provide a way to view it (run with debug flag)
|
||||
log.debug(str(e))
|
||||
|
||||
return val
|
||||
|
||||
|
||||
def resolve_models(*, ctx: commands.Context, models: dict) -> bool:
|
||||
"""
|
||||
Resolves models in order.
|
||||
"""
|
||||
|
||||
cmd_name = ctx.command.qualified_name
|
||||
cog_name = ctx.cog.__class__.__name__
|
||||
|
||||
resolved = None
|
||||
|
||||
to_iter = (("commands", cmd_name), ("cogs", cog_name))
|
||||
|
||||
for model_name, ctx_attr in to_iter:
|
||||
if ctx_attr in models.get(model_name, {}):
|
||||
blacklist = models[model_name][ctx_attr].get("deny", [])
|
||||
whitelist = models[model_name][ctx_attr].get("allow", [])
|
||||
resolved = resolve_lists(ctx=ctx, whitelist=whitelist, blacklist=blacklist)
|
||||
if resolved is not None:
|
||||
return resolved
|
||||
resolved = models[model_name][ctx_attr].get("default", None)
|
||||
if resolved is not None:
|
||||
return resolved
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def resolve_lists(*, ctx: commands.Context, whitelist: list, blacklist: list) -> bool:
|
||||
"""
|
||||
resolves specific lists
|
||||
"""
|
||||
for entry in entries_from_ctx(ctx):
|
||||
if entry in whitelist:
|
||||
return True
|
||||
if entry in blacklist:
|
||||
return False
|
||||
return None
|
||||
19
redbot/cogs/permissions/template.yaml
Normal file
19
redbot/cogs/permissions/template.yaml
Normal file
@@ -0,0 +1,19 @@
|
||||
cogs:
|
||||
Admin:
|
||||
allow:
|
||||
- 78631113035100160
|
||||
deny:
|
||||
- 96733288462286848
|
||||
Audio:
|
||||
allow:
|
||||
- 133049272517001216
|
||||
default: deny
|
||||
commands:
|
||||
cleanup bot:
|
||||
allow:
|
||||
- 78631113035100160
|
||||
default: deny
|
||||
ping:
|
||||
deny:
|
||||
- 96733288462286848
|
||||
default: allow
|
||||
67
redbot/cogs/permissions/yaml_handler.py
Normal file
67
redbot/cogs/permissions/yaml_handler.py
Normal file
@@ -0,0 +1,67 @@
|
||||
import io
|
||||
import yaml
|
||||
import pathlib
|
||||
import discord
|
||||
|
||||
|
||||
def yaml_template() -> dict:
|
||||
template_fp = pathlib.Path(__file__).parent / "template.yaml"
|
||||
|
||||
with template_fp.open() as f:
|
||||
return yaml.safe_load(f)
|
||||
|
||||
|
||||
async def yamlset_acl(ctx, *, config, update):
|
||||
_fp = io.BytesIO()
|
||||
await ctx.message.attachments[0].save(_fp)
|
||||
|
||||
try:
|
||||
data = yaml.safe_load(_fp)
|
||||
except yaml.YAMLError:
|
||||
_fp.close()
|
||||
del _fp
|
||||
raise
|
||||
|
||||
old_data = await config()
|
||||
|
||||
for outer, inner in data.items():
|
||||
for ok, iv in inner.items():
|
||||
for k, v in iv.items():
|
||||
if k == "default":
|
||||
data[outer][ok][k] = {"allow": True, "deny": False}.get(v.lower(), None)
|
||||
|
||||
if not update:
|
||||
continue
|
||||
try:
|
||||
if isinstance(old_data[outer][ok][k], list):
|
||||
data[outer][ok][k].extend(old_data[outer][ok][k])
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
await config.set(data)
|
||||
|
||||
|
||||
async def yamlget_acl(ctx, *, config):
|
||||
data = await config()
|
||||
removals = []
|
||||
|
||||
for outer, inner in data.items():
|
||||
for ok, iv in inner.items():
|
||||
for k, v in iv.items():
|
||||
if k != "default":
|
||||
continue
|
||||
if v is True:
|
||||
data[outer][ok][k] = "allow"
|
||||
elif v is False:
|
||||
data[outer][ok][k] = "deny"
|
||||
else:
|
||||
removals.append((outer, ok, k))
|
||||
|
||||
for tup in removals:
|
||||
o, i, k = tup
|
||||
data[o][i].pop(k, None)
|
||||
|
||||
_fp = io.BytesIO(yaml.dump(data, default_flow_style=False).encode())
|
||||
_fp.seek(0)
|
||||
await ctx.author.send(file=discord.File(_fp, filename="acl.yaml"))
|
||||
_fp.close()
|
||||
@@ -2,34 +2,29 @@ import logging
|
||||
import asyncio
|
||||
from typing import Union
|
||||
from datetime import timedelta
|
||||
|
||||
from copy import copy
|
||||
import contextlib
|
||||
import discord
|
||||
from discord.ext import commands
|
||||
|
||||
from redbot.core import Config, checks, RedContext
|
||||
from redbot.core import Config, checks, commands
|
||||
from redbot.core.utils.chat_formatting import pagify, box
|
||||
from redbot.core.utils.antispam import AntiSpam
|
||||
from redbot.core.bot import Red
|
||||
from redbot.core.i18n import CogI18n
|
||||
from redbot.core.i18n import Translator, cog_i18n
|
||||
from redbot.core.utils.tunnel import Tunnel
|
||||
|
||||
|
||||
_ = CogI18n("Reports", __file__)
|
||||
_ = Translator("Reports", __file__)
|
||||
|
||||
log = logging.getLogger("red.reports")
|
||||
|
||||
|
||||
@cog_i18n(_)
|
||||
class Reports:
|
||||
|
||||
default_guild_settings = {
|
||||
"output_channel": None,
|
||||
"active": False,
|
||||
"next_ticket": 1
|
||||
}
|
||||
default_guild_settings = {"output_channel": None, "active": False, "next_ticket": 1}
|
||||
|
||||
default_report = {
|
||||
'report': {}
|
||||
}
|
||||
default_report = {"report": {}}
|
||||
|
||||
# This can be made configureable later if it
|
||||
# becomes an issue.
|
||||
@@ -41,15 +36,14 @@ class Reports:
|
||||
(timedelta(seconds=5), 1),
|
||||
(timedelta(minutes=5), 3),
|
||||
(timedelta(hours=1), 10),
|
||||
(timedelta(days=1), 24)
|
||||
(timedelta(days=1), 24),
|
||||
]
|
||||
|
||||
def __init__(self, bot: Red):
|
||||
self.bot = bot
|
||||
self.config = Config.get_conf(
|
||||
self, 78631113035100160, force_registration=True)
|
||||
self.config = Config.get_conf(self, 78631113035100160, force_registration=True)
|
||||
self.config.register_guild(**self.default_guild_settings)
|
||||
self.config.register_custom('REPORT', **self.default_report)
|
||||
self.config.register_custom("REPORT", **self.default_report)
|
||||
self.antispam = {}
|
||||
self.user_cache = []
|
||||
self.tunnel_store = {}
|
||||
@@ -58,38 +52,36 @@ class Reports:
|
||||
|
||||
@property
|
||||
def tunnels(self):
|
||||
return [
|
||||
x['tun'] for x in self.tunnel_store.values()
|
||||
]
|
||||
return [x["tun"] for x in self.tunnel_store.values()]
|
||||
|
||||
@checks.admin_or_permissions(manage_guild=True)
|
||||
@commands.guild_only()
|
||||
@commands.group(name="reportset")
|
||||
async def reportset(self, ctx: RedContext):
|
||||
async def reportset(self, ctx: commands.Context):
|
||||
"""
|
||||
settings for reports
|
||||
Settings for the report system.
|
||||
"""
|
||||
pass
|
||||
|
||||
@checks.admin_or_permissions(manage_guild=True)
|
||||
@reportset.command(name="output")
|
||||
async def setoutput(self, ctx: RedContext, channel: discord.TextChannel):
|
||||
"""sets the output channel"""
|
||||
async def setoutput(self, ctx: commands.Context, channel: discord.TextChannel):
|
||||
"""Set the channel where reports will show up"""
|
||||
await self.config.guild(ctx.guild).output_channel.set(channel.id)
|
||||
await ctx.send(_("Report Channel Set."))
|
||||
await ctx.send(_("The report channel has been set."))
|
||||
|
||||
@checks.admin_or_permissions(manage_guild=True)
|
||||
@reportset.command(name="toggleactive")
|
||||
async def report_toggle(self, ctx: RedContext):
|
||||
"""Toggles whether the Reporting tool is enabled or not"""
|
||||
@reportset.command(name="toggle", aliases=["toggleactive"])
|
||||
async def report_toggle(self, ctx: commands.Context):
|
||||
"""Enables or Disables reporting for the server"""
|
||||
|
||||
active = await self.config.guild(ctx.guild).active()
|
||||
active = not active
|
||||
await self.config.guild(ctx.guild).active.set(active)
|
||||
if active:
|
||||
await ctx.send(_("Reporting now enabled"))
|
||||
await ctx.send(_("Reporting is now enabled"))
|
||||
else:
|
||||
await ctx.send(_("Reporting disabled."))
|
||||
await ctx.send(_("Reporting is now disabled."))
|
||||
|
||||
async def internal_filter(self, m: discord.Member, mod=False, perms=None):
|
||||
ret = False
|
||||
@@ -98,9 +90,7 @@ class Reports:
|
||||
admin_role = discord.utils.get(
|
||||
guild.roles, id=await self.bot.db.guild(guild).admin_role()
|
||||
)
|
||||
mod_role = discord.utils.get(
|
||||
guild.roles, id=await self.bot.db.guild(guild).mod_role()
|
||||
)
|
||||
mod_role = discord.utils.get(guild.roles, id=await self.bot.db.guild(guild).mod_role())
|
||||
ret |= any(r in m.roles for r in (mod_role, admin_role))
|
||||
if perms:
|
||||
ret |= m.guild_permissions >= perms
|
||||
@@ -109,10 +99,14 @@ class Reports:
|
||||
ret |= await self.bot.is_owner(m)
|
||||
return ret
|
||||
|
||||
async def discover_guild(self, author: discord.User, *,
|
||||
mod: bool=False,
|
||||
permissions: Union[discord.Permissions, dict]={},
|
||||
prompt: str=""):
|
||||
async def discover_guild(
|
||||
self,
|
||||
author: discord.User,
|
||||
*,
|
||||
mod: bool = False,
|
||||
permissions: Union[discord.Permissions, dict] = None,
|
||||
prompt: str = "",
|
||||
):
|
||||
"""
|
||||
discovers which of shared guilds between the bot
|
||||
and provided user based on conditions (mod or permissions is an or)
|
||||
@@ -120,10 +114,12 @@ class Reports:
|
||||
prompt is for providing a user prompt for selection
|
||||
"""
|
||||
shared_guilds = []
|
||||
if isinstance(permissions, discord.Permissions):
|
||||
if permissions is None:
|
||||
perms = discord.Permissions()
|
||||
elif isinstance(permissions, discord.Permissions):
|
||||
perms = permissions
|
||||
else:
|
||||
permissions = discord.Permissions(**perms)
|
||||
perms = discord.Permissions(**permissions)
|
||||
|
||||
for guild in self.bot.guilds:
|
||||
x = guild.get_member(author.id)
|
||||
@@ -132,7 +128,6 @@ class Reports:
|
||||
shared_guilds.append(guild)
|
||||
if len(shared_guilds) == 0:
|
||||
raise ValueError("No Qualifying Shared Guilds")
|
||||
return
|
||||
if len(shared_guilds) == 1:
|
||||
return shared_guilds[0]
|
||||
output = ""
|
||||
@@ -148,13 +143,9 @@ class Reports:
|
||||
return m.author == author and m.channel == dm.channel
|
||||
|
||||
try:
|
||||
message = await self.bot.wait_for(
|
||||
'message', check=pred, timeout=45
|
||||
)
|
||||
message = await self.bot.wait_for("message", check=pred, timeout=45)
|
||||
except asyncio.TimeoutError:
|
||||
await author.send(
|
||||
_("You took too long to select. Try again later.")
|
||||
)
|
||||
await author.send(_("You took too long to select. Try again later."))
|
||||
return None
|
||||
|
||||
try:
|
||||
@@ -170,115 +161,134 @@ class Reports:
|
||||
|
||||
author = guild.get_member(msg.author.id)
|
||||
report = msg.clean_content
|
||||
avatar = author.avatar_url
|
||||
|
||||
em = discord.Embed(description=report)
|
||||
em.set_author(
|
||||
name=_('Report from {0.display_name}').format(author),
|
||||
icon_url=avatar
|
||||
)
|
||||
|
||||
ticket_number = await self.config.guild(guild).next_ticket()
|
||||
await self.config.guild(guild).next_ticket.set(ticket_number + 1)
|
||||
em.set_footer(text=_("Report #{}").format(ticket_number))
|
||||
|
||||
channel_id = await self.config.guild(guild).output_channel()
|
||||
channel = guild.get_channel(channel_id)
|
||||
if channel is not None:
|
||||
try:
|
||||
await channel.send(embed=em)
|
||||
except (discord.Forbidden, discord.HTTPException):
|
||||
return None
|
||||
else:
|
||||
if channel is None:
|
||||
return None
|
||||
|
||||
await self.config.custom('REPORT', guild.id, ticket_number).report.set(
|
||||
{'user_id': author.id, 'report': report}
|
||||
files = await Tunnel.files_from_attatch(msg)
|
||||
|
||||
ticket_number = await self.config.guild(guild).next_ticket()
|
||||
await self.config.guild(guild).next_ticket.set(ticket_number + 1)
|
||||
|
||||
if await self.bot.embed_requested(channel, author):
|
||||
em = discord.Embed(description=report)
|
||||
em.set_author(
|
||||
name=_("Report from {author}{maybe_nick}").format(
|
||||
author=author, maybe_nick=(f" ({author.nick})" if author.nick else "")
|
||||
),
|
||||
icon_url=author.avatar_url,
|
||||
)
|
||||
em.set_footer(text=_("Report #{}").format(ticket_number))
|
||||
send_content = None
|
||||
else:
|
||||
em = None
|
||||
send_content = _("Report from {author.mention} (Ticket #{number})").format(
|
||||
author=author, number=ticket_number
|
||||
)
|
||||
send_content += "\n" + report
|
||||
|
||||
try:
|
||||
await Tunnel.message_forwarder(
|
||||
destination=channel, content=send_content, embed=em, files=files
|
||||
)
|
||||
except (discord.Forbidden, discord.HTTPException):
|
||||
return None
|
||||
|
||||
await self.config.custom("REPORT", guild.id, ticket_number).report.set(
|
||||
{"user_id": author.id, "report": report}
|
||||
)
|
||||
return ticket_number
|
||||
|
||||
@commands.group(name="report", invoke_without_command=True)
|
||||
async def report(self, ctx: RedContext):
|
||||
"Follow the prompts to make a report"
|
||||
async def report(self, ctx: commands.Context, *, _report: str = ""):
|
||||
"""
|
||||
Send a report.
|
||||
|
||||
Use without arguments for interactive reporting, or do
|
||||
[p]report <text> to use it non-interactively.
|
||||
"""
|
||||
author = ctx.author
|
||||
guild = ctx.guild
|
||||
if guild is None:
|
||||
guild = await self.discover_guild(
|
||||
author,
|
||||
prompt=_("Select a server to make a report in by number.")
|
||||
author, prompt=_("Select a server to make a report in by number.")
|
||||
)
|
||||
else:
|
||||
try:
|
||||
await ctx.message.delete()
|
||||
except discord.Forbidden:
|
||||
pass
|
||||
if guild is None:
|
||||
return
|
||||
g_active = await self.config.guild(guild).active()
|
||||
if not g_active:
|
||||
return await author.send(
|
||||
_("Reporting has not been enabled for this server")
|
||||
)
|
||||
return await author.send(_("Reporting has not been enabled for this server"))
|
||||
if guild.id not in self.antispam:
|
||||
self.antispam[guild.id] = {}
|
||||
if author.id not in self.antispam[guild.id]:
|
||||
self.antispam[guild.id][author.id] = AntiSpam(self.intervals)
|
||||
if self.antispam[guild.id][author.id].spammy:
|
||||
return await author.send(
|
||||
_("You've sent a few too many of these recently. "
|
||||
"Contact a server admin to resolve this, or try again "
|
||||
"later.")
|
||||
_(
|
||||
"You've sent too many reports recently. "
|
||||
"Please contact a server admin if this is important matter, "
|
||||
"or please wait and try again later."
|
||||
)
|
||||
)
|
||||
|
||||
if author.id in self.user_cache:
|
||||
return await author.send(
|
||||
_("Finish making your prior report "
|
||||
"before making an additional one")
|
||||
_(
|
||||
"Please finish making your prior report before trying to make an "
|
||||
"additional one!"
|
||||
)
|
||||
)
|
||||
|
||||
if ctx.guild:
|
||||
try:
|
||||
await ctx.message.delete()
|
||||
except (discord.Forbidden, discord.HTTPException):
|
||||
pass
|
||||
self.user_cache.append(author.id)
|
||||
|
||||
try:
|
||||
dm = await author.send(
|
||||
_("Please respond to this message with your Report."
|
||||
"\nYour report should be a single message")
|
||||
)
|
||||
except discord.Forbidden:
|
||||
await ctx.send(
|
||||
_("This requires DMs enabled.")
|
||||
)
|
||||
self.user_cache.remove(author.id)
|
||||
return
|
||||
|
||||
def pred(m):
|
||||
return m.author == author and m.channel == dm.channel
|
||||
|
||||
try:
|
||||
message = await self.bot.wait_for(
|
||||
'message', check=pred, timeout=180
|
||||
)
|
||||
except asyncio.TimeoutError:
|
||||
await author.send(
|
||||
_("You took too long. Try again later.")
|
||||
)
|
||||
if _report:
|
||||
_m = copy(ctx.message)
|
||||
_m.content = _report
|
||||
_m.content = _m.clean_content
|
||||
val = await self.send_report(_m, guild)
|
||||
else:
|
||||
val = await self.send_report(message, guild)
|
||||
try:
|
||||
dm = await author.send(
|
||||
_(
|
||||
"Please respond to this message with your Report."
|
||||
"\nYour report should be a single message"
|
||||
)
|
||||
)
|
||||
except discord.Forbidden:
|
||||
return await ctx.send(_("This requires DMs enabled."))
|
||||
|
||||
def pred(m):
|
||||
return m.author == author and m.channel == dm.channel
|
||||
|
||||
try:
|
||||
message = await self.bot.wait_for("message", check=pred, timeout=180)
|
||||
except asyncio.TimeoutError:
|
||||
return await author.send(_("You took too long. Try again later."))
|
||||
else:
|
||||
val = await self.send_report(message, guild)
|
||||
|
||||
with contextlib.suppress(discord.Forbidden, discord.HTTPException):
|
||||
if val is None:
|
||||
await author.send(
|
||||
_("There was an error sending your report.")
|
||||
_("There was an error sending your report, please contact a server admin.")
|
||||
)
|
||||
else:
|
||||
await author.send(
|
||||
_("Your report was submitted. (Ticket #{})").format(val)
|
||||
)
|
||||
self.antispam[guild.id][author.id].stamp()
|
||||
await author.send(_("Your report was submitted. (Ticket #{})").format(val))
|
||||
self.antispam[guild.id][author.id].stamp()
|
||||
|
||||
self.user_cache.remove(author.id)
|
||||
@report.after_invoke
|
||||
async def report_cleanup(self, ctx: commands.Context):
|
||||
"""
|
||||
The logic is cleaner this way
|
||||
"""
|
||||
if ctx.author.id in self.user_cache:
|
||||
self.user_cache.remove(ctx.author.id)
|
||||
if ctx.guild and ctx.invoked_subcommand is None:
|
||||
if ctx.channel.permissions_for(ctx.guild.me).manage_messages:
|
||||
try:
|
||||
await ctx.message.delete()
|
||||
except discord.NotFound:
|
||||
pass
|
||||
|
||||
async def on_raw_reaction_add(self, payload):
|
||||
"""
|
||||
@@ -288,18 +298,14 @@ class Reports:
|
||||
return
|
||||
|
||||
_id = payload.message_id
|
||||
t = next(filter(
|
||||
lambda x: _id in x[1]['msgs'],
|
||||
self.tunnel_store.items()
|
||||
), None)
|
||||
t = next(filter(lambda x: _id in x[1]["msgs"], self.tunnel_store.items()), None)
|
||||
|
||||
if t is None:
|
||||
return
|
||||
tun = t[1]['tun']
|
||||
tun = t[1]["tun"]
|
||||
if payload.user_id in [x.id for x in tun.members]:
|
||||
await tun.react_close(
|
||||
uid=payload.user_id,
|
||||
message=_("{closer} has closed the correspondence")
|
||||
uid=payload.user_id, message=_("{closer} has closed the correspondence")
|
||||
)
|
||||
self.tunnel_store.pop(t[0], None)
|
||||
|
||||
@@ -307,68 +313,62 @@ class Reports:
|
||||
for k, v in self.tunnel_store.items():
|
||||
topic = _("Re: ticket# {1} in {0.name}").format(*k)
|
||||
# Tunnels won't forward unintended messages, this is safe
|
||||
msgs = await v['tun'].communicate(message=message, topic=topic)
|
||||
msgs = await v["tun"].communicate(message=message, topic=topic)
|
||||
if msgs:
|
||||
self.tunnel_store[k]['msgs'] = msgs
|
||||
self.tunnel_store[k]["msgs"] = msgs
|
||||
|
||||
@checks.mod_or_permissions(manage_members=True)
|
||||
@report.command(name='interact')
|
||||
@report.command(name="interact")
|
||||
async def response(self, ctx, ticket_number: int):
|
||||
"""
|
||||
opens a message tunnel between things you say in this channel
|
||||
and the ticket opener's direct messages
|
||||
Open a message tunnel.
|
||||
|
||||
This tunnel will forward things you say in this channel
|
||||
to the ticket opener's direct messages.
|
||||
|
||||
tunnels do not persist across bot restarts
|
||||
Tunnels do not persist across bot restarts.
|
||||
"""
|
||||
|
||||
# note, mod_or_permissions is an implicit guild_only
|
||||
guild = ctx.guild
|
||||
rec = await self.config.custom(
|
||||
'REPORT', guild.id, ticket_number).report()
|
||||
rec = await self.config.custom("REPORT", guild.id, ticket_number).report()
|
||||
|
||||
try:
|
||||
user = guild.get_member(rec.get('user_id'))
|
||||
user = guild.get_member(rec.get("user_id"))
|
||||
except KeyError:
|
||||
return await ctx.send(
|
||||
_("That ticket doesn't seem to exist")
|
||||
)
|
||||
return await ctx.send(_("That ticket doesn't seem to exist"))
|
||||
|
||||
if user is None:
|
||||
return await ctx.send(
|
||||
_("That user isn't here anymore.")
|
||||
)
|
||||
return await ctx.send(_("That user isn't here anymore."))
|
||||
|
||||
tun = Tunnel(recipient=user, origin=ctx.channel, sender=ctx.author)
|
||||
|
||||
if tun is None:
|
||||
return await ctx.send(
|
||||
_("Either you or the user you are trying to reach already "
|
||||
"has an open communication.")
|
||||
_(
|
||||
"Either you or the user you are trying to reach already "
|
||||
"has an open communication."
|
||||
)
|
||||
)
|
||||
|
||||
big_topic = _(
|
||||
"{who} opened a 2-way communication."
|
||||
"{who} opened a 2-way communication "
|
||||
"about ticket number {ticketnum}. Anything you say or upload here "
|
||||
"(8MB file size limitation on uploads) "
|
||||
"will be forwarded to them until the communication is closed.\n"
|
||||
"You can close a communication at any point "
|
||||
"by reacting with the X to the last message recieved. "
|
||||
"\nAny message succesfully forwarded with be marked with a check."
|
||||
"\nTunnels are not persistent across bot restarts."
|
||||
"You can close a communication at any point by reacting with "
|
||||
"the \N{NEGATIVE SQUARED CROSS MARK} to the last message recieved.\n"
|
||||
"Any message succesfully forwarded will be marked with "
|
||||
"\N{WHITE HEAVY CHECK MARK}.\n"
|
||||
"Tunnels are not persistent across bot restarts."
|
||||
)
|
||||
topic = big_topic.format(
|
||||
ticketnum=ticket_number,
|
||||
who=_("A moderator in `{guild.name}` has").format(guild=guild)
|
||||
ticketnum=ticket_number, who=_("A moderator in `{guild.name}` has").format(guild=guild)
|
||||
)
|
||||
try:
|
||||
m = await tun.communicate(
|
||||
message=ctx.message, topic=topic, skip_message_content=True
|
||||
)
|
||||
m = await tun.communicate(message=ctx.message, topic=topic, skip_message_content=True)
|
||||
except discord.Forbidden:
|
||||
await ctx.send(_("User has disabled DMs."))
|
||||
tun.close()
|
||||
await ctx.send(_("That user has DMs disabled."))
|
||||
else:
|
||||
self.tunnel_store[(guild, ticket_number)] = {'tun': tun, 'msgs': m}
|
||||
await ctx.send(
|
||||
big_topic.format(who=_("You have"), ticketnum=ticket_number)
|
||||
)
|
||||
self.tunnel_store[(guild, ticket_number)] = {"tun": tun, "msgs": m}
|
||||
await ctx.send(big_topic.format(who=_("You have"), ticketnum=ticket_number))
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
from .streams import Streams
|
||||
|
||||
|
||||
def setup(bot):
|
||||
bot.add_cog(Streams(bot))
|
||||
async def setup(bot):
|
||||
cog = Streams(bot)
|
||||
await cog.initialize()
|
||||
bot.add_cog(cog)
|
||||
|
||||
@@ -27,4 +27,4 @@ class OfflineStream(StreamsError):
|
||||
|
||||
|
||||
class OfflineCommunity(StreamsError):
|
||||
pass
|
||||
pass
|
||||
|
||||
@@ -1,15 +1,11 @@
|
||||
import subprocess
|
||||
|
||||
TO_TRANSLATE = [
|
||||
'../mod.py'
|
||||
]
|
||||
TO_TRANSLATE = ["../mod.py"]
|
||||
|
||||
|
||||
def regen_messages():
|
||||
subprocess.run(
|
||||
['pygettext', '-n'] + TO_TRANSLATE
|
||||
)
|
||||
subprocess.run(["pygettext", "-n"] + TO_TRANSLATE)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
regen_messages()
|
||||
regen_messages()
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user