-
A feature rich Modmail bot for Discord.
+
A feature-rich Modmail bot for Discord.
@@ -32,70 +32,86 @@
Modmail is similar to Reddit's Modmail both in functionality and purpose. It serves as a shared inbox for server staff to communicate with their users in a seamless way.
+This bot is free for everyone and always will be. If you like this project and would like to show your appreciation, you can support us on **[Patreon](https://www.patreon.com/kyber)**, cool benefits included!
+
## How does it work?
When a member sends a direct message to the bot, a channel or "thread" is created within an isolated category for that member. This channel is where messages will be relayed and where any available staff member can respond to that user.
-All threads are logged and you can view previous threads through the corresponding generated log link. Here is an [**example**](https://logs.modmail.tk/example)
+All threads are logged and you can view previous threads through the corresponding generated log link. Here is an [**example**](https://logs.modmail.tk/example).
## Features
-* **Highly Customisable**
+* **Highly Customisable:**
* Bot activity, prefix, category, log channel, etc.
- * Fully customisable command permission system.
- * Interface elements (color, responses, reactions, etc.)
- * Snippets and *command aliases*
- * Minimum account/guild age in order to create a thread.
-* **Thread logs**
+ * Command permission system.
+ * Interface elements (color, responses, reactions, etc).
+ * Snippets and *command aliases*.
+ * Minimum duration for accounts to be created before allowed to contact Modmail (`account_age`).
+ * Minimum duration for member to be in the guild allowed to contact Modmail (`guild_age`).
+* **Thread logs:**
* When you close a thread, a [log link](https://logs.modmail.tk/example) is generated and posted to your log channel.
- * Rendered in styled HTML like Discord.
- * Login in via Discord to protect your logs ([Patron only feature](https://patreon.com/kyber)).
- * See past logs of a user with `?logs`
- * Searchable by text queries using `?logs search`
-* **Robust implementation**
+ * Native Discord dark-mode feel.
+ * Markdown/formatting support.
+ * Login via Discord to protect your logs ([premium Patreon feature](https://patreon.com/kyber)).
+ * See past logs of a user with `?logs`.
+ * Searchable by text queries using `?logs search`.
+* **Robust implementation:**
* Scheduled tasks in human time, e.g. `?close in 2 hours silently`.
- * Editing and deleting messages is synced on both ends.
- * Support for the full range of message content (mutliple images, files).
+ * Editing and deleting messages are synced on all channels.
+ * Support for the full range of message content (multiple images, files).
* Paginated commands interfaces via reactions.
-This list is ever growing thanks to active development and our exceptional contributors. See a full list of documented commands by using the `help` command.
+This list is ever-growing thanks to active development and our exceptional contributors. See a full list of documented commands by using the `?help` command.
## Installation
-### Locally
-Installation locally for development reasons or otherwise is as follows, you will need `python 3.7`.
+### Heroku
+
+This bot can be hosted on Heroku.
+
+Installation via Heroku is possible with only your web browser.
+The [**installation guide**](https://github.com/kyb3r/modmail/wiki/Installation) will guide you through the entire installation process. If you run into any problems, join the [development server](https://discord.gg/etJNHCQ) for help and support.
+
+You can also set up auto-update. To do this:
+ - [Fork the repo](https://github.com/kyb3r/modmail/fork).
+ - [Install the Pull app for your fork](https://github.com/apps/pull).
+ - Then go to the Deploy tab in your Heroku account, select GitHub and connect your fork.
+ - Turn on auto-deploy for the `master` branch.
+
+### Hosting for patrons
+
+If you don't want to go through the trouble of setting up your own bot, and want to support this project as well, we offer installation, hosting and maintenance for Modmail bots for [**Patrons**](https://patreon.com/kyber). Join the support server for more info!
+
+### Locally
+
+Installation locally for development reasons or otherwise is as follows, you will need [`python 3.7`](https://www.python.org/downloads/).
+Follow the [**installation guide**](https://github.com/kyb3r/modmail/wiki/Installation) and disregard deploying Heroku apps. If you run into any problems, join the [development server](https://discord.gg/etJNHCQ) for help and support.
+
+Clone the repo:
-Clone the repo
```console
$ git clone https://github.com/kyb3r/modmail
$ cd modmail
```
-Install dependancies
-```console
-$ pipenv install
-```
+Install dependencies:
-Rename the `.env.example` to `.env` and fill out the fields.
-And finally, run the bot.
```console
-$ pipenv run python3 bot.py
+$ pipenv install
```
-### Hosting for patrons
-
-If you don't want to go through the trouble of setting up your own bot, and want to support this project as well, we offer installation, hosting and maintainance for Modmail bots for [**Patrons**](https://patreon.com/kyber). Join the support server for more info!
+Rename the `.env.example` to `.env` and fill out the fields. If `.env.example` is hidden, create a text file named `.env` and copy the contents of [`.env.example`](https://raw.githubusercontent.com/kyb3r/modmail/master/.env.example) then modify the values.
-### Heroku
-This bot can be hosted on heroku. Installation via Heroku is done in your web browser. The [**installation guide**](https://github.com/kyb3r/modmail/wiki/Installation) will guide you through the entire installation process. If you run into any problems, join the [development server](https://discord.gg/etJNHCQ) for help and support.
+Finally, run the bot.
-You can also set up autoupdates. To do this, [install the Pull app in your fork](https://github.com/apps/pull). Then go to the Deploy tab in your Heroku account, select GitHub and connect your fork. Turn on auto-deploy for the master branch.
+```console
+$ pipenv run bot
+```
## Plugins
-Modmail supports the use of third party plugins to extend or add functionality to the bot. This allows the introduction of niche features as well as anything else outside of the scope of the core functionality of Modmail. A list of third party plugins can be found using the `plugins registry` command. To develop your own, check out the [documentation](https://github.com/kyb3r/modmail/wiki/Plugins) for plugins.
+Modmail supports the use of third-party plugins to extend or add functionality to the bot. This allows the introduction of niche features as well as anything else outside of the scope of the core functionality of Modmail. A list of third party plugins can be found using the `plugins registry` command. To develop your own, check out the [documentation](https://github.com/kyb3r/modmail/wiki/Plugins) for plugins.
## Contributing
-Contributions to Modmail are always welcome, whether it be improvements to the documentation or new functionality, please feel free make the change. Check out our contribution [guidelines](https://github.com/kyb3r/modmail/blob/master/CONTRIBUTING.md) before you get started.
-
-This bot is free for everyone and always will be. If you like this project and would like to show your appreciation, here's the link for our **[Patreon](https://www.patreon.com/kyber)**.
+Contributions to Modmail are always welcome, whether it be improvements to the documentation or new functionality, please feel free to make the change. Check out our contribution [guidelines](https://github.com/kyb3r/modmail/blob/master/CONTRIBUTING.md) before you get started.
From 0ecccca82b6542a25134e616d7aa12fb7926b040 Mon Sep 17 00:00:00 2001
From: Kyber
Date: Mon, 15 Jul 2019 08:42:19 +1000
Subject: [PATCH 0322/1221] New sponsor (#324)
* Update README.md
* Update README.md
---
README.md | 6 ++++++
1 file changed, 6 insertions(+)
diff --git a/README.md b/README.md
index 389840138d..5776675b28 100644
--- a/README.md
+++ b/README.md
@@ -95,10 +95,16 @@ You can also set up autoupdates. To do this, [install the Pull app in your fork]
Sepcial thanks to our sponsors for supporting the project.
+
+
+
+
+
+
Become a [sponsor](https://patreon.com/kyber).
From ecaac569837d3e81ea2bd92452fefbd293bef369 Mon Sep 17 00:00:00 2001
From: Kyber
Date: Mon, 15 Jul 2019 09:13:14 +1000
Subject: [PATCH 0323/1221] Update SPONSORS.json
---
SPONSORS.json | 24 ++++++++++--------------
1 file changed, 10 insertions(+), 14 deletions(-)
diff --git a/SPONSORS.json b/SPONSORS.json
index 29011d0adc..d4b968999c 100644
--- a/SPONSORS.json
+++ b/SPONSORS.json
@@ -1,26 +1,22 @@
[{
"embed": {
- "title": "You can have a custom page for yourself here.",
- "description": "Generate your own embed from [here](https://leovoel.github.io/embed-visualizer/) and make a pull request editing the `SPONSORS.json` file on [GitHub](https://github.com/kyb3r/modmail/blob/master/SPONSORS.json).",
- "url": "https://patreon.com/kyber",
- "color": 6071775,
+ "description": "This is a youtube channel that provides high quality reddit content. Videos are uploaded regularly so you are never out of funny, interesting or even creepy content!",
+ "color": 15114050,
"footer": {
- "icon_url": "https://vignette.wikia.nocookie.net/smashtopia/images/c/c9/Insert_Image_Here.jpg/revision/latest?cb=20170827054712",
- "text": "footer text"
+ "icon_url": "https://i.imgur.com/fvNKUks.png",
+ "text": "I am a robot, son of Daniel (UK)"
},
"thumbnail": {
- "url": "https://vignette.wikia.nocookie.net/smashtopia/images/c/c9/Insert_Image_Here.jpg/revision/latest?cb=20170827054712"
+ "url": "https://i.imgur.com/WyzaPKY.png"
},
"author": {
- "name": "Nothing here yet!",
- "url": "https://patreon.com/kyber",
- "icon_url": "https://vignette.wikia.nocookie.net/smashtopia/images/c/c9/Insert_Image_Here.jpg/revision/latest?cb=20170827054712"
+ "name": "Sir Reddit",
+ "url": "https://www.youtube.com/channel/UCgSmBJD9imASmJRleycTCwQ?sub_confirmation=1",
+ "icon_url": "https://i.imgur.com/WyzaPKY.png"
},
"fields": [
- {
- "name": "🤔",
- "value": "This command lists the sponsors of this project on [**Patreon**](https://patreon.com/kyber)"
- }
+ {"name": "Subscribe!",
+ "value": "[**Click Here**](https://www.youtube.com/channel/UCgSmBJD9imASmJRleycTCwQ?sub_confirmation=1)"}
]
}
}]
From 6005d01fce96c228e57edcc7c132bce828b7ca83 Mon Sep 17 00:00:00 2001
From: Kyber
Date: Mon, 15 Jul 2019 23:40:04 +1000
Subject: [PATCH 0324/1221] Update SPONSORS.json
---
SPONSORS.json | 10 ++++++++--
1 file changed, 8 insertions(+), 2 deletions(-)
diff --git a/SPONSORS.json b/SPONSORS.json
index d4b968999c..989f1dfdfd 100644
--- a/SPONSORS.json
+++ b/SPONSORS.json
@@ -15,8 +15,14 @@
"icon_url": "https://i.imgur.com/WyzaPKY.png"
},
"fields": [
- {"name": "Subscribe!",
- "value": "[**Click Here**](https://www.youtube.com/channel/UCgSmBJD9imASmJRleycTCwQ?sub_confirmation=1)"}
+ {
+ "name": "Subscribe!",
+ "value": "[**Click Here**](https://www.youtube.com/channel/UCgSmBJD9imASmJRleycTCwQ?sub_confirmation=1)"
+ },
+ {
+ "name": "Discord Server!",
+ "value": "[**Click Here**](https://discord.gg/s8Ddphx)"
+ }
]
}
}]
From ef2c10b9901b14b969d17c4d800e0e058edaebfa Mon Sep 17 00:00:00 2001
From: Taaku18 <45324516+Taaku18@users.noreply.github.com>
Date: Wed, 17 Jul 2019 18:51:44 -0700
Subject: [PATCH 0325/1221] Add fallback method of searching through cache when
channel topic is invalid
---
core/thread.py | 11 ++++++++---
core/utils.py | 2 +-
2 files changed, 9 insertions(+), 4 deletions(-)
diff --git a/core/thread.py b/core/thread.py
index 87627dbaf7..d857c91df2 100644
--- a/core/thread.py
+++ b/core/thread.py
@@ -127,7 +127,6 @@ async def setup(self, *, creator=None, category=None):
log_url = log_count = None
# ensure core functionality still works
- topic = f"User ID: {recipient.id}"
if creator:
mention = None
else:
@@ -147,7 +146,7 @@ async def send_genesis_message():
self.ready = True
self.bot.dispatch("thread_ready", self)
- await channel.edit(topic=topic)
+ await channel.edit(topic=f"User ID: {recipient.id}")
self.bot.loop.create_task(send_genesis_message())
# Once thread is ready, tell the recipient.
@@ -735,7 +734,13 @@ async def find(
) -> Thread:
"""Finds a thread from cache or from discord channel topics."""
if recipient is None and channel is not None:
- return self._find_from_channel(channel)
+ thread = self._find_from_channel(channel)
+ if thread is None:
+ id, thread = next(((k, v) for k, v in self.cache.items() if v.channel == channel), (-1, None))
+ if thread is not None:
+ logger.debug('Found thread with tempered ID.')
+ await channel.edit(topic=f"User ID: {id}")
+ return thread
thread = None
diff --git a/core/utils.py b/core/utils.py
index bf88af1bff..411cbe3e65 100644
--- a/core/utils.py
+++ b/core/utils.py
@@ -188,7 +188,7 @@ def match_user_id(text: str) -> int:
int
The user ID if found. Otherwise, -1.
"""
- match = re.match(r"^User ID: (\d+)$", text)
+ match = re.search(r"\bUser ID: (\d+)\b", text)
if match is not None:
return int(match.group(1))
return -1
From 83ebd43051f19dce8f22fbfbfaa3467ca1d6c6e3 Mon Sep 17 00:00:00 2001
From: Stephen <48072084+StephenDaDev@users.noreply.github.com>
Date: Thu, 18 Jul 2019 14:18:04 -0400
Subject: [PATCH 0326/1221] Remove Moderation Plugin (#327)
Removed the Moderation Plugin Per Request of the Developer.
---
plugins/registry.json | 9 ---------
1 file changed, 9 deletions(-)
diff --git a/plugins/registry.json b/plugins/registry.json
index 5c3e6690bb..8822612d5d 100644
--- a/plugins/registry.json
+++ b/plugins/registry.json
@@ -107,15 +107,6 @@
"icon_url": "https://images.ionadev.ml/b/ZIDUUsl.png",
"thumbnail_url": "https://images.ionadev.ml/b/ZIDUUsl.png"
},
- "moderation": {
- "repository": "officialpiyush/modmail-plugins",
- "branch": "master",
- "description": "Rule offenders in your server? Lift the ban hammer or warn them! Moderation plugin to the rescue :tada:",
- "bot_version": "2.20.1",
- "title": "Moderation Plugin",
- "icon_url": "https://images.ionadev.ml/b/ZIDUUsl.png",
- "thumbnail_url": "https://images.ionadev.ml/b/ZIDUUsl.png"
- },
"translator": {
"repository": "officialpiyush/modmail-plugins",
"branch": "master",
From 226b3dc2ac52d5c25643f21cec7c8233be2357a9 Mon Sep 17 00:00:00 2001
From: Taaku18 <45324516+Taaku18@users.noreply.github.com>
Date: Thu, 18 Jul 2019 16:26:21 -0700
Subject: [PATCH 0327/1221] Bump discord.py version to 1.2.3
---
CHANGELOG.md | 1 +
Pipfile | 2 +-
Pipfile.lock | 12 ++++++------
cogs/utility.py | 4 ++--
4 files changed, 10 insertions(+), 9 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 23b77febb3..995e6a104a 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -42,6 +42,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- `config.cache` is no longer accessible, use `config['key']` for getting, `config['key'] = value` for setting, `config.remove('key')` for removing.
- Dynamic attribute for configs are removed, must use `config['key']` or `config.get('key')`.
- Removed helper functions `info()` and `error()` for formatting logging, it's formatted automatically now.
+- Bumped discord.py version to 1.2.3.
# v3.0.3
diff --git a/Pipfile b/Pipfile
index 095270dc6c..0b541bd697 100644
--- a/Pipfile
+++ b/Pipfile
@@ -20,7 +20,7 @@ parsedatetime = "==2.4"
aiohttp = "<3.6.0,>=3.3.0"
python-dotenv = ">=0.10.3"
pipenv = "==2018.11.26"
-"discord.py" = "==1.1.1"
+"discord.py" = "==1.2.3"
[requires]
python_version = "3.7"
diff --git a/Pipfile.lock b/Pipfile.lock
index c845f3a614..5fbfe85392 100644
--- a/Pipfile.lock
+++ b/Pipfile.lock
@@ -1,7 +1,7 @@
{
"_meta": {
"hash": {
- "sha256": "4c7d21c64125daa0eea6dfb5499eef9d589d2164d6955689e845c83e37d115ae"
+ "sha256": "46092f8b1032f2ac529f9b0df1aa725a4c6135d04826cd4cc4844af01879a4c2"
},
"pipfile-spec": 6,
"requires": {
@@ -82,10 +82,10 @@
},
"discord.py": {
"hashes": [
- "sha256:d0ab22f1fee1fcc02ac50a67ff49a5d1f6d7bc7eba77e34e35bd160b3ad3d7e8"
+ "sha256:4684733fa137cc7def18087ae935af615212e423e3dbbe3e84ef01d7ae8ed17d"
],
"index": "pypi",
- "version": "==1.1.1"
+ "version": "==1.2.3"
},
"dnspython": {
"hashes": [
@@ -265,10 +265,10 @@
},
"virtualenv": {
"hashes": [
- "sha256:b7335cddd9260a3dd214b73a2521ffc09647bde3e9457fcca31dc3be3999d04a",
- "sha256:d28ca64c0f3f125f59cabf13e0a150e1c68e5eea60983cc4395d88c584495783"
+ "sha256:861bbce3a418110346c70f5c7a696fdcf23a261424e1d28aa4f9362fc2ccbc19",
+ "sha256:ba8ce6a961d842320681fb90a3d564d0e5134f41dacd0e2bae7f02441dde2d52"
],
- "version": "==16.6.1"
+ "version": "==16.6.2"
},
"virtualenv-clone": {
"hashes": [
diff --git a/cogs/utility.py b/cogs/utility.py
index 1cbbc96b3f..315aff0c94 100644
--- a/cogs/utility.py
+++ b/cogs/utility.py
@@ -546,7 +546,7 @@ async def set_presence(
if status_by_key:
status = Status[status_identifier]
else:
- status = Status(status_identifier)
+ status = Status.try_value(status_identifier)
except (KeyError, ValueError):
if status_identifier is not None:
msg = f"Invalid status type: {status_identifier}"
@@ -564,7 +564,7 @@ async def set_presence(
if activity_by_key:
activity_type = ActivityType[activity_identifier]
else:
- activity_type = ActivityType(activity_identifier)
+ activity_type = ActivityType.try_value(activity_identifier)
except (KeyError, ValueError):
if activity_identifier is not None:
msg = f"Invalid activity type: {activity_identifier}"
From cbdf61fc981a1242530a30674cf9ec0989cd776f Mon Sep 17 00:00:00 2001
From: Taaku18 <45324516+Taaku18@users.noreply.github.com>
Date: Thu, 18 Jul 2019 17:35:44 -0700
Subject: [PATCH 0328/1221] Use tasks
---
CHANGELOG.md | 1 +
bot.py | 90 +++++++++++++++++++++++++++----------------------
cogs/utility.py | 2 +-
core/thread.py | 11 ++++--
4 files changed, 60 insertions(+), 44 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 995e6a104a..2e520bb1f0 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -43,6 +43,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Dynamic attribute for configs are removed, must use `config['key']` or `config.get('key')`.
- Removed helper functions `info()` and `error()` for formatting logging, it's formatted automatically now.
- Bumped discord.py version to 1.2.3.
+- Use discord tasks for metadata loop.
# v3.0.3
diff --git a/bot.py b/bot.py
index 4b3f30464d..5043c0681d 100644
--- a/bot.py
+++ b/bot.py
@@ -11,7 +11,7 @@
from types import SimpleNamespace
import discord
-from discord.ext import commands
+from discord.ext import commands, tasks
from discord.ext.commands.view import StringView
import isodate
@@ -70,6 +70,7 @@ def __init__(self):
self.config = ConfigManager(self)
self.config.populate_cache()
+
self.threads = ThreadManager(self)
self._configure_logging()
@@ -81,7 +82,8 @@ def __init__(self):
self.db = AsyncIOMotorClient(mongo_uri).modmail_bot
self.plugin_db = PluginDatabaseClient(self)
- self.metadata_task = self.loop.create_task(self.metadata_loop())
+ self.metadata_loop = None
+
self._load_extensions()
@property
@@ -183,12 +185,6 @@ def run(self, *args, **kwargs):
except Exception:
logger.critical("Fatal exception", exc_info=True)
finally:
- try:
- self.metadata_task.cancel()
- self.loop.run_until_complete(self.metadata_task)
- except asyncio.CancelledError:
- logger.debug("metadata_task has been cancelled.")
-
self.loop.run_until_complete(self.logout())
for task in asyncio.all_tasks(self.loop):
task.cancel()
@@ -437,6 +433,19 @@ async def on_ready(self):
logger.line()
+ self.metadata_loop = tasks.Loop(
+ self.post_metadata,
+ seconds=0,
+ minutes=0,
+ hours=1,
+ count=None,
+ reconnect=True,
+ loop=None,
+ )
+ self.metadata_loop.before_loop(self.before_post_metadata)
+ self.metadata_loop.after_loop(self.after_post_metadata)
+ self.metadata_loop.start()
+
async def convert_emoji(self, name: str) -> str:
ctx = SimpleNamespace(bot=self, guild=self.modmail_guild)
converter = commands.EmojiConverter()
@@ -958,7 +967,7 @@ async def validate_database_connection(self):
try:
await self.db.command("buildinfo")
except Exception as exc:
- logger.critical("Something went wrong " "while connecting to the database.")
+ logger.critical("Something went wrong while connecting to the database.")
message = f"{type(exc).__name__}: {str(exc)}"
logger.critical(message)
@@ -981,43 +990,44 @@ async def validate_database_connection(self):
else:
logger.info("Successfully connected to the database.")
- async def metadata_loop(self):
+ async def post_metadata(self):
+ owner = (await self.application_info()).owner
+ data = {
+ "owner_name": str(owner),
+ "owner_id": owner.id,
+ "bot_id": self.user.id,
+ "bot_name": str(self.user),
+ "avatar_url": str(self.user.avatar_url),
+ "guild_id": self.guild_id,
+ "guild_name": self.guild.name,
+ "member_count": len(self.guild.members),
+ "uptime": (datetime.utcnow() - self.start_time).total_seconds(),
+ "latency": f"{self.ws.latency * 1000:.4f}",
+ "version": self.version,
+ "selfhosted": True,
+ "last_updated": str(datetime.utcnow()),
+ }
+
+ async with self.session.post("https://api.modmail.tk/metadata", json=data):
+ logger.debug("Uploading metadata to Modmail server.")
+
+ async def before_post_metadata(self):
+ logger.info("Starting metadata loop.")
await self.wait_for_connected()
if not self.guild:
- return
-
- owner = (await self.application_info()).owner
+ self.metadata_loop.cancel()
- while not self.is_closed():
- data = {
- "owner_name": str(owner),
- "owner_id": owner.id,
- "bot_id": self.user.id,
- "bot_name": str(self.user),
- "avatar_url": str(self.user.avatar_url),
- "guild_id": self.guild_id,
- "guild_name": self.guild.name,
- "member_count": len(self.guild.members),
- "uptime": (datetime.utcnow() - self.start_time).total_seconds(),
- "latency": f"{self.ws.latency * 1000:.4f}",
- "version": self.version,
- "selfhosted": True,
- "last_updated": str(datetime.utcnow()),
- }
-
- async with self.session.post("https://api.modmail.tk/metadata", json=data):
- logger.debug("Uploading metadata to Modmail server.")
-
- await asyncio.sleep(3600)
+ async def after_post_metadata(self):
+ logger.info("Metadata loop has been cancelled.")
if __name__ == "__main__":
- if os.name != "nt":
- try:
- import uvloop
+ try:
+ import uvloop
+
+ uvloop.install()
+ except ImportError:
+ pass
- uvloop.install()
- except ImportError:
- pass
bot = ModmailBot()
bot.run()
diff --git a/cogs/utility.py b/cogs/utility.py
index 315aff0c94..ee7b7c9493 100644
--- a/cogs/utility.py
+++ b/cogs/utility.py
@@ -227,7 +227,7 @@ def cog_unload(self):
@commands.command()
@checks.has_permissions(PermissionLevel.REGULAR)
@trigger_typing
- async def changelog(self, ctx, version: str.lower = ''):
+ async def changelog(self, ctx, version: str.lower = ""):
"""Shows the changelog of the Modmail."""
changelog = await Changelog.from_url(self.bot)
version = version.lstrip("vV") if version else changelog.latest_version.version
diff --git a/core/thread.py b/core/thread.py
index bd693b02e3..04e506f7ae 100644
--- a/core/thread.py
+++ b/core/thread.py
@@ -319,7 +319,9 @@ async def _close(
else:
message = self.bot.config["thread_close_response"]
- message = message.format(closer=closer, loglink=log_url, logkey=log_data["key"] if log_data else None)
+ message = message.format(
+ closer=closer, loglink=log_url, logkey=log_data["key"] if log_data else None
+ )
embed.description = message
footer = self.bot.config["thread_close_footer"]
@@ -764,9 +766,12 @@ async def find(
if recipient is None and channel is not None:
thread = self._find_from_channel(channel)
if thread is None:
- id, thread = next(((k, v) for k, v in self.cache.items() if v.channel == channel), (-1, None))
+ id, thread = next(
+ ((k, v) for k, v in self.cache.items() if v.channel == channel),
+ (-1, None),
+ )
if thread is not None:
- logger.debug('Found thread with tempered ID.')
+ logger.debug("Found thread with tempered ID.")
await channel.edit(topic=f"User ID: {id}")
return thread
From 0bc17a88af49c8ab99e729484385b15c04367d90 Mon Sep 17 00:00:00 2001
From: Taaku18 <45324516+Taaku18@users.noreply.github.com>
Date: Sun, 21 Jul 2019 12:02:19 -0700
Subject: [PATCH 0329/1221] Misc
---
CHANGELOG.md | 4 ++
bot.py | 126 +++++++++++++++++++++++-------------
cogs/modmail.py | 38 ++++++++---
cogs/plugins.py | 86 +++++++++++++++----------
cogs/utility.py | 2 +-
core/changelog.py | 6 +-
core/clients.py | 8 ++-
core/config.py | 49 ++++++++------
core/models.py | 3 +-
core/paginator.py | 8 ++-
core/thread.py | 159 +++++++++++++++++++++++-----------------------
core/time.py | 6 +-
12 files changed, 297 insertions(+), 198 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 2e520bb1f0..bf4775ff87 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -26,6 +26,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- `?snippet` is now the default command name instead of `?snippets` (`?snippets` is still usable). This is to make this consistent with `?alias`/`?aliases`.
- `colorama` is no longer a necessity, this is due to some unsupported OS.
- Changelog command can now take a version argument to jump straight to specified version.
+- `?plugin enabled` results are now sorted alphabetically.
+- `?plugin registry` results are now sorted alphabetically, helps user find plugins more easily.
+- `?plugin registry page-number` plugin registry can specify a page number for quick access.
### Fixes
@@ -44,6 +47,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Removed helper functions `info()` and `error()` for formatting logging, it's formatted automatically now.
- Bumped discord.py version to 1.2.3.
- Use discord tasks for metadata loop.
+- More debug based logging.
# v3.0.3
diff --git a/bot.py b/bot.py
index 5043c0681d..b6d9d39ebe 100644
--- a/bot.py
+++ b/bot.py
@@ -19,6 +19,7 @@
from aiohttp import ClientSession
from emoji import UNICODE_EMOJI
from motor.motor_asyncio import AsyncIOMotorClient
+from pymongo.errors import ConfigurationError
try:
from colorama import init
@@ -65,6 +66,8 @@ def __init__(self):
super().__init__(command_prefix=None) # implemented in `get_prefix`
self._session = None
self._api = None
+ self.metadata_loop = None
+
self._connected = asyncio.Event()
self.start_time = datetime.utcnow()
@@ -77,13 +80,17 @@ def __init__(self):
mongo_uri = self.config["mongo_uri"]
if mongo_uri is None:
- raise ValueError("A Mongo URI is necessary for the bot to function.")
-
- self.db = AsyncIOMotorClient(mongo_uri).modmail_bot
- self.plugin_db = PluginDatabaseClient(self)
+ logger.critical("A Mongo URI is necessary for the bot to function.")
+ raise RuntimeError
- self.metadata_loop = None
+ try:
+ self.db = AsyncIOMotorClient(mongo_uri).modmail_bot
+ except ConfigurationError as e:
+ logger.critical("Your MONGO_URI is copied wrong, try re-copying from the source again.")
+ logger.critical(str(e))
+ sys.exit(0)
+ self.plugin_db = PluginDatabaseClient(self)
self._load_extensions()
@property
@@ -134,7 +141,8 @@ def _configure_logging(self):
logger.info("Logging level: %s", level_text)
else:
logger.info("Invalid logging level set.")
- logger.warning("Using default logging level: INFO.")
+ logger.warning("Using default logging level: %s.", level_text)
+ logger.debug("Successfully configured logging.")
@property
def version(self) -> str:
@@ -198,11 +206,21 @@ def run(self, *args, **kwargs):
self.loop.run_until_complete(self.session.close())
logger.error(" - Shutting down bot - ")
+ @property
+ def owner_ids(self):
+ owner_ids = self.config["owners"]
+ if owner_ids is not None:
+ owner_ids = set(map(int, str(owner_ids).split(",")))
+ if self.owner_id is not None:
+ owner_ids.add(self.owner_id)
+ permissions = self.config["level_permissions"].get(PermissionLevel.OWNER.name, [])
+ for perm in permissions:
+ owner_ids.add(int(perm))
+ return owner_ids
+
async def is_owner(self, user: discord.User) -> bool:
- owners = self.config["owners"]
- if owners is not None:
- if user.id in set(map(int, str(owners).split(","))):
- return True
+ if user.id in self.owner_ids:
+ return True
return await super().is_owner(user)
@property
@@ -212,18 +230,18 @@ def log_channel(self) -> typing.Optional[discord.TextChannel]:
channel = self.get_channel(int(channel_id))
if channel is not None:
return channel
+ logger.debug('LOG_CHANNEL_ID was invalid, removed.')
self.config.remove("log_channel_id")
if self.main_category is not None:
try:
channel = self.main_category.channels[0]
self.config["log_channel_id"] = channel.id
- logger.debug("No log channel set, however, one was found. Setting...")
+ logger.warning("No log channel set, setting #%s to be the log channel.", channel.name)
return channel
except IndexError:
pass
- logger.info(
- "No log channel set, set one with `%ssetup` or "
- "`%sconfig set log_channel_id `.",
+ logger.warning(
+ "No log channel set, set one with `%ssetup` or `%sconfig set log_channel_id `.",
self.prefix,
self.prefix,
)
@@ -250,7 +268,8 @@ def aliases(self) -> typing.Dict[str, str]:
def token(self) -> str:
token = self.config["token"]
if token is None:
- raise ValueError("TOKEN must be set, this is your bot token.")
+ logger.critical("TOKEN must be set, set this as bot token found on the Discord Dev Portal.")
+ sys.exit(0)
return token
@property
@@ -260,7 +279,7 @@ def guild_id(self) -> typing.Optional[int]:
try:
return int(str(guild_id))
except ValueError:
- raise ValueError("Invalid guild_id set.")
+ logger.critical("Invalid GUILD_ID set.")
return None
@property
@@ -284,7 +303,7 @@ def modmail_guild(self) -> typing.Optional[discord.Guild]:
if guild is not None:
return guild
self.config.remove("modmail_guild_id")
- logger.error("Invalid modmail_guild_id set.")
+ logger.critical("Invalid MODMAIL_GUILD_ID set.")
return self.guild
@property
@@ -302,10 +321,11 @@ def main_category(self) -> typing.Optional[discord.CategoryChannel]:
if cat is not None:
return cat
self.config.remove("main_category_id")
+ logger.debug('MAIN_CATEGORY_ID was invalid, removed.')
cat = discord.utils.get(self.modmail_guild.categories, name="Modmail")
if cat is not None:
self.config["main_category_id"] = cat.id
- logger.debug("No main category set, however, one was found. Setting...")
+ logger.debug("No main category set explicitly, setting category \"Modmail\" as the main category.")
return cat
return None
@@ -384,6 +404,7 @@ async def setup_indexes(self):
("key", "text"),
]
)
+ logger.debug('Successfully set up database indexes.')
async def on_ready(self):
"""Bot startup, sets uptime."""
@@ -391,14 +412,18 @@ async def on_ready(self):
# Wait until config cache is populated with stuff from db and on_connect ran
await self.wait_for_connected()
+ if self.guild is None:
+ logger.debug('Logging out due to invalid GUILD_ID.')
+ return await self.logout()
+
logger.line()
logger.info("Client ready.")
logger.line()
logger.info("Logged in as: %s", self.user)
logger.info("User ID: %s", self.user.id)
logger.info("Prefix: %s", self.prefix)
- logger.info("Guild Name: %s", self.guild.name if self.guild else "Invalid")
- logger.info("Guild ID: %s", self.guild.id if self.guild else "Invalid")
+ logger.info("Guild Name: %s", self.guild.name)
+ logger.info("Guild ID: %s", self.guild.id)
logger.line()
await self.threads.populate_cache()
@@ -406,6 +431,7 @@ async def on_ready(self):
# closures
closures = self.config["closures"]
logger.info("There are %d thread(s) pending to be closed.", len(closures))
+ logger.line()
for recipient_id, items in tuple(closures.items()):
after = (
@@ -418,10 +444,13 @@ async def on_ready(self):
if not thread:
# If the channel is deleted
+ logger.debug('Failed to close thread for recipient %s.', recipient_id)
self.config["closures"].pop(recipient_id)
await self.config.update()
continue
+ logger.debug('Closing thread for recipient %s.', recipient_id)
+
await thread.close(
closer=self.get_user(items["closer_id"]),
after=after,
@@ -431,8 +460,6 @@ async def on_ready(self):
auto_close=items.get("auto_close", False),
)
- logger.line()
-
self.metadata_loop = tasks.Loop(
self.post_metadata,
seconds=0,
@@ -552,6 +579,7 @@ async def _process_blocked(self, message: discord.Message) -> bool:
reaction = blocked_emoji
changed = False
delta = human_timedelta(min_account_age)
+ logger.debug('Blocked due to account age, user %s.', message.author.name)
if str(message.author.id) not in self.blocked_users:
new_reason = (
@@ -565,7 +593,7 @@ async def _process_blocked(self, message: discord.Message) -> bool:
embed=discord.Embed(
title="Message not sent!",
description=f"Your must wait for {delta} "
- f"before you can contact {self.user.mention}.",
+ f"before you can contact me.",
color=discord.Color.red(),
)
)
@@ -575,6 +603,7 @@ async def _process_blocked(self, message: discord.Message) -> bool:
reaction = blocked_emoji
changed = False
delta = human_timedelta(min_guild_age)
+ logger.debug('Blocked due to guild age, user %s.', message.author.name)
if str(message.author.id) not in self.blocked_users:
new_reason = (
@@ -588,22 +617,24 @@ async def _process_blocked(self, message: discord.Message) -> bool:
embed=discord.Embed(
title="Message not sent!",
description=f"Your must wait for {delta} "
- f"before you can contact {self.user.mention}.",
+ f"before you can contact me.",
color=discord.Color.red(),
)
)
elif str(message.author.id) in self.blocked_users:
- reaction = blocked_emoji
if reason.startswith("System Message: New Account.") or reason.startswith(
"System Message: Recently Joined."
):
# Met the age limit already, otherwise it would've been caught by the previous if's
reaction = sent_emoji
+ logger.debug('No longer internally blocked, user %s.', message.author.name)
self.blocked_users.pop(str(message.author.id))
else:
+ reaction = blocked_emoji
end_time = re.search(r"%(.+?)%$", reason)
if end_time is not None:
+ logger.debug('No longer blocked, user %s.', message.author.name)
after = (
datetime.fromisoformat(end_time.group(1)) - now
).total_seconds()
@@ -611,6 +642,8 @@ async def _process_blocked(self, message: discord.Message) -> bool:
# No longer blocked
reaction = sent_emoji
self.blocked_users.pop(str(message.author.id))
+ else:
+ logger.debug('User blocked, user %s.', message.author.name)
else:
reaction = sent_emoji
@@ -619,7 +652,7 @@ async def _process_blocked(self, message: discord.Message) -> bool:
try:
await message.add_reaction(reaction)
except (discord.HTTPException, discord.InvalidArgument):
- pass
+ logger.warning('Failed to add reaction %s.', reaction, exc_info=True)
return str(message.author.id) in self.blocked_users
async def process_modmail(self, message: discord.Message) -> None:
@@ -805,20 +838,17 @@ async def on_raw_reaction_add(self, payload):
if isinstance(channel, discord.DMChannel):
if str(reaction) == str(close_emoji): # closing thread
+ try:
+ recipient_thread_close = strtobool(self.config["recipient_thread_close"])
+ except ValueError:
+ recipient_thread_close = self.config.remove("recipient_thread_close")
+ if not recipient_thread_close:
+ return
thread = await self.threads.find(recipient=user)
ts = message.embeds[0].timestamp if message.embeds else None
if thread and ts == thread.channel.created_at:
# the reacted message is the corresponding thread creation embed
- try:
- recipient_thread_close = strtobool(
- self.config["recipient_thread_close"]
- )
- except ValueError:
- recipient_thread_close = self.config.remove(
- "recipient_thread_close"
- )
- if recipient_thread_close:
- await thread.close(closer=user)
+ await thread.close(closer=user)
else:
if not message.embeds:
return
@@ -845,6 +875,7 @@ async def on_guild_channel_delete(self, channel):
if isinstance(channel, discord.CategoryChannel):
if self.main_category.id == channel.id:
+ logger.debug('Main category was deleted.')
self.config.remove("main_category_id")
await self.config.update()
return
@@ -853,17 +884,19 @@ async def on_guild_channel_delete(self, channel):
return
if self.log_channel is None or self.log_channel.id == channel.id:
+ logger.info('Log channel deleted.')
self.config.remove("log_channel_id")
await self.config.update()
return
thread = await self.threads.find(channel=channel)
- if not thread:
- return
-
- await thread.close(closer=mod, silent=True, delete_channel=False)
+ if thread:
+ logger.debug('Manually closed channel %s.', channel.name)
+ await thread.close(closer=mod, silent=True, delete_channel=False)
async def on_member_remove(self, member):
+ if member.guild != self.guild:
+ return
thread = await self.threads.find(recipient=member)
if thread:
embed = discord.Embed(
@@ -873,6 +906,8 @@ async def on_member_remove(self, member):
await thread.channel.send(embed=embed)
async def on_member_join(self, member):
+ if member.guild != self.guild:
+ return
thread = await self.threads.find(recipient=member)
if thread:
embed = discord.Embed(
@@ -980,11 +1015,14 @@ async def validate_database_connection(self):
if "OperationFailure" in message:
logger.critical(
- "This is due to having invalid credentials in your MONGO_URI."
+ "This is due to having invalid credentials in your MONGO_URI. "
+ "Remember you need to substitute `` with your actual password."
)
logger.critical(
- "Recheck the username/password and make sure to url encode them. "
- "https://www.urlencoder.io/"
+ "Be sure to URL encode your username and password (not the entire URL!!), "
+ "https://www.urlencoder.io/, if this issue persists, try changing your username and password "
+ "to only include alphanumeric characters, no symbols."
+ ""
)
raise
else:
@@ -1024,7 +1062,7 @@ async def after_post_metadata(self):
if __name__ == "__main__":
try:
import uvloop
-
+ logger.debug('Setting up with uvloop.')
uvloop.install()
except ImportError:
pass
diff --git a/cogs/modmail.py b/cogs/modmail.py
index dc4e92e87b..b8ebaf1adf 100644
--- a/cogs/modmail.py
+++ b/cogs/modmail.py
@@ -1,4 +1,5 @@
import asyncio
+import logging
from datetime import datetime
from typing import Optional, Union
from types import SimpleNamespace as param
@@ -16,6 +17,8 @@
from core.time import UserFriendlyTime, human_timedelta
from core.utils import format_preview, User
+logger = logging.getLogger("Modmail")
+
class Modmail(commands.Cog):
"""Commands directly related to Modmail functionality."""
@@ -35,6 +38,7 @@ async def setup(self, ctx):
"""
if self.bot.main_category is not None:
+ logger.debug("Can't re-setup server, main_category is found.")
return await ctx.send(f"{self.bot.modmail_guild} is already set up.")
if self.bot.modmail_guild is None:
@@ -58,9 +62,8 @@ async def setup(self, ctx):
embed = discord.Embed(
title="Friendly Reminder",
description=f"You may use the `{self.bot.prefix}config set log_channel_id "
- "` command to set up a custom log channel"
- ", then you can delete the default "
- f"{log_channel.mention} channel.",
+ "` command to set up a custom log channel, then you can delete this default "
+ f"{log_channel.mention} log channel.",
color=self.bot.main_color,
)
@@ -92,7 +95,8 @@ async def setup(self, ctx):
and not self.bot.config["level_permissions"]
):
await self.bot.update_perms(PermissionLevel.REGULAR, -1)
- await self.bot.update_perms(PermissionLevel.OWNER, ctx.author.id)
+ for owner_ids in self.bot.owner_ids:
+ await self.bot.update_perms(PermissionLevel.OWNER, owner_ids)
@commands.group(aliases=["snippets"], invoke_without_command=True)
@checks.has_permissions(PermissionLevel.SUPPORTER)
@@ -504,7 +508,11 @@ async def unsubscribe(
async def nsfw(self, ctx):
"""Flags a Modmail thread as NSFW (not safe for work)."""
await ctx.channel.edit(nsfw=True)
- await ctx.message.add_reaction("✅")
+ sent_emoji, _ = await self.bot.retrieve_emoji()
+ try:
+ await ctx.message.add_reaction(sent_emoji)
+ except (discord.HTTPException, discord.InvalidArgument):
+ pass
@commands.command()
@checks.has_permissions(PermissionLevel.SUPPORTER)
@@ -512,7 +520,11 @@ async def nsfw(self, ctx):
async def sfw(self, ctx):
"""Flags a Modmail thread as SFW (safe for work)."""
await ctx.channel.edit(nsfw=False)
- await ctx.message.add_reaction("✅")
+ sent_emoji, _ = await self.bot.retrieve_emoji()
+ try:
+ await ctx.message.add_reaction(sent_emoji)
+ except (discord.HTTPException, discord.InvalidArgument):
+ pass
@commands.command()
@checks.has_permissions(PermissionLevel.SUPPORTER)
@@ -662,7 +674,7 @@ async def logs_search(self, ctx, limit: Optional[int] = None, *, query):
if not embeds:
embed = discord.Embed(
color=discord.Color.red(),
- description="No log entries have been found for that query",
+ description="No log entries have been found for that query.",
)
return await ctx.send(embed=embed)
@@ -764,7 +776,11 @@ async def edit(self, ctx, message_id: Optional[int] = None, *, message: str):
self.bot.api.edit_message(linked_message_id, message),
)
- await ctx.message.add_reaction("✅")
+ sent_emoji, _ = await self.bot.retrieve_emoji()
+ try:
+ await ctx.message.add_reaction(sent_emoji)
+ except (discord.HTTPException, discord.InvalidArgument):
+ pass
@commands.command()
@checks.has_permissions(PermissionLevel.SUPPORTER)
@@ -1093,7 +1109,11 @@ async def delete(self, ctx, message_id: Optional[int] = None):
)
await thread.delete_message(linked_message_id)
- await ctx.message.add_reaction("✅")
+ sent_emoji, _ = await self.bot.retrieve_emoji()
+ try:
+ await ctx.message.add_reaction(sent_emoji)
+ except (discord.HTTPException, discord.InvalidArgument):
+ pass
def setup(bot):
diff --git a/cogs/plugins.py b/cogs/plugins.py
index 7f2dcec871..5f58c697f7 100644
--- a/cogs/plugins.py
+++ b/cogs/plugins.py
@@ -3,12 +3,12 @@
import json
import logging
import os
-import random
import shutil
import site
import stat
import subprocess
import sys
+import typing
from difflib import get_close_matches
import discord
@@ -57,7 +57,7 @@ def parse_plugin(name):
# default branch = master
try:
# when names are formatted with inline code
- result = name.strip("`").split("/")
+ result = name.split("/")
result[2] = "/".join(result[2:])
if "@" in result[2]:
# branch is specified
@@ -83,13 +83,13 @@ async def download_initial_plugins(self):
await self.download_plugin_repo(username, repo, branch)
except DownloadError as exc:
msg = f"{username}/{repo}@{branch} - {exc}"
- logger.error(msg)
+ logger.error(msg, exc_info=True)
else:
try:
await self.load_plugin(username, repo, name, branch)
except DownloadError as exc:
msg = f"{username}/{repo}@{branch}[{name}] - {exc}"
- logger.error(msg)
+ logger.error(msg, exc_info=True)
async def download_plugin_repo(self, username, repo, branch):
try:
@@ -136,7 +136,7 @@ async def load_plugin(self, username, repo, plugin_name, branch):
# so there's no terminal output unless there's an error
except subprocess.CalledProcessError as exc:
err = exc.stderr.decode("utf8").strip()
-
+ logger.error('Error.', exc_info=True)
if err:
msg = f"Requirements Download Error: {username}/{repo}@{branch}[{plugin_name}]"
logger.error(msg)
@@ -154,7 +154,7 @@ async def load_plugin(self, username, repo, plugin_name, branch):
self.bot.load_extension(ext)
except commands.ExtensionError as exc:
msg = f"Plugin Load Failure: {username}/{repo}@{branch}[{plugin_name}]"
- logger.error(msg)
+ logger.error(msg, exc_info=True)
raise DownloadError("Invalid plugin") from exc
else:
msg = f"Loaded Plugin: {username}/{repo}@{branch}[{plugin_name}]"
@@ -174,9 +174,8 @@ async def plugin_add(self, ctx, *, plugin_name: str):
if plugin_name in self.registry:
details = self.registry[plugin_name]
- plugin_name = (
- details["repository"] + "/" + plugin_name + "@" + details["branch"]
- )
+ plugin_name = details["repository"] + "/" + plugin_name + "@" + details["branch"]
+
required_version = details["bot_version"]
if parse_version(self.bot.version) < parse_version(required_version):
@@ -212,7 +211,9 @@ async def plugin_add(self, ctx, *, plugin_name: str):
try:
await self.download_plugin_repo(username, repo, branch)
- except DownloadError as exc:
+ except Exception as exc:
+ if not isinstance(exc, DownloadError):
+ logger.error('Unknown error when adding a plugin:', exc_info=True)
embed = discord.Embed(
description=f"Unable to fetch this plugin from Github: `{exc}`.",
color=self.bot.main_color,
@@ -224,6 +225,8 @@ async def plugin_add(self, ctx, *, plugin_name: str):
try:
await self.load_plugin(username, repo, name, branch)
except Exception as exc:
+ if not isinstance(exc, DownloadError):
+ logger.error('Unknown error when adding a plugin:', exc_info=True)
embed = discord.Embed(
description=f"Unable to load this plugin: `{exc}`.",
color=self.bot.main_color,
@@ -238,13 +241,15 @@ async def plugin_add(self, ctx, *, plugin_name: str):
embed = discord.Embed(
description="The plugin is installed.\n"
- "*Please note: Any plugin that you install is at your **own risk***",
+ "*Friendly reminder, plugins have absolute control over your bot. "
+ "Please only install plugins from developers you trust.*",
color=self.bot.main_color,
)
await ctx.send(embed=embed)
else:
embed = discord.Embed(
- description="Invalid plugin name format: use username/repo/plugin.",
+ description="Invalid plugin name format: use plugin-name or "
+ "username/repo/plugin or username/repo/plugin@branch.",
color=self.bot.main_color,
)
await ctx.send(embed=embed)
@@ -261,19 +266,17 @@ async def plugin_remove(self, ctx, *, plugin_name: str):
)
if plugin_name in self.bot.config["plugins"]:
+ username, repo, name, branch = self.parse_plugin(plugin_name)
try:
- username, repo, name, branch = self.parse_plugin(plugin_name)
-
self.bot.unload_extension(
f"plugins.{username}-{repo}-{branch}.{name}.{name}"
)
- except Exception:
- pass
+ except commands.ExtensionNotLoaded:
+ logger.error('Plugin was never loaded.')
self.bot.config["plugins"].remove(plugin_name)
try:
- # BUG: Local variables 'username', 'branch' and 'repo' might be referenced before assignment
if not any(
i.startswith(f"{username}/{repo}")
for i in self.bot.config["plugins"]
@@ -289,10 +292,9 @@ def onerror(func, path, exc_info): # pylint: disable=W0613
f"plugins/{username}-{repo}-{branch}", onerror=onerror
)
except Exception as exc:
- logger.error(str(exc))
+ logger.error('Failed to remove plugin %s.', plugin_name, exc_info=True)
self.bot.config["plugins"].append(plugin_name)
- logger.error(exc)
- raise exc
+ return
await self.bot.config.update()
@@ -314,9 +316,7 @@ async def plugin_update(self, ctx, *, plugin_name: str):
if plugin_name in self.registry:
details = self.registry[plugin_name]
- plugin_name = (
- details["repository"] + "/" + plugin_name + "@" + details["branch"]
- )
+ plugin_name = details["repository"] + "/" + plugin_name + "@" + details["branch"]
if plugin_name not in self.bot.config["plugins"]:
embed = discord.Embed(
@@ -328,7 +328,8 @@ async def plugin_update(self, ctx, *, plugin_name: str):
username, repo, name, branch = self.parse_plugin(plugin_name)
try:
- cmd = f"cd plugins/{username}-{repo}-{branch} && git reset --hard origin/{branch} && git fetch --all && git pull"
+ cmd = f"cd plugins/{username}-{repo}-{branch} && " \
+ f"git reset --hard origin/{branch} && git fetch --all && git pull"
cmd = await self.bot.loop.run_in_executor(
None, self._asubprocess_run, cmd
)
@@ -339,6 +340,7 @@ async def plugin_update(self, ctx, *, plugin_name: str):
description=f"An error occurred while updating: {err}.",
color=self.bot.main_color,
)
+ logger.error('An error occurred while updating plugin:', exc_info=True)
await ctx.send(embed=embed)
else:
@@ -357,11 +359,12 @@ async def plugin_update(self, ctx, *, plugin_name: str):
try:
await self.load_plugin(username, repo, name, branch)
except DownloadError as exc:
- em = discord.Embed(
+ embed = discord.Embed(
description=f"Unable to start the plugin: `{exc}`.",
color=self.bot.main_color,
)
- await ctx.send(embed=em)
+ logger.error('An error occurred while updating plugin:', exc_info=True)
+ await ctx.send(embed=embed)
@plugin.command(name="enabled", aliases=["installed"])
@checks.has_permissions(PermissionLevel.OWNER)
@@ -369,7 +372,7 @@ async def plugin_enabled(self, ctx):
"""Shows a list of currently enabled plugins."""
if self.bot.config["plugins"]:
- msg = "```\n" + "\n".join(self.bot.config["plugins"]) + "\n```"
+ msg = "```\n" + "\n".join(sorted(self.bot.config["plugins"])) + "\n```"
embed = discord.Embed(description=msg, color=self.bot.main_color)
await ctx.send(embed=embed)
else:
@@ -382,19 +385,32 @@ async def plugin_enabled(self, ctx):
invoke_without_command=True, name="registry", aliases=["list", "info"]
)
@checks.has_permissions(PermissionLevel.OWNER)
- async def plugin_registry(self, ctx, *, plugin_name: str = None):
- """Shows a list of all approved plugins."""
+ async def plugin_registry(self, ctx, *, plugin_name: typing.Union[int, str] = None):
+ """
+ Shows a list of all approved plugins.
+
+ Usage:
+ `{prefix}plugin registry` Details about all plugins.
+ `{prefix}plugin registry plugin-name` Details about the indicated plugin.
+ `{prefix}plugin registry page-number` Jump to a page in the registry.
+ """
await self.populate_registry()
embeds = []
- registry = list(self.registry.items())
- random.shuffle(registry)
+ registry = sorted(self.registry.items(), key=lambda elem: elem[0])
- index = next((i for i, (n, _) in enumerate(registry) if plugin_name == n), None)
+ if isinstance(plugin_name, int):
+ index = plugin_name - 1
+ if index < 0:
+ index = 0
+ if index >= len(registry):
+ index = len(registry) - 1
+ else:
+ index = next((i for i, (n, _) in enumerate(registry) if plugin_name == n), 0)
- if index is None:
+ if not index and plugin_name is not None:
embed = discord.Embed(
color=discord.Color.red(),
description=f'Could not find a plugin with name "{plugin_name}" within the registry.',
@@ -440,13 +456,13 @@ async def plugin_registry(self, ctx, *, plugin_name: str = None):
await paginator.run()
@plugin_registry.command(name="compact")
+ @checks.has_permissions(PermissionLevel.OWNER)
async def plugin_registry_compact(self, ctx):
"""Shows a compact view of all plugins within the registry."""
await self.populate_registry()
- registry = list(self.registry.items())
- registry.sort(key=lambda elem: elem[0])
+ registry = sorted(self.registry.items(), key=lambda elem: elem[0])
pages = [""]
diff --git a/cogs/utility.py b/cogs/utility.py
index ee7b7c9493..9f07d68e57 100644
--- a/cogs/utility.py
+++ b/cogs/utility.py
@@ -555,7 +555,7 @@ async def set_presence(
if activity_identifier is None:
if activity_message is not None:
raise ValueError(
- "activity_message must be None " "if activity_identifier is None."
+ "activity_message must be None if activity_identifier is None."
)
activity_identifier = self.bot.config["activity_type"]
activity_by_key = False
diff --git a/core/changelog.py b/core/changelog.py
index e3f9424546..ec12a89838 100644
--- a/core/changelog.py
+++ b/core/changelog.py
@@ -1,9 +1,12 @@
+import logging
import re
from collections import defaultdict
from typing import List, Union
from discord import Embed, Color
+logger = logging.getLogger("Modmail")
+
class Version:
"""
@@ -117,11 +120,12 @@ class Changelog:
"https://raw.githubusercontent.com/kyb3r/modmail/master/CHANGELOG.md"
)
CHANGELOG_URL = "https://github.com/kyb3r/modmail/blob/master/CHANGELOG.md"
- VERSION_REGEX = re.compile(r"# (v\d+\.\d+\.\d+)([\S\s]*?(?=# v|$))")
+ VERSION_REGEX = re.compile(r"# ([vV]\d+\.\d+(?:\.\d+)?)(.*?(?=# (?:[vV]\d+\.\d+(?:\.\d+)?)|$))", flags=re.DOTALL)
def __init__(self, bot, text: str):
self.bot = bot
self.text = text
+ logger.debug('Fetching changelog from GitHub.')
self.versions = [Version(bot, *m) for m in self.VERSION_REGEX.findall(text)]
@property
diff --git a/core/clients.py b/core/clients.py
index 88ffbd8743..4b8b578cc4 100644
--- a/core/clients.py
+++ b/core/clients.py
@@ -87,15 +87,18 @@ def logs(self):
async def get_user_logs(self, user_id: Union[str, int]) -> list:
query = {"recipient.id": str(user_id), "guild_id": str(self.bot.guild_id)}
-
projection = {"messages": {"$slice": 5}}
+ logger.debug('Retrieving user %s logs.', user_id)
+
return await self.logs.find(query, projection).to_list(None)
async def get_log(self, channel_id: Union[str, int]) -> dict:
+ logger.debug('Retrieving channel %s logs.', channel_id)
return await self.logs.find_one({"channel_id": str(channel_id)})
async def get_log_link(self, channel_id: Union[str, int]) -> str:
doc = await self.get_log(channel_id)
+ logger.debug('Retrieving log link for channel %s.', channel_id)
return f"{self.bot.config['log_url'].strip('/')}{self.bot.config['log_url_prefix']}/{doc['key']}"
async def create_log_entry(
@@ -131,12 +134,13 @@ async def create_log_entry(
"messages": [],
}
)
-
+ logger.debug('Created a log entry, key %s.', key)
return f"{self.bot.config['log_url'].strip('/')}{self.bot.config['log_url_prefix']}/{key}"
async def get_config(self) -> dict:
conf = await self.db.config.find_one({"bot_id": self.bot.user.id})
if conf is None:
+ logger.debug('Creating a new config entry for bot %s.', self.bot.user.id)
await self.db.config.insert_one({"bot_id": self.bot.user.id})
return {"bot_id": self.bot.user.id}
return conf
diff --git a/core/config.py b/core/config.py
index 5f7eda1ea1..c54165dc93 100644
--- a/core/config.py
+++ b/core/config.py
@@ -115,16 +115,13 @@ class ConfigManager:
def __init__(self, bot):
self.bot = bot
+ self.api = self.bot.api
self._cache = {}
self.ready_event = asyncio.Event()
def __repr__(self):
return repr(self._cache)
- @property
- def api(self):
- return self.bot.api
-
def populate_cache(self) -> dict:
data = deepcopy(self.defaults)
@@ -132,17 +129,21 @@ def populate_cache(self) -> dict:
data.update(
{k.lower(): v for k, v in os.environ.items() if k.lower() in self.all_keys}
)
-
- if os.path.exists("config.json"):
- with open("config.json") as f:
+ configjson = os.path.join(os.path.dirname(os.path.abspath(__file__)), "config.json")
+ if os.path.exists(configjson):
+ logger.debug('Loading envs from config.json.')
+ with open(configjson, 'r') as f:
# Config json should override env vars
- data.update(
- {
- k.lower(): v
- for k, v in json.load(f).items()
- if k.lower() in self.all_keys
- }
- )
+ try:
+ data.update(
+ {
+ k.lower(): v
+ for k, v in json.load(f).items()
+ if k.lower() in self.all_keys
+ }
+ )
+ except json.JSONDecodeError:
+ logger.critical('Failed to load config.json env values.', exc_info=True)
self._cache = data
return self._cache
@@ -212,7 +213,7 @@ async def refresh(self) -> dict:
self._cache[k] = v
if not self.ready_event.is_set():
self.ready_event.set()
- logger.info("Config ready.")
+ logger.info("Successfully fetched configurations from database.")
return self._cache
async def wait_until_ready(self) -> None:
@@ -238,12 +239,10 @@ def get(self, key: str, default: typing.Any = Default) -> typing.Any:
if key not in self.all_keys:
raise InvalidConfigError(f'Configuration "{key}" is invalid.')
if key not in self._cache:
- self._cache[key] = deepcopy(self.defaults[key])
if default is Default:
- return self._cache[key]
- return default
- if self._cache[key] == self.defaults[key] and default is not Default:
- return default
+ self._cache[key] = deepcopy(self.defaults[key])
+ if default is not Default and self._cache[key] == self.defaults[key]:
+ self._cache[key] = default
return self._cache[key]
def set(self, key: str, item: typing.Any) -> None:
@@ -279,4 +278,12 @@ def filter_default(
cls, data: typing.Dict[str, typing.Any]
) -> typing.Dict[str, typing.Any]:
# TODO: use .get to prevent errors
- return {k.lower(): v for k, v in data.items() if v != cls.defaults[k.lower()]}
+ filtered = {}
+ for k, v in data.items():
+ default = cls.defaults.get(k.lower(), Default)
+ if default is Default:
+ logger.error('Unexpected configuration detected: %s.', k)
+ continue
+ if v != default:
+ filtered[k.lower()] = v
+ return filtered
diff --git a/core/models.py b/core/models.py
index 34c79da1c5..58ef344115 100644
--- a/core/models.py
+++ b/core/models.py
@@ -46,7 +46,8 @@ def _error_(*msgs):
return f'{Fore.RED}{" ".join(msgs)}{Style.RESET_ALL}'
def debug(self, msg, *args, **kwargs):
- return super().debug(self._debug_(msg), *args, **kwargs)
+ if self.isEnabledFor(logging.DEBUG):
+ self._log(logging.DEBUG, self._debug_(msg), args, **kwargs)
def info(self, msg, *args, **kwargs):
if self.isEnabledFor(logging.INFO):
diff --git a/core/paginator.py b/core/paginator.py
index 58fdcaea20..2fb72da8ff 100644
--- a/core/paginator.py
+++ b/core/paginator.py
@@ -194,8 +194,6 @@ async def close(self, delete: bool = True) -> typing.Optional[Message]:
"""
self.running = False
- self.ctx.bot.loop.create_task(self.ctx.message.add_reaction("✅"))
-
if delete:
return await self.base.delete()
@@ -204,6 +202,12 @@ async def close(self, delete: bool = True) -> typing.Optional[Message]:
except HTTPException:
pass
+ sent_emoji, _ = await self.ctx.bot.retrieve_emoji()
+ try:
+ await self.ctx.message.add_reaction(sent_emoji)
+ except (HTTPException, InvalidArgument):
+ pass
+
async def first_page(self) -> None:
"""
Go to the first page.
diff --git a/core/thread.py b/core/thread.py
index 04e506f7ae..73248d915f 100644
--- a/core/thread.py
+++ b/core/thread.py
@@ -72,6 +72,7 @@ def ready(self) -> bool:
def ready(self, flag: bool):
if flag:
self._ready_event.set()
+ self.bot.dispatch("thread_ready", self)
else:
self._ready_event.clear()
@@ -102,6 +103,7 @@ async def setup(self, *, creator=None, category=None):
reason="Creating a thread channel.",
)
except discord.HTTPException as e: # Failed to create due to 50 channel limit.
+ logger.critical('An error occurred while creating a thread.', exc_info=True)
self.manager.cache.pop(self.id)
embed = discord.Embed(color=discord.Color.red())
@@ -122,7 +124,8 @@ async def setup(self, *, creator=None, category=None):
)
log_count = sum(1 for log in log_data if not log["open"])
- except Exception: # Something went wrong with database?
+ except Exception:
+ logger.error('An error occurred while posting logs to the database.', exc_info=True)
log_url = log_count = None
# ensure core functionality still works
@@ -132,18 +135,17 @@ async def setup(self, *, creator=None, category=None):
mention = self.bot.config["mention"]
async def send_genesis_message():
- info_embed = self.manager.format_info_embed(
+ info_embed = self._format_info_embed(
recipient, log_url, log_count, discord.Color.green()
)
try:
msg = await channel.send(mention, embed=info_embed)
self.bot.loop.create_task(msg.pin())
self.genesis_message = msg
- except Exception:
- pass
+ except Exception as e:
+ logger.error(str(e))
finally:
self.ready = True
- self.bot.dispatch("thread_ready", self)
await channel.edit(topic=f"User ID: {recipient.id}")
self.bot.loop.create_task(send_genesis_message())
@@ -180,6 +182,75 @@ async def send_genesis_message():
close_emoji = await self.bot.convert_emoji(close_emoji)
await msg.add_reaction(close_emoji)
+ def _format_info_embed(self, user, log_url, log_count, color):
+ """Get information about a member of a server
+ supports users from the guild or not."""
+ member = self.bot.guild.get_member(user.id)
+ time = datetime.utcnow()
+
+ # key = log_url.split('/')[-1]
+
+ role_names = ""
+ if member is None:
+ sep_server = self.bot.using_multiple_server_setup
+ separator = ", " if sep_server else " "
+
+ roles = []
+
+ for role in sorted(member.roles, key=lambda r: r.position):
+ if role.name == "@everyone":
+ continue
+
+ fmt = role.name if sep_server else role.mention
+ roles.append(fmt)
+
+ if len(separator.join(roles)) > 1024:
+ roles.append("...")
+ while len(separator.join(roles)) > 1024:
+ roles.pop(-2)
+ break
+
+ role_names = separator.join(roles)
+
+ created = str((time - user.created_at).days)
+ embed = discord.Embed(color=color, description=f"{user.mention} was created {days(created)}", timestamp=time)
+
+ # if not role_names:
+ # embed.add_field(name='Mention', value=user.mention)
+ # embed.add_field(name='Registered', value=created + days(created))
+
+ footer = "User ID: " + str(user.id)
+ embed.set_author(name=str(user), icon_url=user.avatar_url, url=log_url)
+ # embed.set_thumbnail(url=avi)
+
+ if member is None:
+ joined = str((time - member.joined_at).days)
+ # embed.add_field(name='Joined', value=joined + days(joined))
+ embed.description += f", joined {days(joined)}"
+
+ if member.nick:
+ embed.add_field(name="Nickname", value=member.nick, inline=True)
+ if role_names:
+ embed.add_field(name="Roles", value=role_names, inline=True)
+ embed.set_footer(text=footer)
+ else:
+ embed.set_footer(text=f"{footer} • (not in main server)")
+
+ if log_count is not None:
+ # embed.add_field(name='Past logs', value=f'{log_count}')
+ thread = "thread" if log_count == 1 else "threads"
+ embed.description += f" with **{log_count or 'no'}** past {thread}."
+ else:
+ embed.description += "."
+
+ mutual_guilds = [g for g in self.bot.guilds if user in g.members]
+ if member is None or len(mutual_guilds) > 1:
+ embed.add_field(
+ name="Mutual Server(s)", value=", ".join(g.name for g in mutual_guilds)
+ )
+
+ return embed
+
def _close_after(self, closer, silent, delete_channel, message):
return self.bot.loop.create_task(
self._close(closer, silent, delete_channel, message, True)
@@ -203,7 +274,6 @@ async def close(
if after > 0:
# TODO: Add somewhere to clean up broken closures
# (when channel is already deleted)
- await self.bot.config.update()
now = datetime.utcnow()
items = {
# 'initiation_time': now.isoformat(),
@@ -261,7 +331,7 @@ async def _close(
},
)
- if log_data is not None and isinstance(log_data, dict):
+ if isinstance(log_data, dict):
prefix = self.bot.config["log_url_prefix"]
if prefix == "NONE":
prefix = ""
@@ -475,7 +545,7 @@ async def note(self, message: discord.Message) -> None:
async def reply(self, message: discord.Message, anonymous: bool = False) -> None:
if not message.content and not message.attachments:
raise MissingRequiredArgument(param(name="msg"))
- if all(not g.get_member(self.id) for g in self.bot.guilds):
+ if not any(g.get_member(self.id) for g in self.bot.guilds):
return await message.channel.send(
embed=discord.Embed(
color=discord.Color.red(),
@@ -853,77 +923,8 @@ def format_channel_name(self, author):
)
new_name += f"-{author.discriminator}"
+ counter = 1
while new_name in [c.name for c in self.bot.modmail_guild.text_channels]:
- new_name += "-x" # two channels with same name
+ new_name += f'-{counter}' # two channels with same name
return new_name
-
- def format_info_embed(self, user, log_url, log_count, color):
- """Get information about a member of a server
- supports users from the guild or not."""
- member = self.bot.guild.get_member(user.id)
- time = datetime.utcnow()
-
- # key = log_url.split('/')[-1]
-
- role_names = ""
- if member:
- sep_server = self.bot.using_multiple_server_setup
- separator = ", " if sep_server else " "
-
- roles = []
-
- for role in sorted(member.roles, key=lambda r: r.position):
- if role.name == "@everyone":
- continue
-
- fmt = role.name if sep_server else role.mention
- roles.append(fmt)
-
- if len(separator.join(roles)) > 1024:
- roles.append("...")
- while len(separator.join(roles)) > 1024:
- roles.pop(-2)
- break
-
- role_names = separator.join(roles)
-
- embed = discord.Embed(color=color, description=user.mention, timestamp=time)
-
- created = str((time - user.created_at).days)
- # if not role_names:
- # embed.add_field(name='Mention', value=user.mention)
- # embed.add_field(name='Registered', value=created + days(created))
- embed.description += f" was created {days(created)}"
-
- footer = "User ID: " + str(user.id)
- embed.set_footer(text=footer)
- embed.set_author(name=str(user), icon_url=user.avatar_url, url=log_url)
- # embed.set_thumbnail(url=avi)
-
- if member:
- joined = str((time - member.joined_at).days)
- # embed.add_field(name='Joined', value=joined + days(joined))
- embed.description += f", joined {days(joined)}"
-
- if member.nick:
- embed.add_field(name="Nickname", value=member.nick, inline=True)
- if role_names:
- embed.add_field(name="Roles", value=role_names, inline=True)
- else:
- embed.set_footer(text=f"{footer} • (not in main server)")
-
- if log_count:
- # embed.add_field(name='Past logs', value=f'{log_count}')
- thread = "thread" if log_count == 1 else "threads"
- embed.description += f" with **{log_count}** past {thread}."
- else:
- embed.description += "."
-
- mutual_guilds = [g for g in self.bot.guilds if user in g.members]
- if user not in self.bot.guild.members or len(mutual_guilds) > 1:
- embed.add_field(
- name="Mutual Servers", value=", ".join(g.name for g in mutual_guilds)
- )
-
- return embed
diff --git a/core/time.py b/core/time.py
index da9bdafcb4..5e8049567a 100644
--- a/core/time.py
+++ b/core/time.py
@@ -32,7 +32,7 @@ class ShortTime:
def __init__(self, argument):
match = self.compiled.fullmatch(argument)
if match is None or not match.group(0):
- raise BadArgument("invalid time provided")
+ raise BadArgument("Invalid time provided.")
data = {k: int(v) for k, v in match.groupdict(default="0").items()}
now = datetime.utcnow()
@@ -52,7 +52,7 @@ def __init__(self, argument):
now = datetime.utcnow()
dt, status = self.calendar.parseDT(argument, sourceTime=now)
if not status.hasDateOrTime:
- raise BadArgument('invalid time provided, try e.g. "tomorrow" or "3 days"')
+ raise BadArgument('Invalid time provided, try e.g. "tomorrow" or "3 days".')
if not status.hasTime:
# replace it with the current time
@@ -83,7 +83,7 @@ def __init__(self, argument):
super().__init__(argument)
if self._past:
- raise BadArgument("this time is in the past")
+ raise BadArgument("The time is in the past.")
class UserFriendlyTime(Converter):
From 8f7d020021c7217a0f11deb11936bd2986d68fa0 Mon Sep 17 00:00:00 2001
From: Taaku18 <45324516+Taaku18@users.noreply.github.com>
Date: Sun, 21 Jul 2019 12:15:41 -0700
Subject: [PATCH 0330/1221] Black/lint
---
bot.py | 64 ++++++++++++++++++++++++++++++-----------------
cogs/plugins.py | 40 +++++++++++++++++++----------
cogs/utility.py | 2 ++
core/changelog.py | 7 ++++--
core/clients.py | 10 ++++----
core/config.py | 14 +++++++----
core/thread.py | 18 ++++++++-----
7 files changed, 101 insertions(+), 54 deletions(-)
diff --git a/bot.py b/bot.py
index b6d9d39ebe..132ae4c913 100644
--- a/bot.py
+++ b/bot.py
@@ -86,7 +86,9 @@ def __init__(self):
try:
self.db = AsyncIOMotorClient(mongo_uri).modmail_bot
except ConfigurationError as e:
- logger.critical("Your MONGO_URI is copied wrong, try re-copying from the source again.")
+ logger.critical(
+ "Your MONGO_URI is copied wrong, try re-copying from the source again."
+ )
logger.critical(str(e))
sys.exit(0)
@@ -213,7 +215,9 @@ def owner_ids(self):
owner_ids = set(map(int, str(owner_ids).split(",")))
if self.owner_id is not None:
owner_ids.add(self.owner_id)
- permissions = self.config["level_permissions"].get(PermissionLevel.OWNER.name, [])
+ permissions = self.config["level_permissions"].get(
+ PermissionLevel.OWNER.name, []
+ )
for perm in permissions:
owner_ids.add(int(perm))
return owner_ids
@@ -230,13 +234,16 @@ def log_channel(self) -> typing.Optional[discord.TextChannel]:
channel = self.get_channel(int(channel_id))
if channel is not None:
return channel
- logger.debug('LOG_CHANNEL_ID was invalid, removed.')
+ logger.debug("LOG_CHANNEL_ID was invalid, removed.")
self.config.remove("log_channel_id")
if self.main_category is not None:
try:
channel = self.main_category.channels[0]
self.config["log_channel_id"] = channel.id
- logger.warning("No log channel set, setting #%s to be the log channel.", channel.name)
+ logger.warning(
+ "No log channel set, setting #%s to be the log channel.",
+ channel.name,
+ )
return channel
except IndexError:
pass
@@ -268,7 +275,9 @@ def aliases(self) -> typing.Dict[str, str]:
def token(self) -> str:
token = self.config["token"]
if token is None:
- logger.critical("TOKEN must be set, set this as bot token found on the Discord Dev Portal.")
+ logger.critical(
+ "TOKEN must be set, set this as bot token found on the Discord Dev Portal."
+ )
sys.exit(0)
return token
@@ -321,11 +330,13 @@ def main_category(self) -> typing.Optional[discord.CategoryChannel]:
if cat is not None:
return cat
self.config.remove("main_category_id")
- logger.debug('MAIN_CATEGORY_ID was invalid, removed.')
+ logger.debug("MAIN_CATEGORY_ID was invalid, removed.")
cat = discord.utils.get(self.modmail_guild.categories, name="Modmail")
if cat is not None:
self.config["main_category_id"] = cat.id
- logger.debug("No main category set explicitly, setting category \"Modmail\" as the main category.")
+ logger.debug(
+ 'No main category set explicitly, setting category "Modmail" as the main category.'
+ )
return cat
return None
@@ -404,7 +415,7 @@ async def setup_indexes(self):
("key", "text"),
]
)
- logger.debug('Successfully set up database indexes.')
+ logger.debug("Successfully set up database indexes.")
async def on_ready(self):
"""Bot startup, sets uptime."""
@@ -413,7 +424,7 @@ async def on_ready(self):
await self.wait_for_connected()
if self.guild is None:
- logger.debug('Logging out due to invalid GUILD_ID.')
+ logger.debug("Logging out due to invalid GUILD_ID.")
return await self.logout()
logger.line()
@@ -444,12 +455,12 @@ async def on_ready(self):
if not thread:
# If the channel is deleted
- logger.debug('Failed to close thread for recipient %s.', recipient_id)
+ logger.debug("Failed to close thread for recipient %s.", recipient_id)
self.config["closures"].pop(recipient_id)
await self.config.update()
continue
- logger.debug('Closing thread for recipient %s.', recipient_id)
+ logger.debug("Closing thread for recipient %s.", recipient_id)
await thread.close(
closer=self.get_user(items["closer_id"]),
@@ -579,7 +590,7 @@ async def _process_blocked(self, message: discord.Message) -> bool:
reaction = blocked_emoji
changed = False
delta = human_timedelta(min_account_age)
- logger.debug('Blocked due to account age, user %s.', message.author.name)
+ logger.debug("Blocked due to account age, user %s.", message.author.name)
if str(message.author.id) not in self.blocked_users:
new_reason = (
@@ -603,7 +614,7 @@ async def _process_blocked(self, message: discord.Message) -> bool:
reaction = blocked_emoji
changed = False
delta = human_timedelta(min_guild_age)
- logger.debug('Blocked due to guild age, user %s.', message.author.name)
+ logger.debug("Blocked due to guild age, user %s.", message.author.name)
if str(message.author.id) not in self.blocked_users:
new_reason = (
@@ -628,13 +639,15 @@ async def _process_blocked(self, message: discord.Message) -> bool:
):
# Met the age limit already, otherwise it would've been caught by the previous if's
reaction = sent_emoji
- logger.debug('No longer internally blocked, user %s.', message.author.name)
+ logger.debug(
+ "No longer internally blocked, user %s.", message.author.name
+ )
self.blocked_users.pop(str(message.author.id))
else:
reaction = blocked_emoji
end_time = re.search(r"%(.+?)%$", reason)
if end_time is not None:
- logger.debug('No longer blocked, user %s.', message.author.name)
+ logger.debug("No longer blocked, user %s.", message.author.name)
after = (
datetime.fromisoformat(end_time.group(1)) - now
).total_seconds()
@@ -643,7 +656,7 @@ async def _process_blocked(self, message: discord.Message) -> bool:
reaction = sent_emoji
self.blocked_users.pop(str(message.author.id))
else:
- logger.debug('User blocked, user %s.', message.author.name)
+ logger.debug("User blocked, user %s.", message.author.name)
else:
reaction = sent_emoji
@@ -652,7 +665,7 @@ async def _process_blocked(self, message: discord.Message) -> bool:
try:
await message.add_reaction(reaction)
except (discord.HTTPException, discord.InvalidArgument):
- logger.warning('Failed to add reaction %s.', reaction, exc_info=True)
+ logger.warning("Failed to add reaction %s.", reaction, exc_info=True)
return str(message.author.id) in self.blocked_users
async def process_modmail(self, message: discord.Message) -> None:
@@ -839,9 +852,13 @@ async def on_raw_reaction_add(self, payload):
if isinstance(channel, discord.DMChannel):
if str(reaction) == str(close_emoji): # closing thread
try:
- recipient_thread_close = strtobool(self.config["recipient_thread_close"])
+ recipient_thread_close = strtobool(
+ self.config["recipient_thread_close"]
+ )
except ValueError:
- recipient_thread_close = self.config.remove("recipient_thread_close")
+ recipient_thread_close = self.config.remove(
+ "recipient_thread_close"
+ )
if not recipient_thread_close:
return
thread = await self.threads.find(recipient=user)
@@ -875,7 +892,7 @@ async def on_guild_channel_delete(self, channel):
if isinstance(channel, discord.CategoryChannel):
if self.main_category.id == channel.id:
- logger.debug('Main category was deleted.')
+ logger.debug("Main category was deleted.")
self.config.remove("main_category_id")
await self.config.update()
return
@@ -884,14 +901,14 @@ async def on_guild_channel_delete(self, channel):
return
if self.log_channel is None or self.log_channel.id == channel.id:
- logger.info('Log channel deleted.')
+ logger.info("Log channel deleted.")
self.config.remove("log_channel_id")
await self.config.update()
return
thread = await self.threads.find(channel=channel)
if thread:
- logger.debug('Manually closed channel %s.', channel.name)
+ logger.debug("Manually closed channel %s.", channel.name)
await thread.close(closer=mod, silent=True, delete_channel=False)
async def on_member_remove(self, member):
@@ -1062,7 +1079,8 @@ async def after_post_metadata(self):
if __name__ == "__main__":
try:
import uvloop
- logger.debug('Setting up with uvloop.')
+
+ logger.debug("Setting up with uvloop.")
uvloop.install()
except ImportError:
pass
diff --git a/cogs/plugins.py b/cogs/plugins.py
index 5f58c697f7..4f204ef10e 100644
--- a/cogs/plugins.py
+++ b/cogs/plugins.py
@@ -136,7 +136,7 @@ async def load_plugin(self, username, repo, plugin_name, branch):
# so there's no terminal output unless there's an error
except subprocess.CalledProcessError as exc:
err = exc.stderr.decode("utf8").strip()
- logger.error('Error.', exc_info=True)
+ logger.error("Error.", exc_info=True)
if err:
msg = f"Requirements Download Error: {username}/{repo}@{branch}[{plugin_name}]"
logger.error(msg)
@@ -174,7 +174,9 @@ async def plugin_add(self, ctx, *, plugin_name: str):
if plugin_name in self.registry:
details = self.registry[plugin_name]
- plugin_name = details["repository"] + "/" + plugin_name + "@" + details["branch"]
+ plugin_name = (
+ details["repository"] + "/" + plugin_name + "@" + details["branch"]
+ )
required_version = details["bot_version"]
@@ -213,7 +215,9 @@ async def plugin_add(self, ctx, *, plugin_name: str):
await self.download_plugin_repo(username, repo, branch)
except Exception as exc:
if not isinstance(exc, DownloadError):
- logger.error('Unknown error when adding a plugin:', exc_info=True)
+ logger.error(
+ "Unknown error when adding a plugin:", exc_info=True
+ )
embed = discord.Embed(
description=f"Unable to fetch this plugin from Github: `{exc}`.",
color=self.bot.main_color,
@@ -226,7 +230,9 @@ async def plugin_add(self, ctx, *, plugin_name: str):
await self.load_plugin(username, repo, name, branch)
except Exception as exc:
if not isinstance(exc, DownloadError):
- logger.error('Unknown error when adding a plugin:', exc_info=True)
+ logger.error(
+ "Unknown error when adding a plugin:", exc_info=True
+ )
embed = discord.Embed(
description=f"Unable to load this plugin: `{exc}`.",
color=self.bot.main_color,
@@ -249,7 +255,7 @@ async def plugin_add(self, ctx, *, plugin_name: str):
else:
embed = discord.Embed(
description="Invalid plugin name format: use plugin-name or "
- "username/repo/plugin or username/repo/plugin@branch.",
+ "username/repo/plugin or username/repo/plugin@branch.",
color=self.bot.main_color,
)
await ctx.send(embed=embed)
@@ -272,7 +278,7 @@ async def plugin_remove(self, ctx, *, plugin_name: str):
f"plugins.{username}-{repo}-{branch}.{name}.{name}"
)
except commands.ExtensionNotLoaded:
- logger.error('Plugin was never loaded.')
+ logger.error("Plugin was never loaded.")
self.bot.config["plugins"].remove(plugin_name)
@@ -291,8 +297,8 @@ def onerror(func, path, exc_info): # pylint: disable=W0613
shutil.rmtree(
f"plugins/{username}-{repo}-{branch}", onerror=onerror
)
- except Exception as exc:
- logger.error('Failed to remove plugin %s.', plugin_name, exc_info=True)
+ except Exception:
+ logger.error("Failed to remove plugin %s.", plugin_name, exc_info=True)
self.bot.config["plugins"].append(plugin_name)
return
@@ -316,7 +322,9 @@ async def plugin_update(self, ctx, *, plugin_name: str):
if plugin_name in self.registry:
details = self.registry[plugin_name]
- plugin_name = details["repository"] + "/" + plugin_name + "@" + details["branch"]
+ plugin_name = (
+ details["repository"] + "/" + plugin_name + "@" + details["branch"]
+ )
if plugin_name not in self.bot.config["plugins"]:
embed = discord.Embed(
@@ -328,8 +336,10 @@ async def plugin_update(self, ctx, *, plugin_name: str):
username, repo, name, branch = self.parse_plugin(plugin_name)
try:
- cmd = f"cd plugins/{username}-{repo}-{branch} && " \
+ cmd = (
+ f"cd plugins/{username}-{repo}-{branch} && "
f"git reset --hard origin/{branch} && git fetch --all && git pull"
+ )
cmd = await self.bot.loop.run_in_executor(
None, self._asubprocess_run, cmd
)
@@ -340,7 +350,7 @@ async def plugin_update(self, ctx, *, plugin_name: str):
description=f"An error occurred while updating: {err}.",
color=self.bot.main_color,
)
- logger.error('An error occurred while updating plugin:', exc_info=True)
+ logger.error("An error occurred while updating plugin:", exc_info=True)
await ctx.send(embed=embed)
else:
@@ -363,7 +373,9 @@ async def plugin_update(self, ctx, *, plugin_name: str):
description=f"Unable to start the plugin: `{exc}`.",
color=self.bot.main_color,
)
- logger.error('An error occurred while updating plugin:', exc_info=True)
+ logger.error(
+ "An error occurred while updating plugin:", exc_info=True
+ )
await ctx.send(embed=embed)
@plugin.command(name="enabled", aliases=["installed"])
@@ -408,7 +420,9 @@ async def plugin_registry(self, ctx, *, plugin_name: typing.Union[int, str] = No
if index >= len(registry):
index = len(registry) - 1
else:
- index = next((i for i, (n, _) in enumerate(registry) if plugin_name == n), 0)
+ index = next(
+ (i for i, (n, _) in enumerate(registry) if plugin_name == n), 0
+ )
if not index and plugin_name is not None:
embed = discord.Embed(
diff --git a/cogs/utility.py b/cogs/utility.py
index 9f07d68e57..36057ee294 100644
--- a/cogs/utility.py
+++ b/cogs/utility.py
@@ -246,6 +246,8 @@ async def changelog(self, ctx, version: str.lower = ""):
paginator = PaginatorSession(ctx, *changelog.embeds)
paginator.current = index
await paginator.run()
+ except asyncio.CancelledError:
+ pass
except Exception:
await ctx.send(changelog.CHANGELOG_URL)
diff --git a/core/changelog.py b/core/changelog.py
index ec12a89838..386baf176f 100644
--- a/core/changelog.py
+++ b/core/changelog.py
@@ -120,12 +120,15 @@ class Changelog:
"https://raw.githubusercontent.com/kyb3r/modmail/master/CHANGELOG.md"
)
CHANGELOG_URL = "https://github.com/kyb3r/modmail/blob/master/CHANGELOG.md"
- VERSION_REGEX = re.compile(r"# ([vV]\d+\.\d+(?:\.\d+)?)(.*?(?=# (?:[vV]\d+\.\d+(?:\.\d+)?)|$))", flags=re.DOTALL)
+ VERSION_REGEX = re.compile(
+ r"# ([vV]\d+\.\d+(?:\.\d+)?)(.*?(?=# (?:[vV]\d+\.\d+(?:\.\d+)?)|$))",
+ flags=re.DOTALL,
+ )
def __init__(self, bot, text: str):
self.bot = bot
self.text = text
- logger.debug('Fetching changelog from GitHub.')
+ logger.debug("Fetching changelog from GitHub.")
self.versions = [Version(bot, *m) for m in self.VERSION_REGEX.findall(text)]
@property
diff --git a/core/clients.py b/core/clients.py
index 4b8b578cc4..a4ed847622 100644
--- a/core/clients.py
+++ b/core/clients.py
@@ -88,17 +88,17 @@ def logs(self):
async def get_user_logs(self, user_id: Union[str, int]) -> list:
query = {"recipient.id": str(user_id), "guild_id": str(self.bot.guild_id)}
projection = {"messages": {"$slice": 5}}
- logger.debug('Retrieving user %s logs.', user_id)
+ logger.debug("Retrieving user %s logs.", user_id)
return await self.logs.find(query, projection).to_list(None)
async def get_log(self, channel_id: Union[str, int]) -> dict:
- logger.debug('Retrieving channel %s logs.', channel_id)
+ logger.debug("Retrieving channel %s logs.", channel_id)
return await self.logs.find_one({"channel_id": str(channel_id)})
async def get_log_link(self, channel_id: Union[str, int]) -> str:
doc = await self.get_log(channel_id)
- logger.debug('Retrieving log link for channel %s.', channel_id)
+ logger.debug("Retrieving log link for channel %s.", channel_id)
return f"{self.bot.config['log_url'].strip('/')}{self.bot.config['log_url_prefix']}/{doc['key']}"
async def create_log_entry(
@@ -134,13 +134,13 @@ async def create_log_entry(
"messages": [],
}
)
- logger.debug('Created a log entry, key %s.', key)
+ logger.debug("Created a log entry, key %s.", key)
return f"{self.bot.config['log_url'].strip('/')}{self.bot.config['log_url_prefix']}/{key}"
async def get_config(self) -> dict:
conf = await self.db.config.find_one({"bot_id": self.bot.user.id})
if conf is None:
- logger.debug('Creating a new config entry for bot %s.', self.bot.user.id)
+ logger.debug("Creating a new config entry for bot %s.", self.bot.user.id)
await self.db.config.insert_one({"bot_id": self.bot.user.id})
return {"bot_id": self.bot.user.id}
return conf
diff --git a/core/config.py b/core/config.py
index c54165dc93..301ab69f61 100644
--- a/core/config.py
+++ b/core/config.py
@@ -129,10 +129,12 @@ def populate_cache(self) -> dict:
data.update(
{k.lower(): v for k, v in os.environ.items() if k.lower() in self.all_keys}
)
- configjson = os.path.join(os.path.dirname(os.path.abspath(__file__)), "config.json")
+ configjson = os.path.join(
+ os.path.dirname(os.path.abspath(__file__)), "config.json"
+ )
if os.path.exists(configjson):
- logger.debug('Loading envs from config.json.')
- with open(configjson, 'r') as f:
+ logger.debug("Loading envs from config.json.")
+ with open(configjson, "r") as f:
# Config json should override env vars
try:
data.update(
@@ -143,7 +145,9 @@ def populate_cache(self) -> dict:
}
)
except json.JSONDecodeError:
- logger.critical('Failed to load config.json env values.', exc_info=True)
+ logger.critical(
+ "Failed to load config.json env values.", exc_info=True
+ )
self._cache = data
return self._cache
@@ -282,7 +286,7 @@ def filter_default(
for k, v in data.items():
default = cls.defaults.get(k.lower(), Default)
if default is Default:
- logger.error('Unexpected configuration detected: %s.', k)
+ logger.error("Unexpected configuration detected: %s.", k)
continue
if v != default:
filtered[k.lower()] = v
diff --git a/core/thread.py b/core/thread.py
index 73248d915f..d4213a0c93 100644
--- a/core/thread.py
+++ b/core/thread.py
@@ -103,7 +103,7 @@ async def setup(self, *, creator=None, category=None):
reason="Creating a thread channel.",
)
except discord.HTTPException as e: # Failed to create due to 50 channel limit.
- logger.critical('An error occurred while creating a thread.', exc_info=True)
+ logger.critical("An error occurred while creating a thread.", exc_info=True)
self.manager.cache.pop(self.id)
embed = discord.Embed(color=discord.Color.red())
@@ -125,7 +125,9 @@ async def setup(self, *, creator=None, category=None):
log_count = sum(1 for log in log_data if not log["open"])
except Exception:
- logger.error('An error occurred while posting logs to the database.', exc_info=True)
+ logger.error(
+ "An error occurred while posting logs to the database.", exc_info=True
+ )
log_url = log_count = None
# ensure core functionality still works
@@ -213,7 +215,11 @@ def _format_info_embed(self, user, log_url, log_count, color):
role_names = separator.join(roles)
created = str((time - user.created_at).days)
- embed = discord.Embed(color=color, description=f"{user.mention} was created {days(created)}", timestamp=time)
+ embed = discord.Embed(
+ color=color,
+ description=f"{user.mention} was created {days(created)}",
+ timestamp=time,
+ )
# if not role_names:
# embed.add_field(name='Mention', value=user.mention)
@@ -836,13 +842,13 @@ async def find(
if recipient is None and channel is not None:
thread = self._find_from_channel(channel)
if thread is None:
- id, thread = next(
+ user_id, thread = next(
((k, v) for k, v in self.cache.items() if v.channel == channel),
(-1, None),
)
if thread is not None:
logger.debug("Found thread with tempered ID.")
- await channel.edit(topic=f"User ID: {id}")
+ await channel.edit(topic=f"User ID: {user_id}")
return thread
thread = None
@@ -925,6 +931,6 @@ def format_channel_name(self, author):
counter = 1
while new_name in [c.name for c in self.bot.modmail_guild.text_channels]:
- new_name += f'-{counter}' # two channels with same name
+ new_name += f"-{counter}" # two channels with same name
return new_name
From 263f6c95e2735c351af001383f3ffe7774169f5f Mon Sep 17 00:00:00 2001
From: xPolar <50601857+xPolar@users.noreply.github.com>
Date: Sun, 21 Jul 2019 20:40:00 -0700
Subject: [PATCH 0331/1221] Moderation plugin PR (#328)
* Moderation plugin PR
* fixed moderation plugin syntax error
* Update registry.json
---
plugins/registry.json | 11 ++++++++++-
1 file changed, 10 insertions(+), 1 deletion(-)
diff --git a/plugins/registry.json b/plugins/registry.json
index 8822612d5d..cf46dd72be 100644
--- a/plugins/registry.json
+++ b/plugins/registry.json
@@ -178,5 +178,14 @@
"title": "Get Stats",
"icon_url": "https://image.flaticon.com/icons/png/512/117/117761.png",
"thumbnail_url": "http://www.pngmart.com/files/7/Statistics-PNG-Clipart.png"
- }
+ },
+ "moderation":{
+ "repository": "xPolar/modmail-plugins/Moderation/",
+ "branch": "master",
+ "description": "Moderate your server with Modmail, bring the Mod to Modmail!",
+ "bot_version": "3.0.3",
+ "title": "Moderate your server",
+ "icon_url": "https://cdn.discordapp.com/attachments/539943767562780704/601485194196680704/wGFmzZq.png",
+ "thumbnail_url": "https://cdn.discordapp.com/attachments/539943767562780704/601485194196680704/wGFmzZq.png"
+ }
}
From fce6de818305d689196f7d22f463540072bb224e Mon Sep 17 00:00:00 2001
From: codeinteger6 <44692189+codeinteger6@users.noreply.github.com>
Date: Mon, 22 Jul 2019 16:22:20 +0600
Subject: [PATCH 0332/1221] Fix moderation plugin in registry (#329)
---
plugins/registry.json | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/plugins/registry.json b/plugins/registry.json
index cf46dd72be..fc1dd11a2e 100644
--- a/plugins/registry.json
+++ b/plugins/registry.json
@@ -179,8 +179,8 @@
"icon_url": "https://image.flaticon.com/icons/png/512/117/117761.png",
"thumbnail_url": "http://www.pngmart.com/files/7/Statistics-PNG-Clipart.png"
},
- "moderation":{
- "repository": "xPolar/modmail-plugins/Moderation/",
+ "moderation": {
+ "repository": "xPolar/modmail-plugins",
"branch": "master",
"description": "Moderate your server with Modmail, bring the Mod to Modmail!",
"bot_version": "3.0.3",
From 9e15f683ab9c417b36a542f6f9e8a2f735de2140 Mon Sep 17 00:00:00 2001
From: kyb3r
Date: Mon, 22 Jul 2019 23:44:20 +1000
Subject: [PATCH 0333/1221] Fix bug in error handler in thread setup
---
core/thread.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/core/thread.py b/core/thread.py
index 87627dbaf7..669edb9723 100644
--- a/core/thread.py
+++ b/core/thread.py
@@ -108,7 +108,7 @@ async def setup(self, *, creator=None, category=None):
em = discord.Embed(color=discord.Color.red())
em.title = "Error while trying to create a thread"
- em.description = e.message
+ em.description = str(e)
em.add_field(name="Recipient", value=recipient.mention)
if log_channel is not None:
From b3e8f62e993f718b98ee32367022f8324c108a1c Mon Sep 17 00:00:00 2001
From: Taaku18 <45324516+Taaku18@users.noreply.github.com>
Date: Fri, 26 Jul 2019 23:59:51 -0700
Subject: [PATCH 0334/1221] Snippets/aliases
---
CHANGELOG.md | 4 ++
bot.py | 3 +-
cogs/modmail.py | 115 ++++++++++++++++++++++++++++--------------------
cogs/utility.py | 107 ++++++++++++++++++++++++--------------------
core/utils.py | 18 ++++++--
5 files changed, 149 insertions(+), 98 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index bf4775ff87..3872d519a6 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -29,6 +29,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- `?plugin enabled` results are now sorted alphabetically.
- `?plugin registry` results are now sorted alphabetically, helps user find plugins more easily.
- `?plugin registry page-number` plugin registry can specify a page number for quick access.
+- A reworked interface for `?snippet` and `?alias`.
+ - Add an `?snippet raw ` command for viewing the raw content of a snippet (escaped markdown).
### Fixes
@@ -37,6 +39,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- `mod_typing` and `user_typing` will no longer show when user is blocked.
- Better `?block` usage message.
- Resolves errors when message was sent by mods after thread is closed somehow.
+- Recipient join/leave server messages are limited to only the guild set by `GUILD_ID`.
+- When creating snippets and aliases, it now checks if another snippets/aliases with the same name exists.
### Internal
diff --git a/bot.py b/bot.py
index 132ae4c913..7c9e18528c 100644
--- a/bot.py
+++ b/bot.py
@@ -87,7 +87,8 @@ def __init__(self):
self.db = AsyncIOMotorClient(mongo_uri).modmail_bot
except ConfigurationError as e:
logger.critical(
- "Your MONGO_URI is copied wrong, try re-copying from the source again."
+ "Your MONGO_URI might be copied wrong, try re-copying from the source again. "
+ "Otherwise noted in the following message:"
)
logger.critical(str(e))
sys.exit(0)
diff --git a/cogs/modmail.py b/cogs/modmail.py
index 1f396cdc65..d8c8304fbb 100644
--- a/cogs/modmail.py
+++ b/cogs/modmail.py
@@ -1,11 +1,13 @@
import asyncio
import logging
from datetime import datetime
+from itertools import zip_longest, takewhile
from typing import Optional, Union
from types import SimpleNamespace as param
import discord
from discord.ext import commands
+from discord.utils import escape_markdown, escape_mentions
from dateutil import parser
from natural.date import duration
@@ -15,7 +17,7 @@
from core.models import PermissionLevel
from core.paginator import PaginatorSession
from core.time import UserFriendlyTime, human_timedelta
-from core.utils import format_preview, User
+from core.utils import format_preview, User, create_not_found_embed
logger = logging.getLogger("Modmail")
@@ -100,34 +102,36 @@ async def setup(self, ctx):
@commands.group(aliases=["snippets"], invoke_without_command=True)
@checks.has_permissions(PermissionLevel.SUPPORTER)
- async def snippet(self, ctx):
+ async def snippet(self, ctx, *, name: str.lower = None):
"""
Create pre-defined messages for use in threads.
When `{prefix}snippet` is used by itself, this will retrieve
- a list of snippets that are currently set.
+ a list of snippets that are currently set. `{prefix}snippet-name` will show what the
+ snippet point to.
- To use snippets:
-
- First create a snippet using:
+ To create a snippet:
- `{prefix}snippet add snippet-name A pre-defined text.`
- Afterwards, you can use your snippet in a thread channel
+ You can use your snippet in a thread channel
with `{prefix}snippet-name`, the message "A pre-defined text."
will be sent to the recipient.
+ Currently, there is not a default anonymous snippet command; however, a workaround
+ is available using `{prefix}alias`. Here is how:
+ - `{prefix}alias add snippet-name anonreply A pre-defined anonymous text.`
+
See also `{prefix}alias`.
"""
- embeds = []
+ if name is not None:
+ val = self.bot.snippets.get(name)
+ if val is None:
+ embed = create_not_found_embed(name, self.bot.snippets.keys(), 'Snippet')
+ return await ctx.send(embed=embed)
+ return await ctx.send(escape_mentions(val))
- if self.bot.snippets:
- embed = discord.Embed(
- color=self.bot.main_color,
- description="Here is a list of snippets "
- "that are currently configured.",
- )
- else:
+ if not self.bot.snippets:
embed = discord.Embed(
color=discord.Color.red(),
description="You dont have any snippets at the moment.",
@@ -135,25 +139,37 @@ async def snippet(self, ctx):
embed.set_footer(
text=f"Do {self.bot.prefix}help snippet for more commands."
)
+ embed.set_author(name="Snippets", icon_url=ctx.guild.icon_url)
+ return await ctx.send(embed=embed)
- embed.set_author(name="Snippets", icon_url=ctx.guild.icon_url)
- embeds.append(embed)
+ embeds = []
- for name, value in self.bot.snippets.items():
- if len(embed.fields) == 5:
- embed = discord.Embed(
- color=self.bot.main_color, description=embed.description
+ for names in zip_longest(*(iter(sorted(self.bot.snippets)),) * 15):
+ description = "\n".join(
+ ": ".join((str(a), b))
+ for a, b in enumerate(
+ takewhile(lambda x: x is not None, names), start=1
)
- embed.set_author(name="Snippets", icon_url=ctx.guild.icon_url)
- embeds.append(embed)
- embed.add_field(name=name, value=value, inline=False)
+ )
+ embed = discord.Embed(color=self.bot.main_color, description=description)
+ embed.set_author(name="Snippets", icon_url=ctx.guild.icon_url)
+ embeds.append(embed)
session = PaginatorSession(ctx, *embeds)
await session.run()
+ @snippet.command(name="raw")
+ @checks.has_permissions(PermissionLevel.SUPPORTER)
+ async def snippet_raw(self, ctx, *, name: str.lower):
+ val = self.bot.snippets.get(name)
+ if val is None:
+ embed = create_not_found_embed(name, self.bot.snippets.keys(), 'Snippet')
+ return await ctx.send(embed=embed)
+ return await ctx.send(escape_markdown(escape_mentions(val)).replace('<', '\\<'))
+
@snippet.command(name="add")
@checks.has_permissions(PermissionLevel.SUPPORTER)
- async def snippet_add(self, ctx, name: str.lower, *, value):
+ async def snippet_add(self, ctx, name: str.lower, *, value: commands.clean_content):
"""
Add a snippet.
@@ -167,19 +183,35 @@ async def snippet_add(self, ctx, name: str.lower, *, value):
color=discord.Color.red(),
description=f"Snippet `{name}` already exists.",
)
- else:
- self.bot.snippets[name] = value
- await self.bot.config.update()
+ return await ctx.send(embed=embed)
+ if name in self.bot.aliases:
embed = discord.Embed(
- title="Added snippet",
- color=self.bot.main_color,
- description=f'`{name}` will now send "{value}".',
+ title="Error",
+ color=discord.Color.red(),
+ description=f"An alias with the same name already exists: `{name}`.",
+ )
+ return await ctx.send(embed=embed)
+
+ if len(name) > 120:
+ embed = discord.Embed(
+ title="Error",
+ color=discord.Color.red(),
+ description=f"Snippet names cannot be longer than 120 characters.",
)
+ return await ctx.send(embed=embed)
- await ctx.send(embed=embed)
+ self.bot.snippets[name] = value
+ await self.bot.config.update()
+
+ embed = discord.Embed(
+ title="Added snippet",
+ color=self.bot.main_color,
+ description=f'Successfully created snippet.',
+ )
+ return await ctx.send(embed=embed)
- @snippet.command(name="remove", aliases=["del", "delete", "rm"])
+ @snippet.command(name="remove", aliases=["del", "delete"])
@checks.has_permissions(PermissionLevel.SUPPORTER)
async def snippet_remove(self, ctx, *, name: str.lower):
"""Remove a snippet."""
@@ -188,18 +220,12 @@ async def snippet_remove(self, ctx, *, name: str.lower):
embed = discord.Embed(
title="Removed snippet",
color=self.bot.main_color,
- description=f"`{name}` no longer exists.",
+ description=f"Snippet `{name}` is now deleted.",
)
self.bot.snippets.pop(name)
await self.bot.config.update()
-
else:
- embed = discord.Embed(
- title="Error",
- color=discord.Color.red(),
- description=f"Snippet `{name}` does not exist.",
- )
-
+ embed = create_not_found_embed(name, self.bot.snippets.keys(), 'Snippet')
await ctx.send(embed=embed)
@snippet.command(name="edit")
@@ -221,13 +247,8 @@ async def snippet_edit(self, ctx, name: str.lower, *, value):
color=self.bot.main_color,
description=f'`{name}` will now send "{value}".',
)
-
else:
- embed = discord.Embed(
- title="Error",
- color=discord.Color.red(),
- description=f"Snippet `{name}` does not exist.",
- )
+ embed = create_not_found_embed(name, self.bot.snippets.keys(), 'Snippet')
await ctx.send(embed=embed)
@commands.command()
diff --git a/cogs/utility.py b/cogs/utility.py
index c1fb34369f..182f836864 100644
--- a/cogs/utility.py
+++ b/cogs/utility.py
@@ -8,6 +8,7 @@
from datetime import datetime
from difflib import get_close_matches
from io import StringIO, BytesIO
+from itertools import zip_longest, takewhile
from json import JSONDecodeError, loads
from textwrap import indent
from types import SimpleNamespace as param
@@ -16,6 +17,7 @@
from discord import Embed, Color, Activity, Role
from discord.enums import ActivityType, Status
from discord.ext import commands
+from discord.utils import escape_mentions
from aiohttp import ClientResponseError
from pkg_resources import parse_version
@@ -25,7 +27,7 @@
from core.decorators import trigger_typing
from core.models import InvalidConfigError, PermissionLevel
from core.paginator import PaginatorSession, MessagePaginatorSession
-from core.utils import cleanup_code, User, get_perm_level
+from core.utils import cleanup_code, User, get_perm_level, create_not_found_embed
logger = logging.getLogger("Modmail")
@@ -811,12 +813,13 @@ async def config_get(self, ctx, key: str.lower = None):
@commands.group(aliases=["aliases"], invoke_without_command=True)
@checks.has_permissions(PermissionLevel.MODERATOR)
- async def alias(self, ctx):
+ async def alias(self, ctx, *, name: str.lower = None):
"""
Create shortcuts to bot commands.
When `{prefix}alias` is used by itself, this will retrieve
- a list of alias that are currently set.
+ a list of alias that are currently set. `{prefix}alias-name` will show what the
+ alias point to.
To use alias:
@@ -830,44 +833,73 @@ async def alias(self, ctx):
See also `{prefix}snippet`.
"""
- embeds = []
- desc = "Here is a list of aliases that are currently configured."
+ if name is not None:
+ val = self.bot.aliases.get(name)
+ if val is None:
+ embed = create_not_found_embed(name, self.bot.aliases.keys(), 'Alias')
+ return await ctx.send(embed=embed)
+ return await ctx.send(escape_mentions(val))
- if self.bot.aliases:
- embed = Embed(color=self.bot.main_color, description=desc)
- else:
+ if not self.bot.aliases:
embed = Embed(
- color=self.bot.main_color,
+ color=Color.red(),
description="You dont have any aliases at the moment.",
)
- embed.set_author(name="Command aliases:", icon_url=ctx.guild.icon_url)
- embed.set_footer(text=f"Do {self.bot.prefix}help alias for more commands.")
- embeds.append(embed)
+ embed.set_footer(
+ text=f"Do {self.bot.prefix}help alias for more commands."
+ )
+ embed.set_author(name="Aliases", icon_url=ctx.guild.icon_url)
+ return await ctx.send(embed=embed)
- for name, value in self.bot.aliases.items():
- if len(embed.fields) == 5:
- embed = Embed(color=self.bot.main_color, description=desc)
- embed.set_author(name="Command aliases", icon_url=ctx.guild.icon_url)
- embed.set_footer(
- text=f"Do {self.bot.prefix}help alias for more commands."
- )
+ embeds = []
- embeds.append(embed)
- embed.add_field(name=name, value=value, inline=False)
+ for names in zip_longest(*(iter(sorted(self.bot.aliases)),) * 15):
+ description = "\n".join(
+ ": ".join((str(a), b))
+ for a, b in enumerate(
+ takewhile(lambda x: x is not None, names), start=1
+ )
+ )
+ embed = Embed(color=self.bot.main_color, description=description)
+ embed.set_author(name="Command Aliases", icon_url=ctx.guild.icon_url)
+ embeds.append(embed)
session = PaginatorSession(ctx, *embeds)
- return await session.run()
+ await session.run()
@alias.command(name="add")
@checks.has_permissions(PermissionLevel.MODERATOR)
async def alias_add(self, ctx, name: str.lower, *, value):
"""Add an alias."""
- if self.bot.get_command(name) or name in self.bot.aliases:
+ if self.bot.get_command(name):
embed = Embed(
title="Error",
color=Color.red(),
- description="A command or alias already exists "
- f"with the same name: `{name}`.",
+ description=f"A command with the same name already exists: `{name}`.",
+ )
+ return await ctx.send(embed=embed)
+
+ if name in self.bot.aliases:
+ embed = Embed(
+ title="Error",
+ color=Color.red(),
+ description=f"Another alias with the same name already exists: `{name}`.",
+ )
+ return await ctx.send(embed=embed)
+
+ if name in self.bot.snippets:
+ embed = Embed(
+ title="Error",
+ color=Color.red(),
+ description=f"A snippet with the same name already exists: `{name}`.",
+ )
+ return await ctx.send(embed=embed)
+
+ if len(name) > 120:
+ embed = Embed(
+ title="Error",
+ color=Color.red(),
+ description=f"Alias names cannot be longer than 120 characters.",
)
return await ctx.send(embed=embed)
@@ -892,7 +924,7 @@ async def alias_add(self, ctx, name: str.lower, *, value):
return await ctx.send(embed=embed)
- @alias.command(name="remove", aliases=["del", "delete", "rm"])
+ @alias.command(name="remove", aliases=["del", "delete"])
@checks.has_permissions(PermissionLevel.MODERATOR)
async def alias_remove(self, ctx, *, name: str.lower):
"""Remove an alias."""
@@ -906,13 +938,8 @@ async def alias_remove(self, ctx, *, name: str.lower):
color=self.bot.main_color,
description=f"`{name}` no longer exists.",
)
-
else:
- embed = Embed(
- title="Error",
- color=Color.red(),
- description=f"Alias `{name}` does not exist.",
- )
+ embed = create_not_found_embed(name, self.bot.aliases.keys(), 'Alias')
return await ctx.send(embed=embed)
@@ -923,21 +950,7 @@ async def alias_edit(self, ctx, name: str.lower, *, value):
Edit an alias.
"""
if name not in self.bot.aliases:
- embed = Embed(
- title="Error",
- color=Color.red(),
- description=f"Alias `{name}` does not exist.",
- )
-
- return await ctx.send(embed=embed)
-
- if self.bot.get_command(name):
- embed = Embed(
- title="Error",
- color=Color.red(),
- description="A command or alias already exists "
- f"with the same name: `{name}`.",
- )
+ embed = create_not_found_embed(name, self.bot.aliases.keys(), 'Alias')
return await ctx.send(embed=embed)
linked_command = value.split()[0]
diff --git a/core/utils.py b/core/utils.py
index 8f5e6c2589..76c1864a27 100644
--- a/core/utils.py
+++ b/core/utils.py
@@ -1,9 +1,10 @@
import re
import typing
+from difflib import get_close_matches
from distutils.util import strtobool as _stb # pylint: disable=E0401
from urllib import parse
-from discord import Object
+import discord
from discord.ext import commands
from core.models import PermissionLevel
@@ -34,7 +35,7 @@ async def convert(self, ctx, argument):
match = self._get_id_match(argument)
if match is None:
raise commands.BadArgument('User "{}" not found'.format(argument))
- return Object(int(match.group(1)))
+ return discord.Object(int(match.group(1)))
def truncate(text: str, max: int = 50) -> str: # pylint: disable=W0622
@@ -186,7 +187,7 @@ def match_user_id(text: str) -> int:
int
The user ID if found. Otherwise, -1.
"""
- match = re.search(r"\bUser ID: (\d+)\b", text)
+ match = re.search(r"\bUser ID: (\d{17,21})\b", text)
if match is not None:
return int(match.group(1))
return -1
@@ -208,3 +209,14 @@ async def ignore(coro):
await coro
except Exception:
pass
+
+
+def create_not_found_embed(word, possibilities, name, n=2, cutoff=0.6) -> discord.Embed:
+ embed = discord.Embed(
+ color=discord.Color.red(),
+ description=f"**{name.capitalize()} `{word}` cannot be found.**",
+ )
+ val = get_close_matches(word, possibilities, n=n, cutoff=cutoff)
+ if val:
+ embed.description += '\nHowever, perhaps you meant...\n' + '\n'.join(val)
+ return embed
From 1564e1f7e4c9e3d2cdd9a5766aa69027cb0295f4 Mon Sep 17 00:00:00 2001
From: Taaku18 <45324516+Taaku18@users.noreply.github.com>
Date: Sat, 27 Jul 2019 15:05:15 -0700
Subject: [PATCH 0335/1221] Multistep alias
---
.gitignore | 4 ++
CHANGELOG.md | 1 +
bot.py | 131 +++++++++++++++++++++++++------------
cogs/modmail.py | 14 ++--
cogs/utility.py | 170 ++++++++++++++++++++++++++++++++++++++----------
core/utils.py | 35 +++++++++-
6 files changed, 273 insertions(+), 82 deletions(-)
diff --git a/.gitignore b/.gitignore
index 0c953c5e8e..8e80af7c26 100644
--- a/.gitignore
+++ b/.gitignore
@@ -124,6 +124,10 @@ dmypy.json
# VS Code
.vscode/
+# Node
+package-lock.json
+node_modules/
+
# Modmail
config.json
plugins/
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 3872d519a6..707fbd1207 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -19,6 +19,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- New configuration variable `thread_self_closable_creation_footer` — the footer when `recipient_thread_close` is enabled.
- Added a minimalistic version of requirements.txt (named requirements.min.txt) that contains only the absolute minimum of Modmail.
- For users having trouble with pipenv or any other reason.
+- Multi-step alias, see `?help alias add`. Public beta testing, might be unstable.
### Changes
diff --git a/bot.py b/bot.py
index 7c9e18528c..f92630faec 100644
--- a/bot.py
+++ b/bot.py
@@ -6,8 +6,8 @@
import re
import sys
import typing
-
from datetime import datetime
+from itertools import zip_longest
from types import SimpleNamespace
import discord
@@ -30,7 +30,7 @@
from core.clients import ApiClient, PluginDatabaseClient
from core.config import ConfigManager
-from core.utils import human_join, strtobool
+from core.utils import human_join, strtobool, parse_alias
from core.models import PermissionLevel, ModmailLogger
from core.thread import ThreadManager
from core.time import human_timedelta
@@ -669,24 +669,76 @@ async def _process_blocked(self, message: discord.Message) -> bool:
logger.warning("Failed to add reaction %s.", reaction, exc_info=True)
return str(message.author.id) in self.blocked_users
- async def process_modmail(self, message: discord.Message) -> None:
+ async def process_dm_modmail(self, message: discord.Message) -> None:
"""Processes messages sent to the bot."""
- await self.wait_for_connected()
-
blocked = await self._process_blocked(message)
if not blocked:
thread = await self.threads.find_or_create(message.author)
await thread.send(message)
+ async def get_contexts(self, message, *, cls=commands.Context):
+ """
+ Returns all invocation contexts from the message.
+ Supports getting the prefix from database as well as command aliases.
+ """
+
+ view = StringView(message.content)
+ ctx = cls(prefix=self.prefix, view=view, bot=self, message=message)
+ ctx.thread = await self.threads.find(channel=ctx.channel)
+
+ if self._skip_check(message.author.id, self.user.id):
+ return [ctx]
+
+ prefixes = await self.get_prefix()
+
+ invoked_prefix = discord.utils.find(view.skip_string, prefixes)
+ if invoked_prefix is None:
+ return [ctx]
+
+ invoker = view.get_word().lower()
+
+ # Check if there is any aliases being called.
+ alias = self.aliases.get(invoker)
+ if alias is not None:
+ aliases = parse_alias(alias)
+ if not aliases:
+ logger.warning("Alias %s is invalid, removing.", invoker)
+ self.aliases.pop(invoker)
+ else:
+ len_ = len(f"{invoked_prefix}{invoker}")
+ contents = parse_alias(message.content[len_:])
+ if not contents:
+ contents = [message.content[len_:]]
+
+ ctxs = []
+ for alias, content in zip_longest(aliases, contents):
+ if alias is None:
+ break
+ ctx = cls(prefix=self.prefix, view=view, bot=self, message=message)
+ ctx.thread = await self.threads.find(channel=ctx.channel)
+
+ if content is not None:
+ view = StringView(f"{alias} {content.strip()}")
+ else:
+ view = StringView(alias)
+ ctx.view = view
+ ctx.invoked_with = view.get_word()
+ ctx.command = self.all_commands.get(ctx.invoked_with)
+ ctxs += [ctx]
+ return ctxs
+
+ ctx.invoked_with = invoker
+ ctx.command = self.all_commands.get(invoker)
+ return [ctx]
+
async def get_context(self, message, *, cls=commands.Context):
"""
Returns the invocation context from the message.
- Supports getting the prefix from database as well as command aliases.
+ Supports getting the prefix from database.
"""
- await self.wait_for_connected()
view = StringView(message.content)
- ctx = cls(prefix=None, view=view, bot=self, message=message)
+ ctx = cls(prefix=self.prefix, view=view, bot=self, message=message)
if self._skip_check(message.author.id, self.user.id):
return ctx
@@ -701,17 +753,7 @@ async def get_context(self, message, *, cls=commands.Context):
invoker = view.get_word().lower()
- # Check if there is any aliases being called.
- alias = self.aliases.get(invoker)
- if alias is not None:
- ctx._alias_invoked = True # pylint: disable=W0212
- len_ = len(f"{invoked_prefix}{invoker}")
- view = StringView(f"{alias}{ctx.message.content[len_:]}")
- ctx.view = view
- invoker = view.get_word()
-
ctx.invoked_with = invoker
- ctx.prefix = self.prefix # Sane prefix (No mentions)
ctx.command = self.all_commands.get(invoker)
return ctx
@@ -739,47 +781,52 @@ async def update_perms(
async def on_message(self, message):
await self.wait_for_connected()
-
if message.type == discord.MessageType.pins_add and message.author == self.user:
await message.delete()
+ await self.process_commands(message)
+ async def process_commands(self, message):
if message.author.bot:
return
if isinstance(message.channel, discord.DMChannel):
- return await self.process_modmail(message)
+ return await self.process_dm_modmail(message)
- prefix = self.prefix
+ if message.content.startswith(self.prefix):
+ cmd = message.content[len(self.prefix) :].strip()
- if message.content.startswith(prefix):
- cmd = message.content[len(prefix) :].strip()
+ # Process snippets
if cmd in self.snippets:
thread = await self.threads.find(channel=message.channel)
snippet = self.snippets[cmd]
if thread:
snippet = snippet.format(recipient=thread.recipient)
- message.content = f"{prefix}reply {snippet}"
+ message.content = f"{self.prefix}reply {snippet}"
- ctx = await self.get_context(message)
- if ctx.command:
- return await self.invoke(ctx)
+ ctxs = await self.get_contexts(message)
+ for ctx in ctxs:
+ if ctx.command:
+ await self.invoke(ctx)
+ continue
- thread = await self.threads.find(channel=ctx.channel)
- if thread is not None:
- try:
- reply_without_command = strtobool(self.config["reply_without_command"])
- except ValueError:
- reply_without_command = self.config.remove("reply_without_command")
+ thread = await self.threads.find(channel=ctx.channel)
+ if thread is not None:
+ try:
+ reply_without_command = strtobool(
+ self.config["reply_without_command"]
+ )
+ except ValueError:
+ reply_without_command = self.config.remove("reply_without_command")
- if reply_without_command:
- await thread.reply(message)
- else:
- await self.api.append_log(message, type_="internal")
- elif ctx.invoked_with:
- exc = commands.CommandNotFound(
- 'Command "{}" is not found'.format(ctx.invoked_with)
- )
- self.dispatch("command_error", ctx, exc)
+ if reply_without_command:
+ await thread.reply(message)
+ else:
+ await self.api.append_log(message, type_="internal")
+ elif ctx.invoked_with:
+ exc = commands.CommandNotFound(
+ 'Command "{}" is not found'.format(ctx.invoked_with)
+ )
+ self.dispatch("command_error", ctx, exc)
async def on_typing(self, channel, user, _):
await self.wait_for_connected()
diff --git a/cogs/modmail.py b/cogs/modmail.py
index d8c8304fbb..4eae4037e3 100644
--- a/cogs/modmail.py
+++ b/cogs/modmail.py
@@ -127,7 +127,9 @@ async def snippet(self, ctx, *, name: str.lower = None):
if name is not None:
val = self.bot.snippets.get(name)
if val is None:
- embed = create_not_found_embed(name, self.bot.snippets.keys(), 'Snippet')
+ embed = create_not_found_embed(
+ name, self.bot.snippets.keys(), "Snippet"
+ )
return await ctx.send(embed=embed)
return await ctx.send(escape_mentions(val))
@@ -163,9 +165,9 @@ async def snippet(self, ctx, *, name: str.lower = None):
async def snippet_raw(self, ctx, *, name: str.lower):
val = self.bot.snippets.get(name)
if val is None:
- embed = create_not_found_embed(name, self.bot.snippets.keys(), 'Snippet')
+ embed = create_not_found_embed(name, self.bot.snippets.keys(), "Snippet")
return await ctx.send(embed=embed)
- return await ctx.send(escape_markdown(escape_mentions(val)).replace('<', '\\<'))
+ return await ctx.send(escape_markdown(escape_mentions(val)).replace("<", "\\<"))
@snippet.command(name="add")
@checks.has_permissions(PermissionLevel.SUPPORTER)
@@ -207,7 +209,7 @@ async def snippet_add(self, ctx, name: str.lower, *, value: commands.clean_conte
embed = discord.Embed(
title="Added snippet",
color=self.bot.main_color,
- description=f'Successfully created snippet.',
+ description=f"Successfully created snippet.",
)
return await ctx.send(embed=embed)
@@ -225,7 +227,7 @@ async def snippet_remove(self, ctx, *, name: str.lower):
self.bot.snippets.pop(name)
await self.bot.config.update()
else:
- embed = create_not_found_embed(name, self.bot.snippets.keys(), 'Snippet')
+ embed = create_not_found_embed(name, self.bot.snippets.keys(), "Snippet")
await ctx.send(embed=embed)
@snippet.command(name="edit")
@@ -248,7 +250,7 @@ async def snippet_edit(self, ctx, name: str.lower, *, value):
description=f'`{name}` will now send "{value}".',
)
else:
- embed = create_not_found_embed(name, self.bot.snippets.keys(), 'Snippet')
+ embed = create_not_found_embed(name, self.bot.snippets.keys(), "Snippet")
await ctx.send(embed=embed)
@commands.command()
diff --git a/cogs/utility.py b/cogs/utility.py
index 182f836864..a5adde8345 100644
--- a/cogs/utility.py
+++ b/cogs/utility.py
@@ -17,7 +17,7 @@
from discord import Embed, Color, Activity, Role
from discord.enums import ActivityType, Status
from discord.ext import commands
-from discord.utils import escape_mentions
+from discord.utils import escape_markdown
from aiohttp import ClientResponseError
from pkg_resources import parse_version
@@ -27,7 +27,13 @@
from core.decorators import trigger_typing
from core.models import InvalidConfigError, PermissionLevel
from core.paginator import PaginatorSession, MessagePaginatorSession
-from core.utils import cleanup_code, User, get_perm_level, create_not_found_embed
+from core.utils import (
+ cleanup_code,
+ User,
+ get_perm_level,
+ create_not_found_embed,
+ parse_alias,
+)
logger = logging.getLogger("Modmail")
@@ -836,18 +842,43 @@ async def alias(self, ctx, *, name: str.lower = None):
if name is not None:
val = self.bot.aliases.get(name)
if val is None:
- embed = create_not_found_embed(name, self.bot.aliases.keys(), 'Alias')
+ embed = create_not_found_embed(name, self.bot.aliases.keys(), "Alias")
return await ctx.send(embed=embed)
- return await ctx.send(escape_mentions(val))
+
+ values = parse_alias(val)
+
+ if not values:
+ embed = Embed(
+ title="Error",
+ color=Color.red(),
+ description=f"Alias `{name}` is invalid, it used to be `{escape_markdown(val)}`. "
+ f"This alias will now be deleted.",
+ )
+ self.bot.aliases.pop(name)
+ await self.bot.config.update()
+ return await ctx.send(embed=embed)
+
+ if len(values) == 1:
+ embed = Embed(
+ color=self.bot.main_color,
+ description=f"`{name}` points to `{escape_markdown(values[0])}`.",
+ )
+ else:
+ embed = Embed(
+ color=self.bot.main_color,
+ description=f"`{name}` points to the following steps:",
+ )
+ for i, val in enumerate(values, start=1):
+ embed.description += f"\n{i}: {escape_markdown(val)}"
+
+ return await ctx.send(embed=embed)
if not self.bot.aliases:
embed = Embed(
color=Color.red(),
description="You dont have any aliases at the moment.",
)
- embed.set_footer(
- text=f"Do {self.bot.prefix}help alias for more commands."
- )
+ embed.set_footer(text=f"Do {self.bot.prefix}help alias for more commands.")
embed.set_author(name="Aliases", icon_url=ctx.guild.icon_url)
return await ctx.send(embed=embed)
@@ -870,7 +901,19 @@ async def alias(self, ctx, *, name: str.lower = None):
@alias.command(name="add")
@checks.has_permissions(PermissionLevel.MODERATOR)
async def alias_add(self, ctx, name: str.lower, *, value):
- """Add an alias."""
+ """
+ Add an alias.
+
+ Alias also supports multi-step aliases, to create a multi-step alias use quotes
+ to wrap each step and separate each step with `&&`. For example:
+
+ - `{prefix}alias add movenreply "move admin-category" && "reply Thanks for reaching out to the admins"`
+
+ However, if you run into problems, try wrapping the command with quotes. For example:
+
+ - This will fail: `{prefix}alias add reply You'll need to type && to work`
+ - Correct method: `{prefix}alias add reply "You'll need to type && to work"`
+ """
if self.bot.get_command(name):
embed = Embed(
title="Error",
@@ -903,24 +946,55 @@ async def alias_add(self, ctx, name: str.lower, *, value):
)
return await ctx.send(embed=embed)
- linked_command = value.split()[0]
- if not self.bot.get_command(linked_command):
+ values = parse_alias(value)
+
+ if not values:
embed = Embed(
title="Error",
color=Color.red(),
- description="The command you are attempting to point "
- f"to does not exist: `{linked_command}`.",
+ description="Invalid multi-step alias, try wrapping each steps in quotes.",
)
+ embed.set_footer(text=f'See "{self.bot.prefix}alias add" for more details.')
return await ctx.send(embed=embed)
- self.bot.aliases[name] = value
- await self.bot.config.update()
+ if len(values) == 1:
+ linked_command = values[0].split()[0]
+ if not self.bot.get_command(linked_command):
+ embed = Embed(
+ title="Error",
+ color=Color.red(),
+ description="The command you are attempting to point "
+ f"to does not exist: `{linked_command}`.",
+ )
+ return await ctx.send(embed=embed)
- embed = Embed(
- title="Added alias",
- color=self.bot.main_color,
- description=f'`{name}` points to: "{value}".',
- )
+ embed = Embed(
+ title="Added alias",
+ color=self.bot.main_color,
+ description=f'`{name}` points to: "{values[0]}".',
+ )
+
+ else:
+ embed = Embed(
+ title="Added alias",
+ color=self.bot.main_color,
+ description=f"`{name}` now points to the following steps:",
+ )
+
+ for i, val in enumerate(values, start=1):
+ linked_command = val.split()[0]
+ if not self.bot.get_command(linked_command):
+ embed = Embed(
+ title="Error",
+ color=Color.red(),
+ description="The command you are attempting to point "
+ f"to on step {i} does not exist: `{linked_command}`.",
+ )
+ return await ctx.send(embed=embed)
+ embed.description += f"\n{i}: {val}"
+
+ self.bot.aliases[name] = "&&".join(values)
+ await self.bot.config.update()
return await ctx.send(embed=embed)
@@ -936,10 +1010,10 @@ async def alias_remove(self, ctx, *, name: str.lower):
embed = Embed(
title="Removed alias",
color=self.bot.main_color,
- description=f"`{name}` no longer exists.",
+ description=f"Successfully deleted `{name}`.",
)
else:
- embed = create_not_found_embed(name, self.bot.aliases.keys(), 'Alias')
+ embed = create_not_found_embed(name, self.bot.aliases.keys(), "Alias")
return await ctx.send(embed=embed)
@@ -950,27 +1024,57 @@ async def alias_edit(self, ctx, name: str.lower, *, value):
Edit an alias.
"""
if name not in self.bot.aliases:
- embed = create_not_found_embed(name, self.bot.aliases.keys(), 'Alias')
+ embed = create_not_found_embed(name, self.bot.aliases.keys(), "Alias")
return await ctx.send(embed=embed)
- linked_command = value.split()[0]
- if not self.bot.get_command(linked_command):
+ values = parse_alias(value)
+
+ if not values:
embed = Embed(
title="Error",
color=Color.red(),
- description="The command you are attempting to point "
- f"to does not exist: `{linked_command}`.",
+ description="Invalid multi-step alias, try wrapping each steps in quotes.",
)
+ embed.set_footer(text=f'See "{self.bot.prefix}alias add" for more details.')
return await ctx.send(embed=embed)
- self.bot.aliases[name] = value
- await self.bot.config.update()
+ if len(values) == 1:
+ linked_command = values[0].split()[0]
+ if not self.bot.get_command(linked_command):
+ embed = Embed(
+ title="Error",
+ color=Color.red(),
+ description="The command you are attempting to point "
+ f"to does not exist: `{linked_command}`.",
+ )
+ return await ctx.send(embed=embed)
+ embed = Embed(
+ title="Edited alias",
+ color=self.bot.main_color,
+ description=f'`{name}` now points to: "{values[0]}".',
+ )
- embed = Embed(
- title="Edited alias",
- color=self.bot.main_color,
- description=f'`{name}` now points to: "{value}".',
- )
+ else:
+ embed = Embed(
+ title="Edited alias",
+ color=self.bot.main_color,
+ description=f"`{name}` now points to the following steps:",
+ )
+
+ for i, val in enumerate(values, start=1):
+ linked_command = val.split()[0]
+ if not self.bot.get_command(linked_command):
+ embed = Embed(
+ title="Error",
+ color=Color.red(),
+ description="The command you are attempting to point "
+ f"to on step {i} does not exist: `{linked_command}`.",
+ )
+ return await ctx.send(embed=embed)
+ embed.description += f"\n{i}: {val}"
+
+ self.bot.aliases[name] = "&&".join(values)
+ await self.bot.config.update()
return await ctx.send(embed=embed)
@commands.group(aliases=["perms"], invoke_without_command=True)
diff --git a/core/utils.py b/core/utils.py
index 76c1864a27..16fe1675b0 100644
--- a/core/utils.py
+++ b/core/utils.py
@@ -1,4 +1,5 @@
import re
+import shlex
import typing
from difflib import get_close_matches
from distutils.util import strtobool as _stb # pylint: disable=E0401
@@ -218,5 +219,37 @@ def create_not_found_embed(word, possibilities, name, n=2, cutoff=0.6) -> discor
)
val = get_close_matches(word, possibilities, n=n, cutoff=cutoff)
if val:
- embed.description += '\nHowever, perhaps you meant...\n' + '\n'.join(val)
+ embed.description += "\nHowever, perhaps you meant...\n" + "\n".join(val)
return embed
+
+
+def parse_alias(alias):
+ if "&&" not in alias:
+ if alias.startswith('"') and alias.endswith('"'):
+ return [alias[1:-1]]
+ return [alias]
+
+ buffer = ""
+ cmd = []
+ try:
+ for token in shlex.shlex(alias, punctuation_chars="&"):
+ if token != "&&":
+ buffer += " " + token
+ continue
+
+ buffer = buffer.strip()
+ if buffer.startswith('"') and buffer.endswith('"'):
+ buffer = buffer[1:-1]
+ cmd += [buffer]
+ buffer = ""
+ except ValueError:
+ return []
+
+ buffer = buffer.strip()
+ if buffer.startswith('"') and buffer.endswith('"'):
+ buffer = buffer[1:-1]
+ cmd += [buffer]
+
+ if not all(cmd):
+ return []
+ return cmd
From 60b5eb13412885b8cbb20e1580a550383ba46f06 Mon Sep 17 00:00:00 2001
From: Taaku18 <45324516+Taaku18@users.noreply.github.com>
Date: Sat, 27 Jul 2019 15:25:59 -0700
Subject: [PATCH 0336/1221] Address semvar stuff
---
CHANGELOG.md | 5 +++--
1 file changed, 3 insertions(+), 2 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 707fbd1207..c17564c901 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,8 +1,9 @@
# Changelog
All notable changes to this project will be documented in this file.
-The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
-and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
+The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
+This project mostly adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html);
+however, insignificant breaking changes does not guarantee a major version bump, see the reasoning [here](https://github.com/kyb3r/modmail/issues/319).
# v3.1.0
From d22093721f68519cf819c20c0305ffa05d21db40 Mon Sep 17 00:00:00 2001
From: Taaku18 <45324516+Taaku18@users.noreply.github.com>
Date: Sat, 27 Jul 2019 18:54:57 -0700
Subject: [PATCH 0337/1221] misc help and return user_typing
---
CHANGELOG.md | 3 ++-
bot.py | 7 -------
cogs/utility.py | 18 +++++++++++-------
core/config.py | 5 ++---
core/utils.py | 6 +++---
5 files changed, 18 insertions(+), 21 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index c17564c901..66c4d7cf5f 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -21,6 +21,7 @@ however, insignificant breaking changes does not guarantee a major version bump,
- Added a minimalistic version of requirements.txt (named requirements.min.txt) that contains only the absolute minimum of Modmail.
- For users having trouble with pipenv or any other reason.
- Multi-step alias, see `?help alias add`. Public beta testing, might be unstable.
+- Misc commands without cogs are now displayed in `?help`.
### Changes
@@ -38,7 +39,7 @@ however, insignificant breaking changes does not guarantee a major version bump,
- `?notify` no longer carries over to the next thread.
- `discord.NotFound` errors for `on_raw_reaction_add`.
-- `mod_typing` and `user_typing` will no longer show when user is blocked.
+- `mod_typing` ~~and `user_typing`~~ (`user_typing` is by-design to show now) will no longer show when user is blocked.
- Better `?block` usage message.
- Resolves errors when message was sent by mods after thread is closed somehow.
- Recipient join/leave server messages are limited to only the guild set by `GUILD_ID`.
diff --git a/bot.py b/bot.py
index f92630faec..6031fd3574 100644
--- a/bot.py
+++ b/bot.py
@@ -838,13 +838,6 @@ async def _void(*_args, **_kwargs):
pass
if isinstance(channel, discord.DMChannel):
- if await self._process_blocked(
- SimpleNamespace(
- author=user, channel=SimpleNamespace(send=_void), add_reaction=_void
- )
- ):
- return
-
try:
user_typing = strtobool(self.config["user_typing"])
except ValueError:
diff --git a/cogs/utility.py b/cogs/utility.py
index a5adde8345..51d5baa1a5 100644
--- a/cogs/utility.py
+++ b/cogs/utility.py
@@ -39,13 +39,13 @@
class ModmailHelpCommand(commands.HelpCommand):
- async def format_cog_help(self, cog):
+ async def format_cog_help(self, cog, *, no_cog=False):
bot = self.context.bot
prefix = self.clean_prefix
formats = [""]
for cmd in await self.filter_commands(
- cog.get_commands(), sort=True, key=get_perm_level
+ cog.get_commands() if not no_cog else cog, sort=True, key=get_perm_level
):
perm_level = get_perm_level(cmd)
if perm_level is PermissionLevel.INVALID:
@@ -63,16 +63,19 @@ async def format_cog_help(self, cog):
embeds = []
for format_ in formats:
+ description = cog.description or "No description." \
+ if not no_cog else "Miscellaneous commands without a category."
embed = Embed(
- description=f'*{cog.description or "No description."}*',
+ description=f'*{description}*',
color=bot.main_color,
)
embed.add_field(name="Commands", value=format_ or "No commands.")
continued = " (Continued)" if embeds else ""
+ name = cog.qualified_name + " - Help" if not no_cog else "Miscellaneous Commands"
embed.set_author(
- name=cog.qualified_name + " - Help" + continued,
+ name=name + continued,
icon_url=bot.user.avatar_url,
)
@@ -88,9 +91,8 @@ def process_help_msg(self, help_: str):
async def send_bot_help(self, mapping):
embeds = []
- # TODO: Implement for no cog commands
-
- cogs = list(filter(None, mapping))
+ no_cog_commands = sorted(mapping.pop(None), key=lambda c: c.qualified_name)
+ cogs = sorted(mapping, key=lambda c: c.qualified_name)
bot = self.context.bot
@@ -105,6 +107,8 @@ async def send_bot_help(self, mapping):
for cog in default_cogs:
embeds.extend(await self.format_cog_help(cog))
+ if no_cog_commands:
+ embeds.extend(await self.format_cog_help(no_cog_commands, no_cog=True))
p_session = PaginatorSession(
self.context, *embeds, destination=self.get_destination()
diff --git a/core/config.py b/core/config.py
index 301ab69f61..d99d762307 100644
--- a/core/config.py
+++ b/core/config.py
@@ -115,7 +115,6 @@ class ConfigManager:
def __init__(self, bot):
self.bot = bot
- self.api = self.bot.api
self._cache = {}
self.ready_event = asyncio.Event()
@@ -207,11 +206,11 @@ async def clean_data(self, key: str, val: typing.Any) -> typing.Tuple[str, str]:
async def update(self):
"""Updates the config with data from the cache"""
- await self.api.update_config(self.filter_default(self._cache))
+ await self.bot.api.update_config(self.filter_default(self._cache))
async def refresh(self) -> dict:
"""Refreshes internal cache with data from database"""
- for k, v in (await self.api.get_config()).items():
+ for k, v in (await self.bot.api.get_config()).items():
k = k.lower()
if k in self.all_keys:
self._cache[k] = v
diff --git a/core/utils.py b/core/utils.py
index 16fe1675b0..024c6e0f03 100644
--- a/core/utils.py
+++ b/core/utils.py
@@ -8,8 +8,6 @@
import discord
from discord.ext import commands
-from core.models import PermissionLevel
-
def strtobool(val):
if isinstance(val, bool):
@@ -194,7 +192,9 @@ def match_user_id(text: str) -> int:
return -1
-def get_perm_level(cmd) -> PermissionLevel:
+def get_perm_level(cmd):
+ from core.models import PermissionLevel
+
for check in cmd.checks:
perm = getattr(check, "permission_level", None)
if perm is not None:
From 1f8d2d0a7a1cde45f0c25e0ef34148908e288876 Mon Sep 17 00:00:00 2001
From: Taaku18 <45324516+Taaku18@users.noreply.github.com>
Date: Sat, 27 Jul 2019 19:25:37 -0700
Subject: [PATCH 0338/1221] Snippet/alias help msg
---
CHANGELOG.md | 3 ++-
cogs/utility.py | 40 ++++++++++++++++++++++++++++++++++------
2 files changed, 36 insertions(+), 7 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 66c4d7cf5f..b58143ce87 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -22,6 +22,7 @@ however, insignificant breaking changes does not guarantee a major version bump,
- For users having trouble with pipenv or any other reason.
- Multi-step alias, see `?help alias add`. Public beta testing, might be unstable.
- Misc commands without cogs are now displayed in `?help`.
+- `?help` works for alias and snippets.
### Changes
@@ -39,7 +40,7 @@ however, insignificant breaking changes does not guarantee a major version bump,
- `?notify` no longer carries over to the next thread.
- `discord.NotFound` errors for `on_raw_reaction_add`.
-- `mod_typing` ~~and `user_typing`~~ (`user_typing` is by-design to show now) will no longer show when user is blocked.
+- `mod_typing` ~~and `user_typing`~~ (`user_typing` is now by-design to show) will no longer show when user is blocked.
- Better `?block` usage message.
- Resolves errors when message was sent by mods after thread is closed somehow.
- Recipient join/leave server messages are limited to only the guild set by `GUILD_ID`.
diff --git a/cogs/utility.py b/cogs/utility.py
index 51d5baa1a5..a929fc02a2 100644
--- a/cogs/utility.py
+++ b/cogs/utility.py
@@ -17,7 +17,7 @@
from discord import Embed, Color, Activity, Role
from discord.enums import ActivityType, Status
from discord.ext import commands
-from discord.utils import escape_markdown
+from discord.utils import escape_markdown, escape_mentions
from aiohttp import ClientResponseError
from pkg_resources import parse_version
@@ -180,12 +180,40 @@ async def send_group_help(self, group):
await self.get_destination().send(embed=embed)
- async def send_error_message(self, msg): # pylint: disable=W0221
- logger.warning("CommandNotFound: %s", str(msg))
+ async def send_error_message(self, error):
+ command = self.context.kwargs.get("command")
+ val = self.context.bot.snippets.get(command)
+ if val is not None:
+ return await self.get_destination().send(escape_mentions(f'**`{command}` is a snippet, '
+ f'content:**\n\n{val}'))
+
+ val = self.context.bot.aliases.get(command)
+ if val is not None:
+ values = parse_alias(val)
+
+ if len(values) == 1:
+ embed = Embed(
+ title=f"{command} is an alias.",
+ color=self.context.bot.main_color,
+ description=f"`{command}` points to `{escape_markdown(values[0])}`.",
+ )
+ else:
+ embed = Embed(
+ title=f"{command} is an alias.",
+ color=self.context.bot.main_color,
+ description=f"**`{command}` points to the following steps:**",
+ )
+ for i, val in enumerate(values, start=1):
+ embed.description += f"\n{i}: {escape_markdown(val)}"
+ embed.set_footer(text=f'Type "{self.clean_prefix}{self.command_attrs["name"]} alias" for more '
+ 'details on aliases.')
+ return await self.get_destination().send(embed=embed)
+
+ logger.warning("CommandNotFound: %s", str(error))
embed = Embed(color=Color.red())
embed.set_footer(
- text=f'Command/Category "{self.context.kwargs.get("command")}" not found.'
+ text=f'Command/Category "{command}" not found.'
)
choices = set()
@@ -193,7 +221,7 @@ async def send_error_message(self, msg): # pylint: disable=W0221
for name, cmd in self.context.bot.all_commands.items():
if not cmd.hidden:
choices.add(name)
- command = self.context.kwargs.get("command")
+
closest = get_close_matches(command, choices)
if closest:
embed.add_field(
@@ -870,7 +898,7 @@ async def alias(self, ctx, *, name: str.lower = None):
else:
embed = Embed(
color=self.bot.main_color,
- description=f"`{name}` points to the following steps:",
+ description=f"**`{name}` points to the following steps:**",
)
for i, val in enumerate(values, start=1):
embed.description += f"\n{i}: {escape_markdown(val)}"
From 310f65210a211c4b94a350d0b01c9ecd5b5ec88e Mon Sep 17 00:00:00 2001
From: Taaku18 <45324516+Taaku18@users.noreply.github.com>
Date: Sun, 28 Jul 2019 12:55:10 -0700
Subject: [PATCH 0339/1221] Config help
---
CHANGELOG.md | 3 +
cogs/utility.py | 53 +++++-
core/config.py | 14 +-
core/config_help.json | 374 ++++++++++++++++++++++++++++++++++++++++++
4 files changed, 438 insertions(+), 6 deletions(-)
create mode 100644 core/config_help.json
diff --git a/CHANGELOG.md b/CHANGELOG.md
index b58143ce87..c473256b57 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -23,6 +23,7 @@ however, insignificant breaking changes does not guarantee a major version bump,
- Multi-step alias, see `?help alias add`. Public beta testing, might be unstable.
- Misc commands without cogs are now displayed in `?help`.
- `?help` works for alias and snippets.
+- `?config help ` shows a help embed for the configuration.
### Changes
@@ -35,6 +36,7 @@ however, insignificant breaking changes does not guarantee a major version bump,
- `?plugin registry page-number` plugin registry can specify a page number for quick access.
- A reworked interface for `?snippet` and `?alias`.
- Add an `?snippet raw ` command for viewing the raw content of a snippet (escaped markdown).
+- The placeholder channel for the streaming status changed to https://www.twitch.tv/discordmodmail/.
### Fixes
@@ -45,6 +47,7 @@ however, insignificant breaking changes does not guarantee a major version bump,
- Resolves errors when message was sent by mods after thread is closed somehow.
- Recipient join/leave server messages are limited to only the guild set by `GUILD_ID`.
- When creating snippets and aliases, it now checks if another snippets/aliases with the same name exists.
+- Was looking for `config.json` in the wrong directory.
### Internal
diff --git a/cogs/utility.py b/cogs/utility.py
index a929fc02a2..4914663b2b 100644
--- a/cogs/utility.py
+++ b/cogs/utility.py
@@ -733,11 +733,14 @@ async def config(self, ctx):
Type `{prefix}config options` to view a list
of valid configuration variables.
+ Type `{prefix}config help config-name` for info
+ on a config.
+
To set a configuration variable:
- - `{prefix}config set varname value here`
+ - `{prefix}config set config-name value here`
To remove a configuration variable:
- - `{prefix}config remove varname`
+ - `{prefix}config remove config-name`
"""
await ctx.send_help(ctx.command)
@@ -849,6 +852,52 @@ async def config_get(self, ctx, key: str.lower = None):
return await ctx.send(embed=embed)
+ @config.command(name="help", aliases=["info"])
+ @checks.has_permissions(PermissionLevel.OWNER)
+ async def config_help(self, ctx, key: str.lower):
+ """
+ Show information on a specified configuration.
+ """
+ if key not in self.bot.config.public_keys:
+ embed = Embed(
+ title="Error",
+ color=Color.red(),
+ description=f"`{key}` is an invalid key.",
+ )
+ return await ctx.send(embed=embed)
+
+ config_help = self.bot.config.config_help
+ info = config_help.get(key)
+
+ if info is None:
+ embed = Embed(
+ title="Error",
+ color=Color.red(),
+ description=f"No help details found for `{key}`.",
+ )
+ return await ctx.send(embed=embed)
+
+ def fmt(val):
+ return val.format(prefix=self.bot.prefix, bot=self.bot)
+
+ embed = Embed(
+ title=f"Configuration description on {key}:",
+ color=self.bot.main_color
+ )
+ embed.add_field(name='Default:', value=fmt(info['default']), inline=False)
+ embed.add_field(name='Information:', value=fmt(info['description']), inline=False)
+ example_text = ''
+ for example in info['examples']:
+ example_text += f'- {fmt(example)}\n'
+ embed.add_field(name='Example(s):', value=example_text, inline=False)
+
+ note_text = ''
+ for note in info['notes']:
+ note_text += f'- {fmt(note)}\n'
+ if note_text:
+ embed.add_field(name='Notes(s):', value=note_text, inline=False)
+ return await ctx.send(embed=embed)
+
@commands.group(aliases=["aliases"], invoke_without_command=True)
@checks.has_permissions(PermissionLevel.MODERATOR)
async def alias(self, ctx, *, name: str.lower = None):
diff --git a/core/config.py b/core/config.py
index d99d762307..33e493eb0b 100644
--- a/core/config.py
+++ b/core/config.py
@@ -24,7 +24,7 @@ class ConfigManager:
public_keys = {
# activity
- "twitch_url": "https://www.twitch.tv/discord-modmail/",
+ "twitch_url": "https://www.twitch.tv/discordmodmail/",
# bot settings
"main_category_id": None,
"prefix": "?",
@@ -55,8 +55,8 @@ class ConfigManager:
"thread_self_close_response": "You have closed this Modmail thread.",
# moderation
"recipient_color": str(discord.Color.gold()),
- "mod_tag": None,
"mod_color": str(discord.Color.green()),
+ "mod_tag": None,
# anonymous message
"anon_username": None,
"anon_avatar_url": None,
@@ -117,6 +117,7 @@ def __init__(self, bot):
self.bot = bot
self._cache = {}
self.ready_event = asyncio.Event()
+ self.config_help = {}
def __repr__(self):
return repr(self._cache)
@@ -129,7 +130,7 @@ def populate_cache(self) -> dict:
{k.lower(): v for k, v in os.environ.items() if k.lower() in self.all_keys}
)
configjson = os.path.join(
- os.path.dirname(os.path.abspath(__file__)), "config.json"
+ os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "config.json"
)
if os.path.exists(configjson):
logger.debug("Loading envs from config.json.")
@@ -147,8 +148,13 @@ def populate_cache(self) -> dict:
logger.critical(
"Failed to load config.json env values.", exc_info=True
)
-
self._cache = data
+
+ confighelpjson = os.path.join(
+ os.path.dirname(os.path.abspath(__file__)), "config_help.json"
+ )
+ with open(confighelpjson, "r") as f:
+ self.config_help = json.load(f)
return self._cache
async def clean_data(self, key: str, val: typing.Any) -> typing.Tuple[str, str]:
diff --git a/core/config_help.json b/core/config_help.json
new file mode 100644
index 0000000000..a5b385b431
--- /dev/null
+++ b/core/config_help.json
@@ -0,0 +1,374 @@
+{
+ "twitch_url": {
+ "default": "`https://www.twitch.tv/discordmodmail/`",
+ "description": "This channel dictates the linked Twitch channel when the activity is set to \"Streaming\".",
+ "examples": [
+ "`{prefix}config set twitch_url https://www.twitch.tv/yourchannelname/`"
+ ],
+ "notes": [
+ "This has no effect when the activity is not set to \"Streaming\".",
+ "See also: `{prefix}help activity`."
+ ]
+ },
+ "main_category_id": {
+ "default": "`Modmail` (created with `{prefix}setup`)",
+ "description": "This is the category where all new threads will be created.\n\nTo change the Modmail category, you will need to find the [category’s ID](https://support.discordapp.com/hc/en-us/articles/206346498).",
+ "examples": [
+ "`{prefix}config set main_category_id 9234932582312` (`9234932582312` is the category ID)"
+ ],
+ "notes": [
+ "If the Modmail category ended up being non-existent/invalid, Modmail will break. To fix this, run `{prefix}setup` again or set `main_category_id` to a valid category."
+ ]
+ },
+ "prefix": {
+ "default": "`?`",
+ "description": "The prefix of the bot.",
+ "examples": [
+ "`{prefix}prefix !`",
+ "`{prefix}config set prefix !`"
+ ],
+ "notes": [
+ "If you forgot the bot prefix, Modmail will always respond to its mention (ping)."
+ ]
+ },
+ "mention": {
+ "default": "@here",
+ "description": "This is the message above user information for when a new thread is created in the channel.",
+ "examples": [
+ "`{prefix}config set mention Yo~ Here's a new thread for ya!`",
+ "`{prefix}mention Yo~ Here's a new thread for ya!`"
+ ],
+ "notes": [
+ "Unfortunately, its not currently possible to disable mention."
+ ]
+ },
+ "main_color": {
+ "default": "Discord Blurple ([#7289DA](https://placehold.it/100/7289da?text=+))",
+ "description": "This is the main color for Modmail (help/about/ping embed messages, subscribe, move, etc.).",
+ "examples": [
+ "`{prefix}config set main_color olive green`",
+ "`{prefix}config set main_color 12de3a`",
+ "`{prefix}config set main_color #12de3a`",
+ "`{prefix}config set main_color fff`"
+ ],
+ "notes": [
+ "Available color names can be found on [Taki's Blog](https://taaku18.github.io/modmail/colors/).",
+ "See also: `mod_color`, `recipient_color`."
+ ]
+ },
+ "user_typing": {
+ "default": "No",
+ "description": "When this is set to `yes`, whenever the recipient user starts to type in their DM channel, the moderator will see “{bot.user.display_name} is typing…” in the thread channel.",
+ "examples": [
+ "`{prefix}config set user_typing yes`",
+ "`{prefix}config set user_typing no`"
+ ],
+ "notes": [
+ "See also: `mod_typing`."
+ ]
+ },
+ "mod_typing": {
+ "default": "No",
+ "description": "When this is set to `yes`, whenever a moderator starts to type in the thread channel, the recipient user will see \"{bot.user.display_name} is typing…\" in their DM channel.",
+ "examples": [
+ "`{prefix}config set mod_typing yes`",
+ "`{prefix}config set mod_typing no`"
+ ],
+ "notes": [
+ "See also: `mod_typing`."
+ ]
+ },
+ "account_age": {
+ "default": "No age threshold",
+ "description": "The creation date of the recipient user account must be greater than the number of days, hours, minutes or any time-interval specified by this configuration.",
+ "examples": [
+ "`{prefix}config set account_age P12DT3H` (stands for 12 days and 3 hours in [ISO-8601 Duration Format](https://en.wikipedia.org/wiki/ISO_8601#Durations))",
+ "`{prefix}config set account_age 3 days and 5 hours` (accepted readable time)"
+ ],
+ "notes": [
+ "To remove this restriction, do `{prefix}config del account_age`.",
+ "See also: `guild_age`."
+ ]
+ },
+ "guild_age": {
+ "default": "No age threshold",
+ "description": "The join date of the recipient user into this server must be greater than the number of days, hours, minutes or any time-interval specified by this configuration.",
+ "examples": [
+ "`{prefix}config set guild_age P12DT3H` (stands for 12 days and 3 hours in [ISO-8601 Duration Format](https://en.wikipedia.org/wiki/ISO_8601#Durations))",
+ "`{prefix}config set guild_age 3 days and 5 hours` (accepted readable time)"
+ ],
+ "notes": [
+ "To remove this restriction, do `{prefix}config del guild_age`.",
+ "See also: `account_age`."
+ ]
+ },
+ "reply_without_command": {
+ "default": "Disabled",
+ "description": "Setting this configuration will make all non-command messages sent in the thread channel to be forwarded to the recipient without the need of `{prefix}reply`.",
+ "examples": [
+ "`{prefix}config set reply_without_command yes`",
+ "`{prefix}config set reply_without_command no`"
+ ],
+ "notes": [
+ "Unfortunately, anonymous `reply_without_command` is currently not possible."
+ ]
+ },
+ "log_channel_id": {
+ "default": "`#bot-logs` (created with `{prefix}setup`)",
+ "description": "This is the channel where all log messages will be sent (ie. thread close message, update message, etc.).\n\nTo change the log channel, you will need to find the [channel’s ID](https://support.discordapp.com/hc/en-us/articles/206346498). The channel doesn’t necessary have to be under the `main_category`.",
+ "examples": [
+ "`{prefix}config set log_channel_id 9234932582312` (9234932582312 is the channel ID)"
+ ],
+ "notes": [
+ "If the Modmail logging channel ended up being non-existent/invalid, no logs will be sent."
+ ]
+ },
+ "sent_emoji": {
+ "default": "✅",
+ "description": "This is the emoji added to the message when when a Modmail action is invoked successfully (ie. DM Modmail, edit message, etc.).",
+ "examples": [
+ "`{prefix}config set sent_emoji ✨`"
+ ],
+ "notes": [
+ "You can disable `sent_emoji` with `{prefix}config set sent_emoji disable`.",
+ "Custom/animated emojis are also supported, however, the emoji must be added to the server.",
+ "See also: `blocked_emoji`."
+ ]
+ },
+ "blocked_emoji": {
+ "default": "🚫",
+ "description": "This is the emoji added to the message when when a Modmail action is invoked unsuccessfully (ie. DM Modmail when blocked, failed to reply, etc.).",
+ "examples": [
+ "`{prefix}config set blocked_emoji 🙅`"
+ ],
+ "notes": [
+ "You can disable `blocked_emoji` with `{prefix}config set blocked_emoji disable`.",
+ "Custom/animated emojis are also supported, however, the emoji must be added to the server.",
+ "See also: `sent_emoji`."
+ ]
+ },
+ "close_emoji": {
+ "default": "🔒",
+ "description": "This is the emoji the recipient can click to close a thread themselves. The emoji is automatically added to the `thread_creation_response` embed.",
+ "examples": [
+ "`{prefix}config set close_emoji 👍`"
+ ],
+ "notes": [
+ "This will only have an effect when `recipient_thread_close` is enabled.",
+ "See also: `recipient_thread_close`."
+ ]
+ },
+ "recipient_thread_close": {
+ "default": "No",
+ "description": "Setting this configuration will allow recipients to use the `close_emoji` to close the thread themselves.",
+ "examples": [
+ "`{prefix}config set reply_without_command yes`",
+ "`{prefix}config set reply_without_command no`"
+ ],
+ "notes": [
+ "The close emoji is dictated by the configuration `close_emoji`.",
+ "See also: `close_emoji`."
+ ]
+ },
+ "thread_auto_close_silently": {
+ "default": "No",
+ "description": "Setting this configuration will close silently when the thread auto-closes.",
+ "examples": [
+ "`{prefix}config set thread_auto_close_silently yes`",
+ "`{prefix}config set thread_auto_close_silently no`"
+ ],
+ "notes": [
+ "This will only have an effect when `thread_auto_close` is set.",
+ "See also: `thread_auto_close`."
+ ]
+ },
+ "thread_auto_close": {
+ "default": "Never",
+ "description": "Setting this configuration will close threads automatically after the number of days, hours, minutes or any time-interval specified by this configuration.",
+ "examples": [
+ "`{prefix}config set thread_auto_close P12DT3H` (stands for 12 days and 3 hours in [ISO-8601 Duration Format](https://en.wikipedia.org/wiki/ISO_8601#Durations))",
+ "`{prefix}config set thread_auto_close 3 days and 5 hours` (accepted readable time)"
+ ],
+ "notes": [
+ "To disable auto close, do `{prefix}config del thread_auto_close`.",
+ "To prevent a thread from auto-closing, do `{prefix}close cancel`.",
+ "See also: `thread_auto_close_silently`, `thread_auto_close_response`."
+ ]
+ },
+ "thread_auto_close_response": {
+ "default": "\"This thread has been closed automatically due to inactivity after {{timeout}}.\"",
+ "description": "This is the message to display when the thread when the thread auto-closes.",
+ "examples": [
+ "`{prefix}config set thread_auto_close_response Your close message here.`"
+ ],
+ "notes": [
+ "Its possible to use `{{timeout}}` as a placeholder for a formatted timeout text.",
+ "This will not have an effect when `thread_auto_close_silently` is enabled.",
+ "Discord flavoured markdown is fully supported in `thread_auto_close_response`.",
+ "See also: `thread_auto_close`, `thread_auto_close_silently`."
+ ]
+ },
+ "thread_creation_response": {
+ "default": "\"The staff team will get back to you as soon as possible.\"",
+ "description": "This is the message embed content sent to the recipient upon the creation of a new thread.",
+ "examples": [
+ "`{prefix}config set thread_creation_response You will be contacted shortly.`"
+ ],
+ "notes": [
+ "Discord flavoured markdown is fully supported in `thread_creation_response`.",
+ "See also: `thread_creation_title`, `thread_creation_footer`, `thread_close_response`."
+ ]
+ },
+ "thread_creation_footer": {
+ "default": "\"Your message has been sent\"",
+ "description": "This is the message embed footer sent to the recipient upon the creation of a new thread.",
+ "examples": [
+ "`{prefix}config set thread_creation_footer Please Hold...`"
+ ],
+ "notes": [
+ "This is used in place of `thread_self_closable_creation_footer` when `recipient_thread_close` is enabled.",
+ "See also: `thread_creation_title`, `thread_creation_response`, `thread_self_closable_creation_footer`, `thread_close_footer`."
+ ]
+ },
+ "thread_self_closable_creation_footer": {
+ "default": "\"Click the lock to close the thread\"",
+ "description": "This is the message embed footer sent to the recipient upon the creation of a new thread.",
+ "examples": [
+ "`{prefix}config set thread_self_closable_creation_footer Please Hold...`"
+ ],
+ "notes": [
+ "This is used in place of `thread_creation_footer` when `recipient_thread_close` is disabled.",
+ "See also: `thread_creation_title`, `thread_creation_response`, `thread_creation_footer`."
+ ]
+ },
+ "thread_creation_title": {
+ "default": "\"Thread Created\"",
+ "description": "This is the message embed title sent to the recipient upon the creation of a new thread.",
+ "examples": [
+ "`{prefix}config set thread_creation_title Hello!`"
+ ],
+ "notes": [
+ "See also: `thread_creation_response`, `thread_creation_footer`, `thread_close_title`."
+ ]
+ },
+ "thread_close_footer": {
+ "default": "\"Replying will create a new thread\"",
+ "description": "This is the message embed footer sent to the recipient upon the closure of a thread.",
+ "examples": [
+ "`{prefix}config set thread_close_footer Bye!`"
+ ],
+ "notes": [
+ "See also: `thread_close_title`, `thread_close_response`, `thread_creation_footer`."
+ ]
+ },
+ "thread_close_title": {
+ "default": "\"Thread Closed\"",
+ "description": "This is the message embed title sent to the recipient upon the closure of a thread.",
+ "examples": [
+ "`{prefix}config set thread_close_title Farewell!`"
+ ],
+ "notes": [
+ "See also: `thread_close_response`, `thread_close_footer`, `thread_creation_title`."
+ ]
+ },
+ "thread_close_response": {
+ "default": "\"{{closer.mention}} has closed this Modmail thread\"",
+ "description": "This is the message embed content sent to the recipient upon the closure of a thread.",
+ "examples": [
+ "`{prefix}config set thread_close_response Your message is appreciated!`"
+ ],
+ "notes": [
+ "When `recipient_thread_close` is enabled and the recipient closed their own thread, `thread_self_close_response` is used instead of this configuration.",
+ "You may use the `{{closer}}` variable for access to the [Member](https://discordpy.readthedocs.io/en/latest/api.html#discord.Member) that closed the thread.",
+ "`{{loglink}}` can be used as a placeholder substitute for the full URL linked to the thread in the log viewer and `{{loglink}}` for the unique key (ie. 3ifdm9204) of the log.",
+ "Discord flavoured markdown is fully supported in `thread_close_response`.",
+ "See also: `thread_close_title`, `thread_close_footer`, `thread_self_close_response`, `thread_creation_response`."
+ ]
+ },
+ "thread_self_close_response": {
+ "default": "\"You have closed this Modmail thread.\"",
+ "description": "This is the message embed content sent to the recipient upon the closure of a their own thread.",
+ "examples": [
+ "`{prefix}config set thread_self_close_response You have closed your own thread...`"
+ ],
+ "notes": [
+ "When `recipient_thread_close` is disabled or the thread wasn't closed by the recipient, `thread_close_response` is used instead of this configuration.",
+ "You may use the `{{closer}}` variable for access to the [Member](https://discordpy.readthedocs.io/en/latest/api.html#discord.Member) that closed the thread.",
+ "`{{loglink}}` can be used as a placeholder substitute for the full URL linked to the thread in the log viewer and `{{loglink}}` for the unique key (ie. 3ifdm9204) of the log.",
+ "Discord flavoured markdown is fully supported in `thread_self_close_response`.",
+ "See also: `thread_close_title`, `thread_close_footer`, `thread_close_response`."
+ ]
+ },
+ "recipient_color": {
+ "default": "Discord Gold ([#F1C40F](https://placehold.it/100/f1c40f?text=+))",
+ "description": "This is the color of the messages sent by the recipient, this applies to messages received in the thread channel.",
+ "examples": [
+ "`{prefix}config set recipient_color dark beige`",
+ "`{prefix}config set recipient_color cb7723`",
+ "`{prefix}config set recipient_color #cb7723`",
+ "`{prefix}config set recipient_color c4k`"
+ ],
+ "notes": [
+ "Available color names can be found on [Taki's Blog](https://taaku18.github.io/modmail/colors/).",
+ "See also: `main_color`, `mod_color`."
+ ]
+ },
+ "mod_color": {
+ "default": "Discord Green ([#2ECC71](https://placehold.it/100/2ecc71?text=+))",
+ "description": "This is the color of the messages sent by the moderators, this applies to messages within in the thread channel and the DM thread messages received by the recipient.",
+ "examples": [
+ "`{prefix}config set mod_color dark beige`",
+ "`{prefix}config set mod_color cb7723`",
+ "`{prefix}config set mod_color #cb7723`",
+ "`{prefix}config set mod_color c4k`"
+ ],
+ "notes": [
+ "Available color names can be found on [Taki's Blog](https://taaku18.github.io/modmail/colors/).",
+ "See also: `main_color`, `recipient_color`."
+ ]
+ },
+ "mod_tag": {
+ "default": "The moderator's highest role",
+ "description": "This is the name tag in the “footer” section of the embeds sent by moderators in the recipient DM and thread channel.",
+ "examples": [
+ "`{prefix}config set mod_tag Moderator`"
+ ],
+ "notes": [
+ "When the message is sent anonymously, `anon_tag` is used instead.",
+ "See also: `anon_tag`."
+ ]
+ },
+ "anon_username": {
+ "default": "Fallback on `mod_tag`",
+ "description": "This is the name in the “author” section of the embeds sent by anonymous moderators in the recipient DM.",
+ "examples": [
+ "`{prefix}config set anon_username Incognito Mod`"
+ ],
+ "notes": [
+ "A detailed labeling of anonymous message configurations: https://i.imgur.com/SKOC42Z.png.",
+ "See also: `anon_avatar_url`, `anon_tag`."
+ ]
+ },
+ "anon_avatar_url": {
+ "default": "Server avatar",
+ "description": "This is the avatar of the embeds sent by anonymous moderators in the recipient DM.",
+ "examples": [
+ "`{prefix}config set anon_avatar_url https://path.to/your/avatar.png` (you will need to upload the avatar to somewhere)"
+ ],
+ "notes": [
+ "A detailed labeling of anonymous message configurations: https://i.imgur.com/SKOC42Z.png.",
+ "See also: `anon_username`, `anon_tag`."
+ ]
+ },
+ "anon_tag": {
+ "default": "\"Response\"",
+ "description": "This is the name tag in the “footer” section of the embeds sent by anonymous moderators in the recipient DM.",
+ "examples": [
+ "`{prefix}config set anon_tag Support Agent`"
+ ],
+ "notes": [
+ "A detailed labeling of anonymous message configurations: https://i.imgur.com/SKOC42Z.png.",
+ "See also: `anon_avatar_url`, `anon_username`, `mod_tag`."
+ ]
+ }
+}
\ No newline at end of file
From afa3c9ad3bcea4ea480cf1943fe60b2c5316661b Mon Sep 17 00:00:00 2001
From: Taaku18 <45324516+Taaku18@users.noreply.github.com>
Date: Sun, 28 Jul 2019 14:23:41 -0700
Subject: [PATCH 0340/1221] I'm dumb
---
core/checks.py | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/core/checks.py b/core/checks.py
index 42da8cf4b8..3c7f8b701f 100644
--- a/core/checks.py
+++ b/core/checks.py
@@ -81,8 +81,8 @@ async def check_permissions( # pylint: disable=R0911
role.id in level_permissions[level.name] for role in author_roles
)
has_perm_id = ctx.author.id in level_permissions[level.name]
- return has_perm_role or has_perm_id
-
+ if has_perm_role or has_perm_id:
+ return True
return False
From 3afa23e9e53c773cfba75806db8b2e6fa394a3a2 Mon Sep 17 00:00:00 2001
From: Taaku18 <45324516+Taaku18@users.noreply.github.com>
Date: Sun, 28 Jul 2019 14:27:22 -0700
Subject: [PATCH 0341/1221] More fluent English
---
core/config_help.json | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/core/config_help.json b/core/config_help.json
index a5b385b431..1cf4776c8f 100644
--- a/core/config_help.json
+++ b/core/config_help.json
@@ -57,7 +57,7 @@
]
},
"user_typing": {
- "default": "No",
+ "default": "Disabled",
"description": "When this is set to `yes`, whenever the recipient user starts to type in their DM channel, the moderator will see “{bot.user.display_name} is typing…” in the thread channel.",
"examples": [
"`{prefix}config set user_typing yes`",
@@ -68,7 +68,7 @@
]
},
"mod_typing": {
- "default": "No",
+ "default": "Disabled",
"description": "When this is set to `yes`, whenever a moderator starts to type in the thread channel, the recipient user will see \"{bot.user.display_name} is typing…\" in their DM channel.",
"examples": [
"`{prefix}config set mod_typing yes`",
@@ -159,7 +159,7 @@
]
},
"recipient_thread_close": {
- "default": "No",
+ "default": "Disabled",
"description": "Setting this configuration will allow recipients to use the `close_emoji` to close the thread themselves.",
"examples": [
"`{prefix}config set reply_without_command yes`",
From 894ad2fc0272935515b8706fd09facfa4541a5d8 Mon Sep 17 00:00:00 2001
From: Taaku18 <45324516+Taaku18@users.noreply.github.com>
Date: Sun, 28 Jul 2019 14:39:00 -0700
Subject: [PATCH 0342/1221] Numbers carries over in snippet/alias now
---
cogs/modmail.py | 4 ++--
cogs/utility.py | 4 ++--
2 files changed, 4 insertions(+), 4 deletions(-)
diff --git a/cogs/modmail.py b/cogs/modmail.py
index 4eae4037e3..1e8f3f3f95 100644
--- a/cogs/modmail.py
+++ b/cogs/modmail.py
@@ -146,9 +146,9 @@ async def snippet(self, ctx, *, name: str.lower = None):
embeds = []
- for names in zip_longest(*(iter(sorted(self.bot.snippets)),) * 15):
+ for i, names in enumerate(zip_longest(*(iter(sorted(self.bot.snippets)),) * 15)):
description = "\n".join(
- ": ".join((str(a), b))
+ ": ".join((str(a + i * 15), b))
for a, b in enumerate(
takewhile(lambda x: x is not None, names), start=1
)
diff --git a/cogs/utility.py b/cogs/utility.py
index 4914663b2b..2f9d0f60d9 100644
--- a/cogs/utility.py
+++ b/cogs/utility.py
@@ -965,9 +965,9 @@ async def alias(self, ctx, *, name: str.lower = None):
embeds = []
- for names in zip_longest(*(iter(sorted(self.bot.aliases)),) * 15):
+ for i, names in enumerate(zip_longest(*(iter(sorted(self.bot.aliases)),) * 15)):
description = "\n".join(
- ": ".join((str(a), b))
+ ": ".join((str(a + i * 15), b))
for a, b in enumerate(
takewhile(lambda x: x is not None, names), start=1
)
From 7f6992f70c7259543ec5948f22cbc2e48eef9551 Mon Sep 17 00:00:00 2001
From: Taaku18 <45324516+Taaku18@users.noreply.github.com>
Date: Sun, 28 Jul 2019 15:41:42 -0700
Subject: [PATCH 0343/1221] Displays image/thumbnail for config help
---
cogs/utility.py | 9 ++++++++-
core/config_help.json | 29 ++++++++++++++++-------------
2 files changed, 24 insertions(+), 14 deletions(-)
diff --git a/cogs/utility.py b/cogs/utility.py
index 2f9d0f60d9..86f1866796 100644
--- a/cogs/utility.py
+++ b/cogs/utility.py
@@ -895,7 +895,14 @@ def fmt(val):
for note in info['notes']:
note_text += f'- {fmt(note)}\n'
if note_text:
- embed.add_field(name='Notes(s):', value=note_text, inline=False)
+ embed.add_field(name='Note(s):', value=note_text, inline=False)
+
+ if info.get('image') is not None:
+ embed.set_image(url=fmt(info['image']))
+
+ if info.get('thumbnail') is not None:
+ embed.set_thumbnail(url=fmt(info['thumbnail']))
+
return await ctx.send(embed=embed)
@commands.group(aliases=["aliases"], invoke_without_command=True)
diff --git a/core/config_help.json b/core/config_help.json
index 1cf4776c8f..37106f286a 100644
--- a/core/config_help.json
+++ b/core/config_help.json
@@ -43,7 +43,7 @@
]
},
"main_color": {
- "default": "Discord Blurple ([#7289DA](https://placehold.it/100/7289da?text=+))",
+ "default": "Discord Blurple [#7289DA](https://placehold.it/100/7289da?text=+)",
"description": "This is the main color for Modmail (help/about/ping embed messages, subscribe, move, etc.).",
"examples": [
"`{prefix}config set main_color olive green`",
@@ -54,7 +54,8 @@
"notes": [
"Available color names can be found on [Taki's Blog](https://taaku18.github.io/modmail/colors/).",
"See also: `mod_color`, `recipient_color`."
- ]
+ ],
+ "thumbnail": "https://placehold.it/100/7289da?text=+"
},
"user_typing": {
"default": "Disabled",
@@ -285,7 +286,7 @@
"See also: `thread_close_title`, `thread_close_footer`, `thread_self_close_response`, `thread_creation_response`."
]
},
- "thread_self_close_response": {
+ "thread_self_close_response": {
"default": "\"You have closed this Modmail thread.\"",
"description": "This is the message embed content sent to the recipient upon the closure of a their own thread.",
"examples": [
@@ -300,7 +301,7 @@
]
},
"recipient_color": {
- "default": "Discord Gold ([#F1C40F](https://placehold.it/100/f1c40f?text=+))",
+ "default": "Discord Gold [#F1C40F](https://placehold.it/100/f1c40f?text=+)",
"description": "This is the color of the messages sent by the recipient, this applies to messages received in the thread channel.",
"examples": [
"`{prefix}config set recipient_color dark beige`",
@@ -311,10 +312,11 @@
"notes": [
"Available color names can be found on [Taki's Blog](https://taaku18.github.io/modmail/colors/).",
"See also: `main_color`, `mod_color`."
- ]
+ ],
+ "thumbnail": "https://placehold.it/100/f1c40f?text=+"
},
"mod_color": {
- "default": "Discord Green ([#2ECC71](https://placehold.it/100/2ecc71?text=+))",
+ "default": "Discord Green [#2ECC71](https://placehold.it/100/2ecc71?text=+)",
"description": "This is the color of the messages sent by the moderators, this applies to messages within in the thread channel and the DM thread messages received by the recipient.",
"examples": [
"`{prefix}config set mod_color dark beige`",
@@ -325,7 +327,8 @@
"notes": [
"Available color names can be found on [Taki's Blog](https://taaku18.github.io/modmail/colors/).",
"See also: `main_color`, `recipient_color`."
- ]
+ ],
+ "thumbnail": "https://placehold.it/100/2ecc71?text=+"
},
"mod_tag": {
"default": "The moderator's highest role",
@@ -345,9 +348,9 @@
"`{prefix}config set anon_username Incognito Mod`"
],
"notes": [
- "A detailed labeling of anonymous message configurations: https://i.imgur.com/SKOC42Z.png.",
"See also: `anon_avatar_url`, `anon_tag`."
- ]
+ ],
+ "image": "https://i.imgur.com/SKOC42Z.png"
},
"anon_avatar_url": {
"default": "Server avatar",
@@ -356,9 +359,9 @@
"`{prefix}config set anon_avatar_url https://path.to/your/avatar.png` (you will need to upload the avatar to somewhere)"
],
"notes": [
- "A detailed labeling of anonymous message configurations: https://i.imgur.com/SKOC42Z.png.",
"See also: `anon_username`, `anon_tag`."
- ]
+ ],
+ "image": "https://i.imgur.com/SKOC42Z.png"
},
"anon_tag": {
"default": "\"Response\"",
@@ -367,8 +370,8 @@
"`{prefix}config set anon_tag Support Agent`"
],
"notes": [
- "A detailed labeling of anonymous message configurations: https://i.imgur.com/SKOC42Z.png.",
"See also: `anon_avatar_url`, `anon_username`, `mod_tag`."
- ]
+ ],
+ "image": "https://i.imgur.com/SKOC42Z.png"
}
}
\ No newline at end of file
From 106d4a6fcbda21d24c11f4e16cfa5f4676a9d2e0 Mon Sep 17 00:00:00 2001
From: Stephen <48072084+StephenDaDev@users.noreply.github.com>
Date: Sun, 28 Jul 2019 22:06:53 -0400
Subject: [PATCH 0344/1221] Fix Expired Invite (#334)
---
SPONSORS.json | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/SPONSORS.json b/SPONSORS.json
index 989f1dfdfd..5f79ff80ea 100644
--- a/SPONSORS.json
+++ b/SPONSORS.json
@@ -21,7 +21,7 @@
},
{
"name": "Discord Server!",
- "value": "[**Click Here**](https://discord.gg/s8Ddphx)"
+ "value": "[**Click Here**](https://discord.gg/V8ErqHb)"
}
]
}
From b4b9ec56d25c583de78196b2640d980d1d7d147d Mon Sep 17 00:00:00 2001
From: Kyber
Date: Mon, 29 Jul 2019 13:39:08 +1000
Subject: [PATCH 0345/1221] Add dockerfile
---
Dockerfile | 5 +++++
1 file changed, 5 insertions(+)
create mode 100644 Dockerfile
diff --git a/Dockerfile b/Dockerfile
new file mode 100644
index 0000000000..d33f146c9e
--- /dev/null
+++ b/Dockerfile
@@ -0,0 +1,5 @@
+FROM kennethreitz/pipenv
+
+COPY . /app
+
+CMD python3 bot.py
From bdb2bcf71da23176f16b65e42b0410e1054bc3c4 Mon Sep 17 00:00:00 2001
From: Taaku18 <45324516+Taaku18@users.noreply.github.com>
Date: Sun, 28 Jul 2019 21:49:09 -0700
Subject: [PATCH 0346/1221] Paginate config options and patched a thread open
bug
---
CHANGELOG.md | 2 +
cogs/modmail.py | 4 +-
cogs/plugins.py | 2 +-
cogs/utility.py | 111 ++++++++++++++++++++++++++++--------------------
core/config.py | 15 ++++---
core/thread.py | 2 +-
6 files changed, 81 insertions(+), 55 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index c473256b57..b0c5110ce5 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -37,6 +37,8 @@ however, insignificant breaking changes does not guarantee a major version bump,
- A reworked interface for `?snippet` and `?alias`.
- Add an `?snippet raw ` command for viewing the raw content of a snippet (escaped markdown).
- The placeholder channel for the streaming status changed to https://www.twitch.tv/discordmodmail/.
+- Removed unclear `rm` alias for some `remove` commands.
+- Paginate `?config options`.
### Fixes
diff --git a/cogs/modmail.py b/cogs/modmail.py
index 1e8f3f3f95..7949688577 100644
--- a/cogs/modmail.py
+++ b/cogs/modmail.py
@@ -146,7 +146,9 @@ async def snippet(self, ctx, *, name: str.lower = None):
embeds = []
- for i, names in enumerate(zip_longest(*(iter(sorted(self.bot.snippets)),) * 15)):
+ for i, names in enumerate(
+ zip_longest(*(iter(sorted(self.bot.snippets)),) * 15)
+ ):
description = "\n".join(
": ".join((str(a + i * 15), b))
for a, b in enumerate(
diff --git a/cogs/plugins.py b/cogs/plugins.py
index 4f204ef10e..6ed1dccf1b 100644
--- a/cogs/plugins.py
+++ b/cogs/plugins.py
@@ -260,7 +260,7 @@ async def plugin_add(self, ctx, *, plugin_name: str):
)
await ctx.send(embed=embed)
- @plugin.command(name="remove", aliases=["del", "delete", "rm"])
+ @plugin.command(name="remove", aliases=["del", "delete"])
@checks.has_permissions(PermissionLevel.OWNER)
async def plugin_remove(self, ctx, *, plugin_name: str):
"""Remove a plugin."""
diff --git a/cogs/utility.py b/cogs/utility.py
index 86f1866796..d90bd390b5 100644
--- a/cogs/utility.py
+++ b/cogs/utility.py
@@ -63,21 +63,22 @@ async def format_cog_help(self, cog, *, no_cog=False):
embeds = []
for format_ in formats:
- description = cog.description or "No description." \
- if not no_cog else "Miscellaneous commands without a category."
- embed = Embed(
- description=f'*{description}*',
- color=bot.main_color,
+ description = (
+ cog.description or "No description."
+ if not no_cog
+ else "Miscellaneous commands without a category."
)
+ embed = Embed(description=f"*{description}*", color=bot.main_color)
embed.add_field(name="Commands", value=format_ or "No commands.")
continued = " (Continued)" if embeds else ""
- name = cog.qualified_name + " - Help" if not no_cog else "Miscellaneous Commands"
- embed.set_author(
- name=name + continued,
- icon_url=bot.user.avatar_url,
+ name = (
+ cog.qualified_name + " - Help"
+ if not no_cog
+ else "Miscellaneous Commands"
)
+ embed.set_author(name=name + continued, icon_url=bot.user.avatar_url)
embed.set_footer(
text=f'Type "{prefix}{self.command_attrs["name"]} command" '
@@ -184,8 +185,9 @@ async def send_error_message(self, error):
command = self.context.kwargs.get("command")
val = self.context.bot.snippets.get(command)
if val is not None:
- return await self.get_destination().send(escape_mentions(f'**`{command}` is a snippet, '
- f'content:**\n\n{val}'))
+ return await self.get_destination().send(
+ escape_mentions(f"**`{command}` is a snippet, " f"content:**\n\n{val}")
+ )
val = self.context.bot.aliases.get(command)
if val is not None:
@@ -205,16 +207,16 @@ async def send_error_message(self, error):
)
for i, val in enumerate(values, start=1):
embed.description += f"\n{i}: {escape_markdown(val)}"
- embed.set_footer(text=f'Type "{self.clean_prefix}{self.command_attrs["name"]} alias" for more '
- 'details on aliases.')
+ embed.set_footer(
+ text=f'Type "{self.clean_prefix}{self.command_attrs["name"]} alias" for more '
+ "details on aliases."
+ )
return await self.get_destination().send(embed=embed)
logger.warning("CommandNotFound: %s", str(error))
embed = Embed(color=Color.red())
- embed.set_footer(
- text=f'Command/Category "{command}" not found.'
- )
+ embed.set_footer(text=f'Command/Category "{command}" not found.')
choices = set()
@@ -748,10 +750,20 @@ async def config(self, ctx):
@checks.has_permissions(PermissionLevel.OWNER)
async def config_options(self, ctx):
"""Return a list of valid configuration names you can change."""
- allowed = self.bot.config.public_keys
- valid = ", ".join(f"`{k}`" for k in allowed)
- embed = Embed(title="Valid Keys", description=valid, color=self.bot.main_color)
- return await ctx.send(embed=embed)
+ embeds = []
+ for names in zip_longest(*(iter(sorted(self.bot.config.public_keys)),) * 15):
+ description = "\n".join(
+ f"`{name}`" for name in takewhile(lambda x: x is not None, names)
+ )
+ embed = Embed(
+ title="Available configuration keys:",
+ color=self.bot.main_color,
+ description=description,
+ )
+ embeds.append(embed)
+
+ session = PaginatorSession(ctx, *embeds)
+ await session.run()
@config.command(name="set", aliases=["add"])
@checks.has_permissions(PermissionLevel.OWNER)
@@ -784,7 +796,7 @@ async def config_set(self, ctx, key: str.lower, *, value: str):
return await ctx.send(embed=embed)
- @config.command(name="remove", aliases=["del", "delete", "rm"])
+ @config.command(name="remove", aliases=["del", "delete"])
@checks.has_permissions(PermissionLevel.OWNER)
async def config_remove(self, ctx, key: str.lower):
"""Delete a set configuration variable."""
@@ -867,9 +879,8 @@ async def config_help(self, ctx, key: str.lower):
return await ctx.send(embed=embed)
config_help = self.bot.config.config_help
- info = config_help.get(key)
- if info is None:
+ if key not in config_help:
embed = Embed(
title="Error",
color=Color.red(),
@@ -880,30 +891,40 @@ async def config_help(self, ctx, key: str.lower):
def fmt(val):
return val.format(prefix=self.bot.prefix, bot=self.bot)
- embed = Embed(
- title=f"Configuration description on {key}:",
- color=self.bot.main_color
- )
- embed.add_field(name='Default:', value=fmt(info['default']), inline=False)
- embed.add_field(name='Information:', value=fmt(info['description']), inline=False)
- example_text = ''
- for example in info['examples']:
- example_text += f'- {fmt(example)}\n'
- embed.add_field(name='Example(s):', value=example_text, inline=False)
+ index = 0
+ embeds = []
+ for i, (current_key, info) in enumerate(config_help.items()):
+ if current_key == key:
+ index = i
+ embed = Embed(
+ title=f"Configuration description on {current_key}:",
+ color=self.bot.main_color,
+ )
+ embed.add_field(name="Default:", value=fmt(info["default"]), inline=False)
+ embed.add_field(
+ name="Information:", value=fmt(info["description"]), inline=False
+ )
+ example_text = ""
+ for example in info["examples"]:
+ example_text += f"- {fmt(example)}\n"
+ embed.add_field(name="Example(s):", value=example_text, inline=False)
- note_text = ''
- for note in info['notes']:
- note_text += f'- {fmt(note)}\n'
- if note_text:
- embed.add_field(name='Note(s):', value=note_text, inline=False)
+ note_text = ""
+ for note in info["notes"]:
+ note_text += f"- {fmt(note)}\n"
+ if note_text:
+ embed.add_field(name="Note(s):", value=note_text, inline=False)
- if info.get('image') is not None:
- embed.set_image(url=fmt(info['image']))
+ if info.get("image") is not None:
+ embed.set_image(url=fmt(info["image"]))
- if info.get('thumbnail') is not None:
- embed.set_thumbnail(url=fmt(info['thumbnail']))
+ if info.get("thumbnail") is not None:
+ embed.set_thumbnail(url=fmt(info["thumbnail"]))
+ embeds += [embed]
- return await ctx.send(embed=embed)
+ paginator = PaginatorSession(ctx, *embeds)
+ paginator.current = index
+ await paginator.run()
@commands.group(aliases=["aliases"], invoke_without_command=True)
@checks.has_permissions(PermissionLevel.MODERATOR)
@@ -1265,9 +1286,7 @@ async def permissions_add_level(
return await ctx.send(embed=embed)
@permissions.group(
- name="remove",
- aliases=["del", "delete", "rm", "revoke"],
- invoke_without_command=True,
+ name="remove", aliases=["del", "delete", "revoke"], invoke_without_command=True
)
@checks.has_permissions(PermissionLevel.OWNER)
async def permissions_remove(self, ctx):
diff --git a/core/config.py b/core/config.py
index 33e493eb0b..ae0a1f0a3e 100644
--- a/core/config.py
+++ b/core/config.py
@@ -3,6 +3,7 @@
import logging
import os
import typing
+from collections import namedtuple
from copy import deepcopy
from dotenv import load_dotenv
@@ -129,12 +130,12 @@ def populate_cache(self) -> dict:
data.update(
{k.lower(): v for k, v in os.environ.items() if k.lower() in self.all_keys}
)
- configjson = os.path.join(
+ config_json = os.path.join(
os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "config.json"
)
- if os.path.exists(configjson):
+ if os.path.exists(config_json):
logger.debug("Loading envs from config.json.")
- with open(configjson, "r") as f:
+ with open(config_json, "r") as f:
# Config json should override env vars
try:
data.update(
@@ -150,11 +151,13 @@ def populate_cache(self) -> dict:
)
self._cache = data
- confighelpjson = os.path.join(
+ config_help_json = os.path.join(
os.path.dirname(os.path.abspath(__file__)), "config_help.json"
)
- with open(confighelpjson, "r") as f:
- self.config_help = json.load(f)
+ with open(config_help_json, "r") as f:
+ Entry = namedtuple("Entry", ["index", "embed"])
+ self.config_help = dict(sorted(json.load(f).items()))
+
return self._cache
async def clean_data(self, key: str, val: typing.Any) -> typing.Tuple[str, str]:
diff --git a/core/thread.py b/core/thread.py
index d4213a0c93..bb2f244926 100644
--- a/core/thread.py
+++ b/core/thread.py
@@ -229,7 +229,7 @@ def _format_info_embed(self, user, log_url, log_count, color):
embed.set_author(name=str(user), icon_url=user.avatar_url, url=log_url)
# embed.set_thumbnail(url=avi)
- if member is None:
+ if member is not None:
joined = str((time - member.joined_at).days)
# embed.add_field(name='Joined', value=joined + days(joined))
embed.description += f", joined {days(joined)}"
From c7dd2fe3590f7688143d7761ca3d664eb329f93c Mon Sep 17 00:00:00 2001
From: Taaku18 <45324516+Taaku18@users.noreply.github.com>
Date: Sun, 28 Jul 2019 21:55:42 -0700
Subject: [PATCH 0347/1221] Another bug...
---
core/thread.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/core/thread.py b/core/thread.py
index bb2f244926..12bb71ab3c 100644
--- a/core/thread.py
+++ b/core/thread.py
@@ -193,7 +193,7 @@ def _format_info_embed(self, user, log_url, log_count, color):
# key = log_url.split('/')[-1]
role_names = ""
- if member is None:
+ if member is not None:
sep_server = self.bot.using_multiple_server_setup
separator = ", " if sep_server else " "
From 0ce91d036b2bd1af40e0ce2f9d0d5edbc81f1b82 Mon Sep 17 00:00:00 2001
From: Stephen <48072084+StephenDaDev@users.noreply.github.com>
Date: Sun, 28 Jul 2019 22:06:53 -0400
Subject: [PATCH 0348/1221] Fix Expired Invite (#334)
---
SPONSORS.json | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/SPONSORS.json b/SPONSORS.json
index 989f1dfdfd..5f79ff80ea 100644
--- a/SPONSORS.json
+++ b/SPONSORS.json
@@ -21,7 +21,7 @@
},
{
"name": "Discord Server!",
- "value": "[**Click Here**](https://discord.gg/s8Ddphx)"
+ "value": "[**Click Here**](https://discord.gg/V8ErqHb)"
}
]
}
From fb257d4810f9c0fdb5a3f77509625682ca072c8b Mon Sep 17 00:00:00 2001
From: Kyber
Date: Mon, 29 Jul 2019 13:39:08 +1000
Subject: [PATCH 0349/1221] Add dockerfile
---
Dockerfile | 5 +++++
1 file changed, 5 insertions(+)
create mode 100644 Dockerfile
diff --git a/Dockerfile b/Dockerfile
new file mode 100644
index 0000000000..d33f146c9e
--- /dev/null
+++ b/Dockerfile
@@ -0,0 +1,5 @@
+FROM kennethreitz/pipenv
+
+COPY . /app
+
+CMD python3 bot.py
From 31c116489c1a9d63e99e2041c52200c4592a2461 Mon Sep 17 00:00:00 2001
From: Taaku18 <45324516+Taaku18@users.noreply.github.com>
Date: Mon, 29 Jul 2019 02:19:35 -0700
Subject: [PATCH 0350/1221] perms stuff
---
CHANGELOG.md | 4 +-
cogs/utility.py | 458 +++++++++++++++++++++---------------------------
2 files changed, 198 insertions(+), 264 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index b0c5110ce5..47934e28ff 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -24,6 +24,7 @@ however, insignificant breaking changes does not guarantee a major version bump,
- Misc commands without cogs are now displayed in `?help`.
- `?help` works for alias and snippets.
- `?config help ` shows a help embed for the configuration.
+- Support setting permissions for sub commands.
### Changes
@@ -61,7 +62,8 @@ however, insignificant breaking changes does not guarantee a major version bump,
- Bumped discord.py version to 1.2.3.
- Use discord tasks for metadata loop.
- More debug based logging.
-
+- Reduce redundancies in `?perms` sub commands.
+
# v3.0.3
### Added
diff --git a/cogs/utility.py b/cogs/utility.py
index d90bd390b5..1252341d89 100644
--- a/cogs/utility.py
+++ b/cogs/utility.py
@@ -1209,331 +1209,263 @@ async def permissions(self, ctx):
"""
await ctx.send_help(ctx.command)
- @permissions.group(name="add", invoke_without_command=True)
- @checks.has_permissions(PermissionLevel.OWNER)
- async def permissions_add(self, ctx):
- """Add a permission to a command or a permission level."""
- await ctx.send_help(ctx.command)
-
- @permissions_add.command(name="command")
- @checks.has_permissions(PermissionLevel.OWNER)
- async def permissions_add_command(
- self, ctx, command: str, *, user_or_role: Union[User, Role, str]
- ):
- """
- Add a user, role, or everyone permission to use a command.
-
- Do not ping `@everyone` for granting permission to everyone, use "everyone" or "all" instead,
- `user_or_role` may be a role ID, name, mention, user ID, name, mention, "all", or "everyone".
- """
- if command not in self.bot.all_commands:
- embed = Embed(
- title="Error",
- color=Color.red(),
- description="The command you are attempting to point "
- f"to does not exist: `{command}`.",
- )
- return await ctx.send(embed=embed)
-
+ @staticmethod
+ def _verify_user_or_role(user_or_role):
if hasattr(user_or_role, "id"):
- value = user_or_role.id
+ return user_or_role.id
elif user_or_role in {"everyone", "all"}:
- value = -1
+ return -1
else:
raise commands.BadArgument(f'User or Role "{user_or_role}" not found')
- await self.bot.update_perms(self.bot.all_commands[command].name, value)
- embed = Embed(
- title="Success",
- color=self.bot.main_color,
- description=f"Permission for {command} was successfully updated.",
- )
- return await ctx.send(embed=embed)
-
- @permissions_add.command(name="level", aliases=["group"])
+ @permissions.command(name="add", usage="[command/level] [name] [user_or_role]")
@checks.has_permissions(PermissionLevel.OWNER)
- async def permissions_add_level(
- self, ctx, level: str, *, user_or_role: Union[User, Role, str]
+ async def permissions_add(
+ self, ctx, type_: str.lower, name: str, *, user_or_role: Union[User, Role, str]
):
"""
- Add a user, role, or everyone permission to use commands of a permission level.
+ Add a permission to a command or a permission level.
+
+ For sub commands, wrap the complete command name with quotes.
+ To find a list of permission levels, see `{prefix}help perms`.
+
+ Examples:
+ - `{prefix}perms add level REGULAR everyone`
+ - `{prefix}perms add command reply @user`
+ - `{prefix}perms add command "plugin enabled" @role`
+ - `{prefix}perms add command help 984301093849028`
Do not ping `@everyone` for granting permission to everyone, use "everyone" or "all" instead,
- `user_or_role` may be a role ID, name, mention, user ID, name, mention, "all", or "everyone".
"""
- if level.upper() not in PermissionLevel.__members__:
+
+ if type_ not in {"command", "level"}:
+ return await ctx.send_help(ctx.command)
+
+ command = level = None
+ if type_ == "command":
+ command = self.bot.get_command(name.lower())
+ check = command is not None
+ else:
+ check = name.upper() in PermissionLevel.__members__
+ level = PermissionLevel[name.upper()] if check else None
+
+ if not check:
embed = Embed(
title="Error",
color=Color.red(),
- description="The permission level you are attempting to point "
- f"to does not exist: `{level}`.",
+ description=f"The referenced {type_} does not exist: `{name}`.",
)
return await ctx.send(embed=embed)
- if hasattr(user_or_role, "id"):
- value = user_or_role.id
- elif user_or_role in {"everyone", "all"}:
- value = -1
+ value = self._verify_user_or_role(user_or_role)
+ if type_ == "command":
+ await self.bot.update_perms(command.qualified_name, value)
+ name = command.qualified_name
else:
- raise commands.BadArgument(f'User or Role "{user_or_role}" not found')
+ await self.bot.update_perms(level, value)
+ name = level.name
- await self.bot.update_perms(PermissionLevel[level.upper()], value)
embed = Embed(
title="Success",
color=self.bot.main_color,
- description=f"Permission for {level} was successfully updated.",
+ description=f"Permission for `{name}` was successfully updated.",
)
return await ctx.send(embed=embed)
- @permissions.group(
- name="remove", aliases=["del", "delete", "revoke"], invoke_without_command=True
+ @permissions.command(
+ name="remove",
+ aliases=["del", "delete", "revoke"],
+ usage="[command/level] [name] [user_or_role]",
)
@checks.has_permissions(PermissionLevel.OWNER)
- async def permissions_remove(self, ctx):
- """Remove permission to use a command or permission level."""
- await ctx.send_help(ctx.command)
-
- @permissions_remove.command(name="command")
- @checks.has_permissions(PermissionLevel.OWNER)
- async def permissions_remove_command(
- self, ctx, command: str, *, user_or_role: Union[User, Role, str]
+ async def permissions_remove(
+ self, ctx, type_: str.lower, name: str, *, user_or_role: Union[User, Role, str]
):
"""
- Remove a user, role, or everyone permission to use a command.
+ Remove permission to use a command or permission level.
+
+ For sub commands, wrap the complete command name with quotes.
+ To find a list of permission levels, see `{prefix}help perms`.
+
+ Examples:
+ - `{prefix}perms remove level REGULAR everyone`
+ - `{prefix}perms remove command reply @user`
+ - `{prefix}perms remove command "plugin enabled" @role`
+ - `{prefix}perms remove command help 984301093849028`
Do not ping `@everyone` for granting permission to everyone, use "everyone" or "all" instead,
- `user_or_role` may be a role ID, name, mention, user ID, name, mention, "all", or "everyone".
"""
- if command not in self.bot.all_commands:
- embed = Embed(
- title="Error",
- color=Color.red(),
- description="The command you are attempting to point "
- f"to does not exist: `{command}`.",
- )
- return await ctx.send(embed=embed)
+ if type_ not in {"command", "level"}:
+ return await ctx.send_help(ctx.command)
- if hasattr(user_or_role, "id"):
- value = user_or_role.id
- elif user_or_role in {"everyone", "all"}:
- value = -1
+ level = None
+ if type_ == "command":
+ command = self.bot.get_command(name.lower())
+ name = command.qualified_name if command is not None else name
else:
- raise commands.BadArgument(f'User or Role "{user_or_role}" not found')
+ if name.upper() not in PermissionLevel.__members__:
+ embed = Embed(
+ title="Error",
+ color=Color.red(),
+ description=f"The referenced {type_} does not exist: `{name}`.",
+ )
+ return await ctx.send(embed=embed)
+ level = PermissionLevel[name.upper()]
+ name = level.name
+
+ value = self._verify_user_or_role(user_or_role)
+ await self.bot.update_perms(level or name, value, add=False)
- await self.bot.update_perms(
- self.bot.all_commands[command].name, value, add=False
- )
embed = Embed(
title="Success",
color=self.bot.main_color,
- description=f"Permission for {command} was successfully updated.",
+ description=f"Permission for `{name}` was successfully updated.",
)
return await ctx.send(embed=embed)
- @permissions_remove.command(name="level", aliases=["group"])
- @checks.has_permissions(PermissionLevel.OWNER)
- async def permissions_remove_level(
- self, ctx, level: str, *, user_or_role: Union[User, Role, str]
- ):
- """
- Remove a user, role, or everyone permission to use commands of a permission level.
-
- Do not ping `@everyone` for granting permission to everyone, use "everyone" or "all" instead,
- `user_or_role` may be a role ID, name, mention, user ID, name, mention, "all", or "everyone".
- """
- if level.upper() not in PermissionLevel.__members__:
+ def _get_perm(self, ctx, name, type_):
+ if type_ == "command":
+ permissions = self.bot.config["command_permissions"].get(name, [])
+ else:
+ permissions = self.bot.config["level_permissions"].get(name, [])
+ if not permissions:
embed = Embed(
- title="Error",
- color=Color.red(),
- description="The permission level you are attempting to point "
- f"to does not exist: `{level}`.",
+ title=f"Permission entries for {type_} `{name}`:",
+ description="No permission entries found.",
+ color=self.bot.main_color,
)
- return await ctx.send(embed=embed)
-
- if hasattr(user_or_role, "id"):
- value = user_or_role.id
- elif user_or_role in {"everyone", "all"}:
- value = -1
else:
- raise commands.BadArgument(f'User or Role "{user_or_role}" not found')
+ values = []
+ for perm in permissions:
+ if perm == -1:
+ values.insert(0, "**everyone**")
+ continue
+ member = ctx.guild.get_member(perm)
+ if member is not None:
+ values.append(member.mention)
+ continue
+ user = self.bot.get_user(perm)
+ if user is not None:
+ values.append(user.mention)
+ continue
+ role = ctx.guild.get_role(perm)
+ if role is not None:
+ values.append(role.mention)
+ else:
+ values.append(str(perm))
- await self.bot.update_perms(PermissionLevel[level.upper()], value, add=False)
- embed = Embed(
- title="Success",
- color=self.bot.main_color,
- description=f"Permission for {level} was successfully updated.",
- )
- return await ctx.send(embed=embed)
+ embed = Embed(
+ title=f"Permission entries for {type_} `{name}`:",
+ description=", ".join(values),
+ color=self.bot.main_color,
+ )
+ return embed
- @permissions.group(name="get", invoke_without_command=True)
+ @permissions.command(name="get", usage="[@user] or [command/level] [name]")
@checks.has_permissions(PermissionLevel.OWNER)
- async def permissions_get(self, ctx, *, user_or_role: Union[User, Role, str]):
+ async def permissions_get(
+ self, ctx, user_or_role: Union[User, Role, str], *, name: str = None
+ ):
"""
View the currently-set permissions.
- You can specify `user_or_role` as an alternative to get-by-command or get-by-level.
+ To find a list of permission levels, see `{prefix}help perms`.
+
+ Examples:
+ - `{prefix}perms get @user`
+ - `{prefix}perms get 984301093849028`
+ - `{prefix}perms get command reply`
+ - `{prefix}perms get command plugin remove`
+ - `{prefix}perms get level SUPPORTER`
Do not ping `@everyone` for granting permission to everyone, use "everyone" or "all" instead,
- `user_or_role` may be a role ID, name, mention, user ID, name, mention, "all", or "everyone".
"""
- if hasattr(user_or_role, "id"):
- value = user_or_role.id
- elif user_or_role in {"everyone", "all"}:
- value = -1
- else:
- raise commands.BadArgument(f'User or Role "{user_or_role}" not found')
+ if name is None and user_or_role not in {"command", "level"}:
+ value = self._verify_user_or_role(user_or_role)
- cmds = []
- levels = []
- for cmd in self.bot.commands:
- permissions = self.bot.config["command_permissions"].get(cmd.name, [])
- if value in permissions:
- cmds.append(cmd.name)
- for level in PermissionLevel:
- permissions = self.bot.config["level_permissions"].get(level.name, [])
- if value in permissions:
- levels.append(level.name)
- mention = user_or_role.name if hasattr(user_or_role, "name") else user_or_role
- desc_cmd = (
- ", ".join(map(lambda x: f"`{x}`", cmds))
- if cmds
- else "No permission entries found."
- )
- desc_level = (
- ", ".join(map(lambda x: f"`{x}`", levels))
- if levels
- else "No permission entries found."
- )
+ cmds = []
+ levels = []
- embeds = [
- Embed(
- title=f"{mention} has permission with the following commands:",
- description=desc_cmd,
- color=self.bot.main_color,
- ),
- Embed(
- title=f"{mention} has permission with the following permission groups:",
- description=desc_level,
- color=self.bot.main_color,
- ),
- ]
- p_session = PaginatorSession(ctx, *embeds)
- return await p_session.run()
-
- @permissions_get.command(name="command")
- @checks.has_permissions(PermissionLevel.OWNER)
- async def permissions_get_command(self, ctx, *, command: str = None):
- """View currently-set permissions for a command."""
+ done = set()
+ for _, command in self.bot.all_commands.items():
+ if command not in done:
+ done.add(command)
+ permissions = self.bot.config["command_permissions"].get(
+ command.qualified_name, []
+ )
+ if value in permissions:
+ cmds.append(command.qualified_name)
+
+ for level in PermissionLevel:
+ permissions = self.bot.config["level_permissions"].get(level.name, [])
+ if value in permissions:
+ levels.append(level.name)
+
+ mention = getattr(user_or_role, "name", user_or_role)
+ desc_cmd = (
+ ", ".join(map(lambda x: f"`{x}`", cmds))
+ if cmds
+ else "No permission entries found."
+ )
+ desc_level = (
+ ", ".join(map(lambda x: f"`{x}`", levels))
+ if levels
+ else "No permission entries found."
+ )
- def get_command(cmd):
- permissions = self.bot.config["command_permissions"].get(cmd.name, [])
- if not permissions:
- embed = Embed(
- title=f"Permission entries for command `{cmd.name}`:",
- description="No permission entries found.",
+ embeds = [
+ Embed(
+ title=f"{mention} has permission with the following commands:",
+ description=desc_cmd,
color=self.bot.main_color,
- )
- else:
- values = []
- for perm in permissions:
- if perm == -1:
- values.insert(0, "**everyone**")
- continue
- member = ctx.guild.get_member(perm)
- if member is not None:
- values.append(member.mention)
- continue
- user = self.bot.get_user(perm)
- if user is not None:
- values.append(user.mention)
- continue
- role = ctx.guild.get_role(perm)
- if role is not None:
- values.append(role.mention)
- else:
- values.append(str(perm))
-
- embed = Embed(
- title=f"Permission entries for command `{cmd.name}`:",
- description=", ".join(values),
+ ),
+ Embed(
+ title=f"{mention} has permission with the following permission groups:",
+ description=desc_level,
color=self.bot.main_color,
- )
- return embed
-
- embeds = []
- if command is not None:
- if command not in self.bot.all_commands:
- embed = Embed(
- title="Error",
- color=Color.red(),
- description="The command you are attempting to point "
- f"to does not exist: `{command}`.",
- )
- return await ctx.send(embed=embed)
- embeds.append(get_command(self.bot.all_commands[command]))
+ ),
+ ]
else:
- for cmd in self.bot.commands:
- embeds.append(get_command(cmd))
-
- p_session = PaginatorSession(ctx, *embeds)
- return await p_session.run()
-
- @permissions_get.command(name="level", aliases=["group"])
- @checks.has_permissions(PermissionLevel.OWNER)
- async def permissions_get_level(self, ctx, *, level: str = None):
- """View currently-set permissions for commands of a permission level."""
+ if user_or_role not in {"command", "level"}:
+ return await ctx.send_help(ctx.command)
+ embeds = []
+ if name is not None:
+ name = name.strip('"')
+ command = level = None
+ if user_or_role == "command":
+ command = self.bot.get_command(name.lower())
+ check = command is not None
+ else:
+ check = name.upper() in PermissionLevel.__members__
+ level = PermissionLevel[name.upper()] if check else None
+
+ if not check:
+ embed = Embed(
+ title="Error",
+ color=Color.red(),
+ description=f"The referenced {user_or_role} does not exist: `{name}`.",
+ )
+ return await ctx.send(embed=embed)
- def get_level(perm_level):
- permissions = self.bot.config["level_permissions"].get(perm_level.name, [])
- if not permissions:
- embed = Embed(
- title="Permission entries for permission "
- f"level `{perm_level.name}`:",
- description="No permission entries found.",
- color=self.bot.main_color,
- )
+ if user_or_role == "command":
+ embeds.append(
+ self._get_perm(ctx, command.qualified_name, "command")
+ )
+ else:
+ embeds.append(self._get_perm(ctx, level.name, "level"))
else:
- values = []
- for perm in permissions:
- if perm == -1:
- values.insert(0, "**everyone**")
- continue
- member = ctx.guild.get_member(perm)
- if member is not None:
- values.append(member.mention)
- continue
- user = self.bot.get_user(perm)
- if user is not None:
- values.append(user.mention)
- continue
- role = ctx.guild.get_role(perm)
- if role is not None:
- values.append(role.mention)
- else:
- values.append(str(perm))
-
- embed = Embed(
- title=f"Permission entries for permission level `{perm_level.name}`:",
- description=", ".join(values),
- color=self.bot.main_color,
- )
- return embed
-
- embeds = []
- if level is not None:
- if level.upper() not in PermissionLevel.__members__:
- embed = Embed(
- title="Error",
- color=Color.red(),
- description="The permission level you are attempting to point "
- f"to does not exist: `{level}`.",
- )
- return await ctx.send(embed=embed)
- embeds.append(get_level(PermissionLevel[level.upper()]))
- else:
- for perm_level in PermissionLevel:
- embeds.append(get_level(perm_level))
+ if user_or_role == "command":
+ done = set()
+ for _, command in self.bot.all_commands.items():
+ if command not in done:
+ done.add(command)
+ embeds.append(
+ self._get_perm(ctx, command.qualified_name, "command")
+ )
+ else:
+ for perm_level in PermissionLevel:
+ embeds.append(self._get_perm(ctx, perm_level.name, "level"))
p_session = PaginatorSession(ctx, *embeds)
return await p_session.run()
From 52b5b2b4b2502ba102dcb7df43747ac410b1e95f Mon Sep 17 00:00:00 2001
From: KongGal
Date: Mon, 29 Jul 2019 13:57:44 +0200
Subject: [PATCH 0351/1221] Updated Dockerfile to allow for automated building
(#335)
---
Dockerfile | 10 ++++++----
1 file changed, 6 insertions(+), 4 deletions(-)
diff --git a/Dockerfile b/Dockerfile
index d33f146c9e..773031d731 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,5 +1,7 @@
-FROM kennethreitz/pipenv
+FROM library/python:latest
+RUN apt update && apt install -y pipenv
+RUN mkdir -p /bot && cd /bot && git clone https://github.com/kyb3r/modmail .
+WORKDIR /bot
+RUN pipenv install
-COPY . /app
-
-CMD python3 bot.py
+CMD ["pipenv", "run", "python3", "bot.py"]
From af904218d129beb5167b3ed0f3ac0669c2cf5814 Mon Sep 17 00:00:00 2001
From: Taaku18 <45324516+Taaku18@users.noreply.github.com>
Date: Mon, 29 Jul 2019 12:09:27 -0700
Subject: [PATCH 0352/1221] Fix stuff with viewing perm
---
CHANGELOG.md | 4 ++--
cogs/modmail.py | 2 +-
cogs/utility.py | 19 +++++++++----------
3 files changed, 12 insertions(+), 13 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 47934e28ff..8d57ffdc9d 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -225,7 +225,7 @@ Un-deprecated the `OWNERS` config variable to support discord developer team acc
### New Permissions System
- A brand new permission system! Replacing the old guild-based permissions (ie. manage channels, manage messages), the new system enables you to customize your desired permission level specific to a command or a group of commands for a role or user.
-- There are five permission groups/levels:
+- There are five permission levels:
- Owner [5]
- Administrator [4]
- Moderator [3]
@@ -247,7 +247,7 @@ The same applies to individual commands permissions:
To revoke permission, use `remove` instead of `add`.
-To view all roles and users with permission for a permission group or command do:
+To view all roles and users with permission for a permission level or command do:
- `?permissions get command command-name`
- `?permissions get level owner`
diff --git a/cogs/modmail.py b/cogs/modmail.py
index 7949688577..c29cfec308 100644
--- a/cogs/modmail.py
+++ b/cogs/modmail.py
@@ -87,7 +87,7 @@ async def setup(self, ctx):
await self.bot.config.update()
await ctx.send(
"Successfully set up server.\n"
- "Consider setting permission groups to give access "
+ "Consider setting permission levels to give access "
"to roles or users the ability to use Modmail.\n"
f"Type `{self.bot.prefix}permissions` for more info."
)
diff --git a/cogs/utility.py b/cogs/utility.py
index 1252341d89..4f7d17a502 100644
--- a/cogs/utility.py
+++ b/cogs/utility.py
@@ -220,9 +220,9 @@ async def send_error_message(self, error):
choices = set()
- for name, cmd in self.context.bot.all_commands.items():
+ for cmd in self.context.bot.walk_commands():
if not cmd.hidden:
- choices.add(name)
+ choices.add(cmd.name)
closest = get_close_matches(command, choices)
if closest:
@@ -1221,7 +1221,7 @@ def _verify_user_or_role(user_or_role):
@permissions.command(name="add", usage="[command/level] [name] [user_or_role]")
@checks.has_permissions(PermissionLevel.OWNER)
async def permissions_add(
- self, ctx, type_: str.lower, name: str, *, user_or_role: Union[User, Role, str]
+ self, ctx, type_: str.lower, name: str, *, user_or_role: Union[Role, User, str]
):
"""
Add a permission to a command or a permission level.
@@ -1279,7 +1279,7 @@ async def permissions_add(
)
@checks.has_permissions(PermissionLevel.OWNER)
async def permissions_remove(
- self, ctx, type_: str.lower, name: str, *, user_or_role: Union[User, Role, str]
+ self, ctx, type_: str.lower, name: str, *, user_or_role: Union[Role, User, str]
):
"""
Remove permission to use a command or permission level.
@@ -1300,8 +1300,7 @@ async def permissions_remove(
level = None
if type_ == "command":
- command = self.bot.get_command(name.lower())
- name = command.qualified_name if command is not None else name
+ name = getattr(self.bot.get_command(name.lower()), "qualified_name", name)
else:
if name.upper() not in PermissionLevel.__members__:
embed = Embed(
@@ -1364,7 +1363,7 @@ def _get_perm(self, ctx, name, type_):
@permissions.command(name="get", usage="[@user] or [command/level] [name]")
@checks.has_permissions(PermissionLevel.OWNER)
async def permissions_get(
- self, ctx, user_or_role: Union[User, Role, str], *, name: str = None
+ self, ctx, user_or_role: Union[Role, User, str], *, name: str = None
):
"""
View the currently-set permissions.
@@ -1388,7 +1387,7 @@ async def permissions_get(
levels = []
done = set()
- for _, command in self.bot.all_commands.items():
+ for command in self.bot.walk_commands():
if command not in done:
done.add(command)
permissions = self.bot.config["command_permissions"].get(
@@ -1421,7 +1420,7 @@ async def permissions_get(
color=self.bot.main_color,
),
Embed(
- title=f"{mention} has permission with the following permission groups:",
+ title=f"{mention} has permission with the following permission levels:",
description=desc_level,
color=self.bot.main_color,
),
@@ -1457,7 +1456,7 @@ async def permissions_get(
else:
if user_or_role == "command":
done = set()
- for _, command in self.bot.all_commands.items():
+ for command in self.bot.walk_commands():
if command not in done:
done.add(command)
embeds.append(
From c596f1c48b8c6f21e61c64b4446d9625fd54deb7 Mon Sep 17 00:00:00 2001
From: Taaku18 <45324516+Taaku18@users.noreply.github.com>
Date: Mon, 29 Jul 2019 12:50:22 -0700
Subject: [PATCH 0353/1221] 1-5 as level names
---
CHANGELOG.md | 1 +
cogs/utility.py | 38 +++++++++++++++++++++++++++++++-------
2 files changed, 32 insertions(+), 7 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 8d57ffdc9d..ee45406fb2 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -25,6 +25,7 @@ however, insignificant breaking changes does not guarantee a major version bump,
- `?help` works for alias and snippets.
- `?config help ` shows a help embed for the configuration.
- Support setting permissions for sub commands.
+- Support numbers (1-5) as substitutes for Permission Level REGULAR - OWNER in `?perms` sub commands.
### Changes
diff --git a/cogs/utility.py b/cogs/utility.py
index 4f7d17a502..62d02ebeba 100644
--- a/cogs/utility.py
+++ b/cogs/utility.py
@@ -1218,6 +1218,22 @@ def _verify_user_or_role(user_or_role):
else:
raise commands.BadArgument(f'User or Role "{user_or_role}" not found')
+ @staticmethod
+ def _parse_level(name):
+ name = name.upper()
+ try:
+ return PermissionLevel[name]
+ except KeyError:
+ pass
+ transform = {
+ "1": PermissionLevel.REGULAR,
+ "2": PermissionLevel.SUPPORTER,
+ "3": PermissionLevel.MODERATOR,
+ "4": PermissionLevel.ADMINISTRATOR,
+ "5": PermissionLevel.OWNER,
+ }
+ return transform.get(name)
+
@permissions.command(name="add", usage="[command/level] [name] [user_or_role]")
@checks.has_permissions(PermissionLevel.OWNER)
async def permissions_add(
@@ -1246,8 +1262,8 @@ async def permissions_add(
command = self.bot.get_command(name.lower())
check = command is not None
else:
- check = name.upper() in PermissionLevel.__members__
- level = PermissionLevel[name.upper()] if check else None
+ level = self._parse_level(name)
+ check = level is not None
if not check:
embed = Embed(
@@ -1302,14 +1318,14 @@ async def permissions_remove(
if type_ == "command":
name = getattr(self.bot.get_command(name.lower()), "qualified_name", name)
else:
- if name.upper() not in PermissionLevel.__members__:
+ level = self._parse_level(name)
+ if level is None:
embed = Embed(
title="Error",
color=Color.red(),
description=f"The referenced {type_} does not exist: `{name}`.",
)
return await ctx.send(embed=embed)
- level = PermissionLevel[name.upper()]
name = level.name
value = self._verify_user_or_role(user_or_role)
@@ -1370,9 +1386,15 @@ async def permissions_get(
To find a list of permission levels, see `{prefix}help perms`.
+ To view all command and level permissions:
+
Examples:
- `{prefix}perms get @user`
- `{prefix}perms get 984301093849028`
+
+ To view all users and roles of a command or level permission:
+
+ Examples:
- `{prefix}perms get command reply`
- `{prefix}perms get command plugin remove`
- `{prefix}perms get level SUPPORTER`
@@ -1401,7 +1423,9 @@ async def permissions_get(
if value in permissions:
levels.append(level.name)
- mention = getattr(user_or_role, "name", user_or_role)
+ mention = getattr(
+ user_or_role, "name", getattr(user_or_role, "id", user_or_role)
+ )
desc_cmd = (
", ".join(map(lambda x: f"`{x}`", cmds))
if cmds
@@ -1436,8 +1460,8 @@ async def permissions_get(
command = self.bot.get_command(name.lower())
check = command is not None
else:
- check = name.upper() in PermissionLevel.__members__
- level = PermissionLevel[name.upper()] if check else None
+ level = self._parse_level(name)
+ check = level is not None
if not check:
embed = Embed(
From 3a07329b79941832bb1370b21dc0233f478ffe7c Mon Sep 17 00:00:00 2001
From: Taaku18 <45324516+Taaku18@users.noreply.github.com>
Date: Tue, 30 Jul 2019 17:25:19 -0700
Subject: [PATCH 0354/1221] Add doc for snippet raw
---
cogs/modmail.py | 5 ++++-
1 file changed, 4 insertions(+), 1 deletion(-)
diff --git a/cogs/modmail.py b/cogs/modmail.py
index c29cfec308..32d716ec89 100644
--- a/cogs/modmail.py
+++ b/cogs/modmail.py
@@ -117,7 +117,7 @@ async def snippet(self, ctx, *, name: str.lower = None):
with `{prefix}snippet-name`, the message "A pre-defined text."
will be sent to the recipient.
- Currently, there is not a default anonymous snippet command; however, a workaround
+ Currently, there is not a built-in anonymous snippet command; however, a workaround
is available using `{prefix}alias`. Here is how:
- `{prefix}alias add snippet-name anonreply A pre-defined anonymous text.`
@@ -165,6 +165,9 @@ async def snippet(self, ctx, *, name: str.lower = None):
@snippet.command(name="raw")
@checks.has_permissions(PermissionLevel.SUPPORTER)
async def snippet_raw(self, ctx, *, name: str.lower):
+ """
+ View the raw content of a snippet.
+ """
val = self.bot.snippets.get(name)
if val is None:
embed = create_not_found_embed(name, self.bot.snippets.keys(), "Snippet")
From 92fad07af76c6d89c2b8a81ec0b6d078df04cb38 Mon Sep 17 00:00:00 2001
From: Taaku18 <45324516+Taaku18@users.noreply.github.com>
Date: Tue, 30 Jul 2019 17:39:27 -0700
Subject: [PATCH 0355/1221] Fix log url prefix
Signed-off-by: Taaku18 <45324516+Taaku18@users.noreply.github.com>
---
cogs/modmail.py | 12 ++++--------
core/clients.py | 10 ++++++++--
core/thread.py | 10 ++++------
3 files changed, 16 insertions(+), 16 deletions(-)
diff --git a/cogs/modmail.py b/cogs/modmail.py
index 32d716ec89..dc4368160c 100644
--- a/cogs/modmail.py
+++ b/cogs/modmail.py
@@ -570,16 +570,12 @@ def format_log_embeds(self, logs, avatar_url):
title = f"Total Results Found ({len(logs)})"
for entry in logs:
-
- key = entry["key"]
-
created_at = parser.parse(entry["created_at"])
- prefix = self.bot.config["log_url_prefix"]
- if prefix == "NONE":
- prefix = ""
-
- log_url = self.bot.config["log_url"].strip("/") + f"{prefix}/{key}"
+ prefix = self.bot.config['log_url_prefix'].strip('/')
+ if prefix == 'NONE':
+ prefix = ''
+ log_url = f"{self.bot.config['log_url'].strip('/')}{'/' + prefix if prefix else ''}/{entry['key']}"
username = entry["recipient"]["name"] + "#"
username += entry["recipient"]["discriminator"]
diff --git a/core/clients.py b/core/clients.py
index a4ed847622..31a73be390 100644
--- a/core/clients.py
+++ b/core/clients.py
@@ -99,7 +99,10 @@ async def get_log(self, channel_id: Union[str, int]) -> dict:
async def get_log_link(self, channel_id: Union[str, int]) -> str:
doc = await self.get_log(channel_id)
logger.debug("Retrieving log link for channel %s.", channel_id)
- return f"{self.bot.config['log_url'].strip('/')}{self.bot.config['log_url_prefix']}/{doc['key']}"
+ prefix = self.bot.config['log_url_prefix'].strip('/')
+ if prefix == 'NONE':
+ prefix = ''
+ return f"{self.bot.config['log_url'].strip('/')}{'/' + prefix if prefix else ''}/{doc['key']}"
async def create_log_entry(
self, recipient: Member, channel: TextChannel, creator: Member
@@ -135,7 +138,10 @@ async def create_log_entry(
}
)
logger.debug("Created a log entry, key %s.", key)
- return f"{self.bot.config['log_url'].strip('/')}{self.bot.config['log_url_prefix']}/{key}"
+ prefix = self.bot.config['log_url_prefix'].strip('/')
+ if prefix == 'NONE':
+ prefix = ''
+ return f"{self.bot.config['log_url'].strip('/')}{'/' + prefix if prefix else ''}/{key}"
async def get_config(self) -> dict:
conf = await self.db.config.find_one({"bot_id": self.bot.user.id})
diff --git a/core/thread.py b/core/thread.py
index 12bb71ab3c..7e8a2574db 100644
--- a/core/thread.py
+++ b/core/thread.py
@@ -338,12 +338,10 @@ async def _close(
)
if isinstance(log_data, dict):
- prefix = self.bot.config["log_url_prefix"]
- if prefix == "NONE":
- prefix = ""
- log_url = (
- f"{self.bot.config['log_url'].strip('/')}{prefix}/{log_data['key']}"
- )
+ prefix = self.bot.config['log_url_prefix'].strip('/')
+ if prefix == 'NONE':
+ prefix = ''
+ log_url = f"{self.bot.config['log_url'].strip('/')}{'/' + prefix if prefix else ''}/{log_data['key']}"
if log_data["messages"]:
content = str(log_data["messages"][0]["content"])
From 5a26c28900616ca2958376c4f0e7bbf723957472 Mon Sep 17 00:00:00 2001
From: Taaku18 <45324516+Taaku18@users.noreply.github.com>
Date: Tue, 30 Jul 2019 22:07:52 -0700
Subject: [PATCH 0356/1221] Config help on protected keys
---
CHANGELOG.md | 1 +
cogs/modmail.py | 6 ++--
cogs/utility.py | 18 ++++++-----
core/config_help.json | 72 +++++++++++++++++++++++++++++++++++++++++++
4 files changed, 87 insertions(+), 10 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index ee45406fb2..49d41d382f 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -12,6 +12,7 @@ however, insignificant breaking changes does not guarantee a major version bump,
- `disable_recipient_thread_close` is removed, a new configuration variable `recipient_thread_close` replaces it which defaults to False.
- Truthy and falsy values for binary configuration variables are now interpreted respectfully.
+- `LOG_URL_PREFIX` cannot be set to "NONE" to specify no additional path in the future, "/" is the new method.
### Added
diff --git a/cogs/modmail.py b/cogs/modmail.py
index dc4368160c..693adafb18 100644
--- a/cogs/modmail.py
+++ b/cogs/modmail.py
@@ -572,9 +572,9 @@ def format_log_embeds(self, logs, avatar_url):
for entry in logs:
created_at = parser.parse(entry["created_at"])
- prefix = self.bot.config['log_url_prefix'].strip('/')
- if prefix == 'NONE':
- prefix = ''
+ prefix = self.bot.config["log_url_prefix"].strip("/")
+ if prefix == "NONE":
+ prefix = ""
log_url = f"{self.bot.config['log_url'].strip('/')}{'/' + prefix if prefix else ''}/{entry['key']}"
username = entry["recipient"]["name"] + "#"
diff --git a/cogs/utility.py b/cogs/utility.py
index 62d02ebeba..672d8f3448 100644
--- a/cogs/utility.py
+++ b/cogs/utility.py
@@ -844,8 +844,9 @@ async def config_get(self, ctx, key: str.lower = None):
color=Color.red(),
description=f"`{key}` is an invalid key.",
)
- valid_keys = [f"`{k}`" for k in keys]
- embed.add_field(name="Valid keys", value=", ".join(valid_keys))
+ embed.set_footer(
+ text=f'Type "{self.bot.prefix}config options" for a list of config variables.'
+ )
else:
embed = Embed(
@@ -870,7 +871,9 @@ async def config_help(self, ctx, key: str.lower):
"""
Show information on a specified configuration.
"""
- if key not in self.bot.config.public_keys:
+ if not (
+ key in self.bot.config.public_keys or key in self.bot.config.protected_keys
+ ):
embed = Embed(
title="Error",
color=Color.red(),
@@ -904,10 +907,11 @@ def fmt(val):
embed.add_field(
name="Information:", value=fmt(info["description"]), inline=False
)
- example_text = ""
- for example in info["examples"]:
- example_text += f"- {fmt(example)}\n"
- embed.add_field(name="Example(s):", value=example_text, inline=False)
+ if info["examples"]:
+ example_text = ""
+ for example in info["examples"]:
+ example_text += f"- {fmt(example)}\n"
+ embed.add_field(name="Example(s):", value=example_text, inline=False)
note_text = ""
for note in info["notes"]:
diff --git a/core/config_help.json b/core/config_help.json
index 37106f286a..f4e8eae36f 100644
--- a/core/config_help.json
+++ b/core/config_help.json
@@ -373,5 +373,77 @@
"See also: `anon_avatar_url`, `anon_username`, `mod_tag`."
],
"image": "https://i.imgur.com/SKOC42Z.png"
+ },
+ "modmail_guild_id": {
+ "default": "Fallback on `GUILD_ID`",
+ "description": "The ID of the discord server where the threads channels should be created (receiving server).",
+ "examples": [
+ ],
+ "notes": [
+ "This configuration can only to be set through `.env` file or environment (config) variables."
+ ]
+ },
+ "guild_id": {
+ "default": "None, required",
+ "description": "The ID of the discord server where recipient users reside (users server).",
+ "examples": [
+ ],
+ "notes": [
+ "This configuration can only to be set through `.env` file or environment (config) variables."
+ ]
+ },
+ "log_url": {
+ "default": "https://example.com/",
+ "description": "The base log viewer URL link, leave this as-is to not configure a log viewer.",
+ "examples": [
+ ],
+ "notes": [
+ "This configuration can only to be set through `.env` file or environment (config) variables."
+ ]
+ },
+ "log_url_prefix": {
+ "default": "`/logs`",
+ "description": "The path to your log viewer extending from your `LOG_URL`, set this to `/` to specify no extra path to the log viewer.",
+ "examples": [
+ ],
+ "notes": [
+ "This configuration can only to be set through `.env` file or environment (config) variables."
+ ]
+ },
+ "mongo_uri": {
+ "default": "None, required",
+ "description": "A MongoDB SRV connection string.",
+ "examples": [
+ ],
+ "notes": [
+ "This configuration can only to be set through `.env` file or environment (config) variables."
+ ]
+ },
+ "owners": {
+ "default": "None, required",
+ "description": "A list of definite bot owners, use `{prefix}perms add level OWNER @user` to set flexible bot owners.",
+ "examples": [
+ ],
+ "notes": [
+ "This configuration can only to be set through `.env` file, ~~`config.json` file~~ (removed), or environment (config) variables."
+ ]
+ },
+ "token": {
+ "default": "None, required",
+ "description": "Your bot token as found in the Discord Developer Portal.",
+ "examples": [
+ ],
+ "notes": [
+ "This configuration can only to be set through `.env` file, ~~`config.json` file~~ (removed), or environment (config) variables."
+ ]
+ },
+ "log_level": {
+ "default": "INFO",
+ "description": "The logging level for logging to stdout.",
+ "examples": [
+ ],
+ "notes": [
+ "This configuration can only to be set through `.env` file, ~~`config.json` file~~ (removed), or environment (config) variables."
+ ]
}
}
\ No newline at end of file
From 30a6c7f6bc6547f25450ec09209df60e2251f53c Mon Sep 17 00:00:00 2001
From: Taaku18 <45324516+Taaku18@users.noreply.github.com>
Date: Wed, 31 Jul 2019 01:51:03 -0700
Subject: [PATCH 0357/1221] Added raw alias
---
.pylintrc | 2 +-
CHANGELOG.md | 1 +
app.json | 4 ----
cogs/modmail.py | 11 +++--------
cogs/plugins.py | 2 +-
cogs/utility.py | 44 +++++++++++++++++++++++++-------------------
core/checks.py | 4 ++--
core/config.py | 2 --
core/thread.py | 19 +++++++++----------
core/utils.py | 12 ++++++++++--
10 files changed, 52 insertions(+), 49 deletions(-)
diff --git a/.pylintrc b/.pylintrc
index 1bed8185c7..a45837fa82 100644
--- a/.pylintrc
+++ b/.pylintrc
@@ -19,7 +19,7 @@ ignore-patterns=
# Use multiple processes to speed up Pylint. Specifying 0 will auto-detect the
# number of processors available to use.
-jobs=1
+jobs=0
# Control the amount of potential inferred values when inferring a single
# object. This can help the performance when dealing with large functions or
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 49d41d382f..f904043075 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -39,6 +39,7 @@ however, insignificant breaking changes does not guarantee a major version bump,
- `?plugin registry page-number` plugin registry can specify a page number for quick access.
- A reworked interface for `?snippet` and `?alias`.
- Add an `?snippet raw ` command for viewing the raw content of a snippet (escaped markdown).
+ - Add an `?alias raw ` command for viewing the raw content of a alias (escaped markdown).
- The placeholder channel for the streaming status changed to https://www.twitch.tv/discordmodmail/.
- Removed unclear `rm` alias for some `remove` commands.
- Paginate `?config options`.
diff --git a/app.json b/app.json
index 8d16cd75c2..e4f5032e81 100644
--- a/app.json
+++ b/app.json
@@ -15,10 +15,6 @@
"description": "Comma separated user IDs of people that are allowed to use owner only commands. (eval and update).",
"required": true
},
- "GITHUB_ACCESS_TOKEN": {
- "description": "Your personal access token for GitHub, adding this gives you the ability to use the 'update' command, which will sync your fork with the main repo.",
- "required": false
- },
"MONGO_URI": {
"description": "Mongo DB connection URI for self-hosting your data.",
"required": true
diff --git a/cogs/modmail.py b/cogs/modmail.py
index 693adafb18..081385d202 100644
--- a/cogs/modmail.py
+++ b/cogs/modmail.py
@@ -1,7 +1,7 @@
import asyncio
import logging
from datetime import datetime
-from itertools import zip_longest, takewhile
+from itertools import zip_longest
from typing import Optional, Union
from types import SimpleNamespace as param
@@ -17,7 +17,7 @@
from core.models import PermissionLevel
from core.paginator import PaginatorSession
from core.time import UserFriendlyTime, human_timedelta
-from core.utils import format_preview, User, create_not_found_embed
+from core.utils import format_preview, User, create_not_found_embed, format_description
logger = logging.getLogger("Modmail")
@@ -149,12 +149,7 @@ async def snippet(self, ctx, *, name: str.lower = None):
for i, names in enumerate(
zip_longest(*(iter(sorted(self.bot.snippets)),) * 15)
):
- description = "\n".join(
- ": ".join((str(a + i * 15), b))
- for a, b in enumerate(
- takewhile(lambda x: x is not None, names), start=1
- )
- )
+ description = format_description(i, names)
embed = discord.Embed(color=self.bot.main_color, description=description)
embed.set_author(name="Snippets", icon_url=ctx.guild.icon_url)
embeds.append(embed)
diff --git a/cogs/plugins.py b/cogs/plugins.py
index 6ed1dccf1b..ebb9a72241 100644
--- a/cogs/plugins.py
+++ b/cogs/plugins.py
@@ -288,7 +288,7 @@ async def plugin_remove(self, ctx, *, plugin_name: str):
for i in self.bot.config["plugins"]
):
# if there are no more of such repos, delete the folder
- def onerror(func, path, exc_info): # pylint: disable=W0613
+ def onerror(func, path, _):
if not os.access(path, os.W_OK):
# Is the error an access error?
os.chmod(path, stat.S_IWUSR)
diff --git a/cogs/utility.py b/cogs/utility.py
index 672d8f3448..b1fe7aa7b3 100644
--- a/cogs/utility.py
+++ b/cogs/utility.py
@@ -33,6 +33,7 @@
get_perm_level,
create_not_found_embed,
parse_alias,
+ format_description,
)
logger = logging.getLogger("Modmail")
@@ -248,11 +249,10 @@ def __init__(self, bot):
verify_checks=False, command_attrs={"help": "Shows this help message."}
)
# Looks a bit ugly
- # noinspection PyProtectedMember
- self.bot.help_command._command_impl = checks.has_permissions( # pylint: disable=W0212
+ self.bot.help_command._command_impl = checks.has_permissions( # pylint: disable=protected-access
PermissionLevel.REGULAR
)(
- self.bot.help_command._command_impl # pylint: disable=W0212
+ self.bot.help_command._command_impl # pylint: disable=protected-access
)
self.bot.help_command.cog = self
@@ -998,12 +998,7 @@ async def alias(self, ctx, *, name: str.lower = None):
embeds = []
for i, names in enumerate(zip_longest(*(iter(sorted(self.bot.aliases)),) * 15)):
- description = "\n".join(
- ": ".join((str(a + i * 15), b))
- for a, b in enumerate(
- takewhile(lambda x: x is not None, names), start=1
- )
- )
+ description = format_description(i, names)
embed = Embed(color=self.bot.main_color, description=description)
embed.set_author(name="Command Aliases", icon_url=ctx.guild.icon_url)
embeds.append(embed)
@@ -1011,6 +1006,18 @@ async def alias(self, ctx, *, name: str.lower = None):
session = PaginatorSession(ctx, *embeds)
await session.run()
+ @alias.command(name="raw")
+ @checks.has_permissions(PermissionLevel.MODERATOR)
+ async def alias_raw(self, ctx, *, name: str.lower):
+ """
+ View the raw content of an alias.
+ """
+ val = self.bot.aliases.get(name)
+ if val is None:
+ embed = create_not_found_embed(name, self.bot.aliases.keys(), "Alias")
+ return await ctx.send(embed=embed)
+ return await ctx.send(escape_markdown(escape_mentions(val)).replace("<", "\\<"))
+
@alias.command(name="add")
@checks.has_permissions(PermissionLevel.MODERATOR)
async def alias_add(self, ctx, name: str.lower, *, value):
@@ -1027,36 +1034,36 @@ async def alias_add(self, ctx, name: str.lower, *, value):
- This will fail: `{prefix}alias add reply You'll need to type && to work`
- Correct method: `{prefix}alias add reply "You'll need to type && to work"`
"""
+ embed = None
if self.bot.get_command(name):
embed = Embed(
title="Error",
color=Color.red(),
description=f"A command with the same name already exists: `{name}`.",
)
- return await ctx.send(embed=embed)
- if name in self.bot.aliases:
+ elif name in self.bot.aliases:
embed = Embed(
title="Error",
color=Color.red(),
description=f"Another alias with the same name already exists: `{name}`.",
)
- return await ctx.send(embed=embed)
- if name in self.bot.snippets:
+ elif name in self.bot.snippets:
embed = Embed(
title="Error",
color=Color.red(),
description=f"A snippet with the same name already exists: `{name}`.",
)
- return await ctx.send(embed=embed)
- if len(name) > 120:
+ elif len(name) > 120:
embed = Embed(
title="Error",
color=Color.red(),
description=f"Alias names cannot be longer than 120 characters.",
)
+
+ if embed is not None:
return await ctx.send(embed=embed)
values = parse_alias(value)
@@ -1106,7 +1113,7 @@ async def alias_add(self, ctx, name: str.lower, *, value):
return await ctx.send(embed=embed)
embed.description += f"\n{i}: {val}"
- self.bot.aliases[name] = "&&".join(values)
+ self.bot.aliases[name] = " && ".join(values)
await self.bot.config.update()
return await ctx.send(embed=embed)
@@ -1217,10 +1224,9 @@ async def permissions(self, ctx):
def _verify_user_or_role(user_or_role):
if hasattr(user_or_role, "id"):
return user_or_role.id
- elif user_or_role in {"everyone", "all"}:
+ if user_or_role in {"everyone", "all"}:
return -1
- else:
- raise commands.BadArgument(f'User or Role "{user_or_role}" not found')
+ raise commands.BadArgument(f'User or Role "{user_or_role}" not found')
@staticmethod
def _parse_level(name):
diff --git a/core/checks.py b/core/checks.py
index 3c7f8b701f..503877b3a7 100644
--- a/core/checks.py
+++ b/core/checks.py
@@ -33,7 +33,7 @@ async def predicate(ctx):
if not has_perm and ctx.command.qualified_name != "help":
logger.error(
- "You does not have permission to use this command: `%s` (%s).",
+ "You do not have permission to use this command: `%s` (%s).",
str(ctx.command.qualified_name),
str(permission_level.name),
)
@@ -43,7 +43,7 @@ async def predicate(ctx):
return commands.check(predicate)
-async def check_permissions( # pylint: disable=R0911
+async def check_permissions( # pylint: disable=too-many-return-statements
ctx, command_name, permission_level
) -> bool:
"""Logic for checking permissions for a command for a user"""
diff --git a/core/config.py b/core/config.py
index ae0a1f0a3e..e7776cb39d 100644
--- a/core/config.py
+++ b/core/config.py
@@ -3,7 +3,6 @@
import logging
import os
import typing
-from collections import namedtuple
from copy import deepcopy
from dotenv import load_dotenv
@@ -155,7 +154,6 @@ def populate_cache(self) -> dict:
os.path.dirname(os.path.abspath(__file__)), "config_help.json"
)
with open(config_help_json, "r") as f:
- Entry = namedtuple("Entry", ["index", "embed"])
self.config_help = dict(sorted(json.load(f).items()))
return self._cache
diff --git a/core/thread.py b/core/thread.py
index 7e8a2574db..95eeadc3dd 100644
--- a/core/thread.py
+++ b/core/thread.py
@@ -338,9 +338,9 @@ async def _close(
)
if isinstance(log_data, dict):
- prefix = self.bot.config['log_url_prefix'].strip('/')
- if prefix == 'NONE':
- prefix = ''
+ prefix = self.bot.config["log_url_prefix"].strip("/")
+ if prefix == "NONE":
+ prefix = ""
log_url = f"{self.bot.config['log_url'].strip('/')}{'/' + prefix if prefix else ''}/{log_data['key']}"
if log_data["messages"]:
@@ -410,7 +410,9 @@ async def _close(
await asyncio.gather(*tasks)
async def cancel_closure(
- self, auto_close: bool = False, all: bool = False # pylint: disable=W0622
+ self,
+ auto_close: bool = False,
+ all: bool = False, # pylint: disable=redefined-builtin
) -> None:
if self.close_task is not None and (not auto_close or all):
self.close_task.cancel()
@@ -746,8 +748,7 @@ async def send(
file_upload_count += 1
if from_mod:
- # noinspection PyUnresolvedReferences,PyDunderSlots
- embed.color = self.bot.mod_color # pylint: disable=E0237
+ embed.colour = self.bot.mod_color
# Anonymous reply sent in thread channel
if anonymous and isinstance(destination, discord.TextChannel):
embed.set_footer(text="Anonymous Reply")
@@ -760,12 +761,10 @@ async def send(
else:
embed.set_footer(text=self.bot.config["anon_tag"])
elif note:
- # noinspection PyUnresolvedReferences,PyDunderSlots
- embed.color = discord.Color.blurple() # pylint: disable=E0237
+ embed.colour = discord.Color.blurple()
else:
embed.set_footer(text=f"Recipient")
- # noinspection PyUnresolvedReferences,PyDunderSlots
- embed.color = self.bot.recipient_color # pylint: disable=E0237
+ embed.colour = self.bot.recipient_color
try:
await destination.trigger_typing()
diff --git a/core/utils.py b/core/utils.py
index 024c6e0f03..89f9b55f81 100644
--- a/core/utils.py
+++ b/core/utils.py
@@ -2,7 +2,8 @@
import shlex
import typing
from difflib import get_close_matches
-from distutils.util import strtobool as _stb # pylint: disable=E0401
+from distutils.util import strtobool as _stb
+from itertools import takewhile
from urllib import parse
import discord
@@ -37,7 +38,7 @@ async def convert(self, ctx, argument):
return discord.Object(int(match.group(1)))
-def truncate(text: str, max: int = 50) -> str: # pylint: disable=W0622
+def truncate(text: str, max: int = 50) -> str: # pylint: disable=redefined-builtin
"""
Reduces the string to `max` length, by trimming the message into "...".
@@ -253,3 +254,10 @@ def parse_alias(alias):
if not all(cmd):
return []
return cmd
+
+
+def format_description(i, names):
+ return "\n".join(
+ ": ".join((str(a + i * 15), b))
+ for a, b in enumerate(takewhile(lambda x: x is not None, names), start=1)
+ )
From c89ce1495f2e7ce5bdae2167347722f11c7afb7d Mon Sep 17 00:00:00 2001
From: Taaku18 <45324516+Taaku18@users.noreply.github.com>
Date: Wed, 31 Jul 2019 23:48:00 -0600
Subject: [PATCH 0358/1221] Emoji wasn't showing cuz im dumb
---
core/paginator.py | 18 +++++++++++-------
1 file changed, 11 insertions(+), 7 deletions(-)
diff --git a/core/paginator.py b/core/paginator.py
index 2fb72da8ff..a19db604b7 100644
--- a/core/paginator.py
+++ b/core/paginator.py
@@ -194,6 +194,12 @@ async def close(self, delete: bool = True) -> typing.Optional[Message]:
"""
self.running = False
+ sent_emoji, _ = await self.ctx.bot.retrieve_emoji()
+ try:
+ await self.ctx.message.add_reaction(sent_emoji)
+ except (HTTPException, InvalidArgument):
+ pass
+
if delete:
return await self.base.delete()
@@ -202,12 +208,6 @@ async def close(self, delete: bool = True) -> typing.Optional[Message]:
except HTTPException:
pass
- sent_emoji, _ = await self.ctx.bot.retrieve_emoji()
- try:
- await self.ctx.message.add_reaction(sent_emoji)
- except (HTTPException, InvalidArgument):
- pass
-
async def first_page(self) -> None:
"""
Go to the first page.
@@ -391,7 +391,11 @@ async def close(self, delete: bool = True) -> typing.Optional[Message]:
"""
self.running = False
- self.ctx.bot.loop.create_task(self.ctx.message.add_reaction("✅"))
+ sent_emoji, _ = await self.ctx.bot.retrieve_emoji()
+ try:
+ await self.ctx.message.add_reaction(sent_emoji)
+ except (HTTPException, InvalidArgument):
+ pass
if delete:
return await self.base.delete()
From 2cb9fdcb296bb20a7c8a273b19fd94db40b59c89 Mon Sep 17 00:00:00 2001
From: Kyber
Date: Fri, 2 Aug 2019 18:53:49 +1000
Subject: [PATCH 0359/1221] Remove sponsor
---
README.md | 6 ------
1 file changed, 6 deletions(-)
diff --git a/README.md b/README.md
index 5776675b28..d887601646 100644
--- a/README.md
+++ b/README.md
@@ -99,12 +99,6 @@ Sepcial thanks to our sponsors for supporting the project.
-
-
-
-
-
-
Become a [sponsor](https://patreon.com/kyber).
From bef1437930140dd1944e46bd1fee65abde05829c Mon Sep 17 00:00:00 2001
From: Taaku18 <45324516+Taaku18@users.noreply.github.com>
Date: Fri, 2 Aug 2019 21:41:34 -0600
Subject: [PATCH 0360/1221] Paginator change
---
CHANGELOG.md | 3 +-
cogs/modmail.py | 13 ++-
cogs/plugins.py | 6 +-
cogs/utility.py | 24 ++--
core/paginator.py | 274 ++++++++++------------------------------------
5 files changed, 81 insertions(+), 239 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index f904043075..da6784d12c 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -66,7 +66,8 @@ however, insignificant breaking changes does not guarantee a major version bump,
- Use discord tasks for metadata loop.
- More debug based logging.
- Reduce redundancies in `?perms` sub commands.
-
+- paginator been split into `EmbedPaginatorSession` and `MessagePaginatorSession`, both subclassing `PaginatorSession`.
+
# v3.0.3
### Added
diff --git a/cogs/modmail.py b/cogs/modmail.py
index 081385d202..c3750d7900 100644
--- a/cogs/modmail.py
+++ b/cogs/modmail.py
@@ -15,7 +15,7 @@
from core import checks
from core.decorators import trigger_typing
from core.models import PermissionLevel
-from core.paginator import PaginatorSession
+from core.paginator import EmbedPaginatorSession
from core.time import UserFriendlyTime, human_timedelta
from core.utils import format_preview, User, create_not_found_embed, format_description
@@ -154,7 +154,7 @@ async def snippet(self, ctx, *, name: str.lower = None):
embed.set_author(name="Snippets", icon_url=ctx.guild.icon_url)
embeds.append(embed)
- session = PaginatorSession(ctx, *embeds)
+ session = EmbedPaginatorSession(ctx, *embeds)
await session.run()
@snippet.command(name="raw")
@@ -631,7 +631,7 @@ async def logs(self, ctx, *, user: User = None):
embeds = self.format_log_embeds(logs, avatar_url=icon_url)
- session = PaginatorSession(ctx, *embeds)
+ session = EmbedPaginatorSession(ctx, *embeds)
await session.run()
@logs.command(name="closed-by", aliases=["closeby"])
@@ -664,7 +664,7 @@ async def logs_closed_by(self, ctx, *, user: User = None):
)
return await ctx.send(embed=embed)
- session = PaginatorSession(ctx, *embeds)
+ session = EmbedPaginatorSession(ctx, *embeds)
await session.run()
@logs.command(name="search", aliases=["find"])
@@ -697,7 +697,7 @@ async def logs_search(self, ctx, limit: Optional[int] = None, *, query):
)
return await ctx.send(embed=embed)
- session = PaginatorSession(ctx, *embeds)
+ session = EmbedPaginatorSession(ctx, *embeds)
await session.run()
@commands.command()
@@ -893,7 +893,8 @@ async def blocked(self, ctx):
else:
embeds[-1].description = "Currently there are no blocked users."
- await PaginatorSession(ctx, *embeds).run()
+ session = EmbedPaginatorSession(ctx, *embeds)
+ await session.run()
@blocked.command(name="whitelist")
@checks.has_permissions(PermissionLevel.MODERATOR)
diff --git a/cogs/plugins.py b/cogs/plugins.py
index ebb9a72241..f871209bb6 100644
--- a/cogs/plugins.py
+++ b/cogs/plugins.py
@@ -17,7 +17,7 @@
from core import checks
from core.models import PermissionLevel
-from core.paginator import PaginatorSession
+from core.paginator import EmbedPaginatorSession
logger = logging.getLogger("Modmail")
@@ -465,7 +465,7 @@ async def plugin_registry(self, ctx, *, plugin_name: typing.Union[int, str] = No
embeds.append(embed)
- paginator = PaginatorSession(ctx, *embeds)
+ paginator = EmbedPaginatorSession(ctx, *embeds)
paginator.current = index
await paginator.run()
@@ -499,7 +499,7 @@ async def plugin_registry_compact(self, ctx):
embed.set_author(name="Plugin Registry", icon_url=self.bot.user.avatar_url)
embeds.append(embed)
- paginator = PaginatorSession(ctx, *embeds)
+ paginator = EmbedPaginatorSession(ctx, *embeds)
await paginator.run()
diff --git a/cogs/utility.py b/cogs/utility.py
index b1fe7aa7b3..f661cecb40 100644
--- a/cogs/utility.py
+++ b/cogs/utility.py
@@ -26,7 +26,7 @@
from core.changelog import Changelog
from core.decorators import trigger_typing
from core.models import InvalidConfigError, PermissionLevel
-from core.paginator import PaginatorSession, MessagePaginatorSession
+from core.paginator import EmbedPaginatorSession, MessagePaginatorSession
from core.utils import (
cleanup_code,
User,
@@ -112,17 +112,17 @@ async def send_bot_help(self, mapping):
if no_cog_commands:
embeds.extend(await self.format_cog_help(no_cog_commands, no_cog=True))
- p_session = PaginatorSession(
+ session = EmbedPaginatorSession(
self.context, *embeds, destination=self.get_destination()
)
- return await p_session.run()
+ return await session.run()
async def send_cog_help(self, cog):
embeds = await self.format_cog_help(cog)
- p_session = PaginatorSession(
+ session = EmbedPaginatorSession(
self.context, *embeds, destination=self.get_destination()
)
- return await p_session.run()
+ return await session.run()
async def send_command_help(self, command):
if not await self.filter_commands([command]):
@@ -285,7 +285,7 @@ async def changelog(self, ctx, version: str.lower = ""):
)
try:
- paginator = PaginatorSession(ctx, *changelog.embeds)
+ paginator = EmbedPaginatorSession(ctx, *changelog.embeds)
paginator.current = index
await paginator.run()
except asyncio.CancelledError:
@@ -358,7 +358,7 @@ async def sponsors(self, ctx):
random.shuffle(embeds)
- session = PaginatorSession(ctx, *embeds)
+ session = EmbedPaginatorSession(ctx, *embeds)
await session.run()
@commands.group(invoke_without_command=True)
@@ -762,7 +762,7 @@ async def config_options(self, ctx):
)
embeds.append(embed)
- session = PaginatorSession(ctx, *embeds)
+ session = EmbedPaginatorSession(ctx, *embeds)
await session.run()
@config.command(name="set", aliases=["add"])
@@ -926,7 +926,7 @@ def fmt(val):
embed.set_thumbnail(url=fmt(info["thumbnail"]))
embeds += [embed]
- paginator = PaginatorSession(ctx, *embeds)
+ paginator = EmbedPaginatorSession(ctx, *embeds)
paginator.current = index
await paginator.run()
@@ -1003,7 +1003,7 @@ async def alias(self, ctx, *, name: str.lower = None):
embed.set_author(name="Command Aliases", icon_url=ctx.guild.icon_url)
embeds.append(embed)
- session = PaginatorSession(ctx, *embeds)
+ session = EmbedPaginatorSession(ctx, *embeds)
await session.run()
@alias.command(name="raw")
@@ -1500,8 +1500,8 @@ async def permissions_get(
for perm_level in PermissionLevel:
embeds.append(self._get_perm(ctx, perm_level.name, "level"))
- p_session = PaginatorSession(ctx, *embeds)
- return await p_session.run()
+ session = EmbedPaginatorSession(ctx, *embeds)
+ return await session.run()
@commands.group(invoke_without_command=True)
@checks.has_permissions(PermissionLevel.OWNER)
diff --git a/core/paginator.py b/core/paginator.py
index a19db604b7..0a8aa8b814 100644
--- a/core/paginator.py
+++ b/core/paginator.py
@@ -8,7 +8,7 @@
class PaginatorSession:
"""
- Class that interactively paginates a list of `Embed`.
+ Class that interactively paginates something.
Parameters
----------
@@ -16,11 +16,8 @@ class PaginatorSession:
The context of the command.
timeout : float
How long to wait for before the session closes.
- embeds : List[Embed]
+ pages : List[Any]
A list of entries to paginate.
- edit_footer : bool, optional
- Whether to set the footer.
- Defaults to `True`.
Attributes
----------
@@ -28,7 +25,7 @@ class PaginatorSession:
The context of the command.
timeout : float
How long to wait for before the session closes.
- embeds : List[Embed]
+ pages : List[Any]
A list of entries to paginate.
running : bool
Whether the paginate session is running.
@@ -36,18 +33,17 @@ class PaginatorSession:
The `Message` of the `Embed`.
current : int
The current page number.
- reaction_map : Dict[str, meth]
+ reaction_map : Dict[str, method]
A mapping for reaction to method.
-
"""
- def __init__(self, ctx: commands.Context, *embeds, **options):
+ def __init__(self, ctx: commands.Context, *pages, **options):
self.ctx = ctx
self.timeout: int = options.get("timeout", 210)
- self.embeds: typing.List[Embed] = list(embeds)
self.running = False
self.base: Message = None
self.current = 0
+ self.pages = list(pages)
self.destination = options.get("destination", ctx)
self.reaction_map = {
"⏮": self.first_page,
@@ -57,48 +53,31 @@ def __init__(self, ctx: commands.Context, *embeds, **options):
"🛑": self.close,
}
- if options.get("edit_footer", True) and len(self.embeds) > 1:
- for i, embed in enumerate(self.embeds):
- footer_text = f"Page {i + 1} of {len(self.embeds)}"
- if embed.footer.text:
- footer_text = footer_text + " • " + embed.footer.text
- embed.set_footer(text=footer_text, icon_url=embed.footer.icon_url)
-
- def add_page(self, embed: Embed) -> None:
+ def add_page(self, item) -> None:
"""
- Add a `Embed` page.
-
- Parameters
- ----------
- embed : Embed
- The `Embed` to add.
+ Add a page.
"""
- if isinstance(embed, Embed):
- self.embeds.append(embed)
- else:
- raise TypeError("Page must be an Embed object.")
+ raise NotImplementedError
- async def create_base(self, embed: Embed) -> None:
+ async def create_base(self, item) -> None:
"""
Create a base `Message`.
-
- Parameters
- ----------
- embed : Embed
- The `Embed` to fill the base `Message`.
"""
- self.base = await self.destination.send(embed=embed)
+ await self._create_base(item)
- if len(self.embeds) == 1:
+ if len(self.pages) == 1:
self.running = False
return
self.running = True
for reaction in self.reaction_map:
- if len(self.embeds) == 2 and reaction in "⏮⏭":
+ if len(self.pages) == 2 and reaction in "⏮⏭":
continue
await self.base.add_reaction(reaction)
+ async def _create_base(self, item) -> None:
+ raise NotImplementedError
+
async def show_page(self, index: int) -> None:
"""
Show a page by page number.
@@ -108,17 +87,20 @@ async def show_page(self, index: int) -> None:
index : int
The index of the page.
"""
- if not 0 <= index < len(self.embeds):
+ if not 0 <= index < len(self.pages):
return
self.current = index
- page = self.embeds[index]
+ page = self.pages[index]
if self.running:
- await self.base.edit(embed=page)
+ return await self._show_page(page)
else:
await self.create_base(page)
+ async def _show_page(self, page):
+ raise NotImplementedError
+
def react_check(self, reaction: Reaction, user: User) -> bool:
"""
@@ -218,201 +200,59 @@ async def last_page(self) -> None:
"""
Go to the last page.
"""
- await self.show_page(len(self.embeds) - 1)
+ await self.show_page(len(self.pages) - 1)
-class MessagePaginatorSession:
+class EmbedPaginatorSession(PaginatorSession):
+ def __init__(self, ctx: commands.Context, *embeds, **options):
+ super().__init__(ctx, *embeds, **options)
- # TODO: Subclass MessagePaginatorSession from PaginatorSession
+ if len(self.pages) > 1:
+ for i, embed in enumerate(self.pages):
+ footer_text = f"Page {i + 1} of {len(self.pages)}"
+ if embed.footer.text:
+ footer_text = footer_text + " • " + embed.footer.text
+ embed.set_footer(text=footer_text, icon_url=embed.footer.icon_url)
+
+ def add_page(self, embed: Embed) -> None:
+ if isinstance(embed, Embed):
+ self.pages.append(embed)
+ else:
+ raise TypeError("Page must be an Embed object.")
+
+ async def _create_base(self, embed: Embed) -> None:
+ self.base = await self.destination.send(embed=embed)
+
+ async def _show_page(self, page):
+ await self.base.edit(embed=page)
+
+
+class MessagePaginatorSession(PaginatorSession):
def __init__(
self, ctx: commands.Context, *messages, embed: Embed = None, **options
):
- self.ctx = ctx
- self.timeout: int = options.get("timeout", 180)
- self.messages: typing.List[str] = list(messages)
-
- self.running = False
- self.base: Message = None
self.embed = embed
- if embed is not None:
- self.footer_text = self.embed.footer.text
- else:
- self.footer_text = None
-
- self.current = 0
- self.reaction_map = {
- "⏮": self.first_page,
- "◀": self.previous_page,
- "▶": self.next_page,
- "⏭": self.last_page,
- "🛑": self.close,
- }
+ self.footer_text = self.embed.footer.text if embed is not None else None
+ super().__init__(ctx, *messages, **options)
def add_page(self, msg: str) -> None:
- """
- Add a message page.
-
- Parameters
- ----------
- msg : str
- The message to add.
- """
if isinstance(msg, str):
- self.messages.append(msg)
+ self.pages.append(msg)
else:
raise TypeError("Page must be a str object.")
- async def create_base(self, msg: str) -> None:
- """
- Create a base `Message`.
-
- Parameters
- ----------
- msg : str
- The message content to fill the base `Message`.
- """
+ def _set_footer(self):
if self.embed is not None:
- footer_text = f"Page {self.current+1} of {len(self.messages)}"
+ footer_text = f"Page {self.current+1} of {len(self.pages)}"
if self.footer_text:
footer_text = footer_text + " • " + self.footer_text
self.embed.set_footer(text=footer_text, icon_url=self.embed.footer.icon_url)
+ async def _create_base(self, msg: str) -> None:
+ self._set_footer()
self.base = await self.ctx.send(content=msg, embed=self.embed)
- if len(self.messages) == 1:
- self.running = False
- return
-
- self.running = True
- for reaction in self.reaction_map:
- if len(self.messages) == 2 and reaction in "⏮⏭":
- continue
- await self.base.add_reaction(reaction)
-
- async def show_page(self, index: int) -> None:
- """
- Show a page by page number.
-
- Parameters
- ----------
- index : int
- The index of the page.
- """
- if not 0 <= index < len(self.messages):
- return
-
- self.current = index
- page = self.messages[index]
-
- if self.embed is not None:
- footer_text = f"Page {self.current + 1} of {len(self.messages)}"
- if self.footer_text:
- footer_text = footer_text + " • " + self.footer_text
- self.embed.set_footer(text=footer_text, icon_url=self.embed.footer.icon_url)
-
- if self.running:
- await self.base.edit(content=page, embed=self.embed)
- else:
- await self.create_base(page)
-
- def react_check(self, reaction: Reaction, user: User) -> bool:
- """
-
- Parameters
- ----------
- reaction : Reaction
- The `Reaction` object of the reaction.
- user : User
- The `User` or `Member` object of who sent the reaction.
-
- Returns
- -------
- bool
- """
- return (
- reaction.message.id == self.base.id
- and user.id == self.ctx.author.id
- and reaction.emoji in self.reaction_map.keys()
- )
-
- async def run(self) -> typing.Optional[Message]:
- """
- Starts the pagination session.
-
- Returns
- -------
- Optional[Message]
- If it's closed before running ends.
- """
- if not self.running:
- await self.show_page(self.current)
- while self.running:
- try:
- reaction, user = await self.ctx.bot.wait_for(
- "reaction_add", check=self.react_check, timeout=self.timeout
- )
- except asyncio.TimeoutError:
- return await self.close(delete=False)
- else:
- action = self.reaction_map.get(reaction.emoji)
- await action()
- try:
- await self.base.remove_reaction(reaction, user)
- except (HTTPException, InvalidArgument):
- pass
-
- async def previous_page(self) -> None:
- """
- Go to the previous page.
- """
- await self.show_page(self.current - 1)
-
- async def next_page(self) -> None:
- """
- Go to the next page.
- """
- await self.show_page(self.current + 1)
-
- async def close(self, delete: bool = True) -> typing.Optional[Message]:
- """
- Closes the pagination session.
-
- Parameters
- ----------
- delete : bool, optional
- Whether or delete the message upon closure.
- Defaults to `True`.
-
- Returns
- -------
- Optional[Message]
- If `delete` is `True`.
- """
- self.running = False
-
- sent_emoji, _ = await self.ctx.bot.retrieve_emoji()
- try:
- await self.ctx.message.add_reaction(sent_emoji)
- except (HTTPException, InvalidArgument):
- pass
-
- if delete:
- return await self.base.delete()
-
- try:
- await self.base.clear_reactions()
- except HTTPException:
- pass
-
- async def first_page(self) -> None:
- """
- Go to the first page.
- """
- await self.show_page(0)
-
- async def last_page(self) -> None:
- """
- Go to the last page.
- """
- await self.show_page(len(self.messages) - 1)
+ async def _show_page(self, page) -> None:
+ self._set_footer()
+ await self.base.edit(content=page, embed=self.embed)
From e0d6c07828b992e59a104e90f73f6470492dd437 Mon Sep 17 00:00:00 2001
From: Taaku18 <45324516+Taaku18@users.noreply.github.com>
Date: Mon, 5 Aug 2019 09:25:55 -0600
Subject: [PATCH 0361/1221] Add pull.yml
---
.github/pull.yml | 8 ++++++++
1 file changed, 8 insertions(+)
create mode 100644 .github/pull.yml
diff --git a/.github/pull.yml b/.github/pull.yml
new file mode 100644
index 0000000000..2fec8bbda0
--- /dev/null
+++ b/.github/pull.yml
@@ -0,0 +1,8 @@
+version: "1"
+rules:
+ - base: master
+ upstream: kyb3r:master
+ mergeMethod: hardreset
+ - base: development
+ upstream: kyb3r:development
+ mergeMethod: hardreset
\ No newline at end of file
From c11593d184a529b9d732ae9cc0f1b7d221698c8f Mon Sep 17 00:00:00 2001
From: Agent-Fennec <12753219+Agent-Fennec@users.noreply.github.com>
Date: Tue, 6 Aug 2019 02:42:07 -0400
Subject: [PATCH 0362/1221] A few style fixes and customisations (#342)
---
README.md | 50 ++++++++++++++++++++++++++++++++++----------------
core/thread.py | 11 ++++++++---
2 files changed, 42 insertions(+), 19 deletions(-)
diff --git a/README.md b/README.md
index d887601646..afc5f1f51c 100644
--- a/README.md
+++ b/README.md
@@ -4,21 +4,39 @@
A feature rich Modmail bot for Discord.
-
+
-
+
-
-
+
+
+
+
-
+
-
+
-
+
+
+
+
+
-
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -33,7 +51,7 @@
Modmail is similar to Reddit's Modmail both in functionality and purpose. It serves as a shared inbox for server staff to communicate with their users in a seamless way.
## How does it work?
-When a member sends a direct message to the bot, a channel or "thread" is created within an isolated category for that member. This channel is where messages will be relayed and where any available staff member can respond to that user.
+When a member sends a direct message to the bot, a channel or "thread" is created within an isolated category for that member. This channel is where messages will be relayed and where any available staff member can respond to that user.
All threads are logged and you can view previous threads through the corresponding generated log link. [**Example**](https://logs.modmail.tk/example)
@@ -61,7 +79,7 @@ This list is ever growing thanks to active development and our exceptional contr
## Installation
-### Locally
+### Locally
Installation locally for development reasons or otherwise is as follows, you will need `python 3.7`.
Clone the repo
@@ -75,7 +93,7 @@ Install dependancies
$ pipenv install
```
-Rename the `.env.example` to `.env` and fill out the fields.
+Rename the `.env.example` to `.env` and fill out the fields.
And finally, run the bot.
```console
$ pipenv run python3 bot.py
@@ -83,7 +101,7 @@ $ pipenv run python3 bot.py
### Hosting for patrons
-If you don't want to go through the trouble of setting up your own bot, and want to support this project as well, we offer installation, hosting and maintainance for Modmail bots for [**Patrons**](https://patreon.com/kyber). Join the support server for more info!
+If you don't want to go through the trouble of setting up your own bot, and want to support this project as well, we offer installation, hosting and maintainance for Modmail bots for [**Patrons**](https://patreon.com/kyber). Join the support server for more info!
### Heroku
This bot can be hosted on heroku. Installation via Heroku is done in your web browser. The [**installation guide**](https://github.com/kyb3r/modmail/wiki/Installation) will guide you through the entire installation process. If you run into any problems, join the [development server](https://discord.gg/etJNHCQ) for help and support.
@@ -95,8 +113,8 @@ You can also set up autoupdates. To do this, [install the Pull app in your fork]
Sepcial thanks to our sponsors for supporting the project.
-
-
+
+
Become a [sponsor](https://patreon.com/kyber).
@@ -108,6 +126,6 @@ Modmail supports the use of third party plugins to extend or add functionality t
## Contributing
-Contributions to Modmail are always welcome, whether it be improvements to the documentation or new functionality, please feel free make the change. Check out our contribution [guidelines](https://github.com/kyb3r/modmail/blob/master/CONTRIBUTING.md) before you get started.
+Contributions to Modmail are always welcome, whether it be improvements to the documentation or new functionality, please feel free make the change. Check out our contribution [guidelines](https://github.com/kyb3r/modmail/blob/master/CONTRIBUTING.md) before you get started.
-This bot is free for everyone and always will be. If you like this project and would like to show your appreciation, here's the link for our **[Patreon](https://www.patreon.com/kyber)**.
+This bot is free for everyone and always will be. If you like this project and would like to show your appreciation, here's the link for our **[Patreon](https://www.patreon.com/kyber)**.
diff --git a/core/thread.py b/core/thread.py
index b307a0dc24..6590d93fb8 100644
--- a/core/thread.py
+++ b/core/thread.py
@@ -316,7 +316,9 @@ async def _close(
"{closer.mention} has closed this Modmail thread.",
)
- message = message.format(closer=closer, loglink=log_url, logkey=log_data["key"] if log_data else None)
+ message = message.format(
+ closer=closer, loglink=log_url, logkey=log_data["key"] if log_data else None
+ )
embed.description = message
footer = self.bot.config.get(
@@ -736,9 +738,12 @@ async def find(
if recipient is None and channel is not None:
thread = self._find_from_channel(channel)
if thread is None:
- id, thread = next(((k, v) for k, v in self.cache.items() if v.channel == channel), (-1, None))
+ id, thread = next(
+ ((k, v) for k, v in self.cache.items() if v.channel == channel),
+ (-1, None),
+ )
if thread is not None:
- logger.debug('Found thread with tempered ID.')
+ logger.debug("Found thread with tempered ID.")
await channel.edit(topic=f"User ID: {id}")
return thread
From 56cef8dd7b16d5f1d7e8b542aeb036a780516640 Mon Sep 17 00:00:00 2001
From: Taaku18 <45324516+Taaku18@users.noreply.github.com>
Date: Tue, 6 Aug 2019 00:35:17 -0700
Subject: [PATCH 0363/1221] Formatting and changelog change
---
CHANGELOG.md | 3 +
bot.py | 111 ++++++++++++++---------------
cogs/modmail.py | 48 ++++++++++---
cogs/plugins.py | 6 +-
cogs/utility.py | 161 ++++++++++++++++++++++++++----------------
core/checks.py | 10 +++
core/clients.py | 12 ++--
core/config_help.json | 4 +-
core/paginator.py | 1 -
core/thread.py | 10 +--
10 files changed, 220 insertions(+), 146 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index da6784d12c..22dc37c980 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -43,6 +43,9 @@ however, insignificant breaking changes does not guarantee a major version bump,
- The placeholder channel for the streaming status changed to https://www.twitch.tv/discordmodmail/.
- Removed unclear `rm` alias for some `remove` commands.
- Paginate `?config options`.
+- All users configured with a permission level greater than REGULAR has access to the main Modmail category.
+ - Category overrides also changes when a level is removed or added to a user or role.
+- `@everyone` is now accepted for `?perms add`.
### Fixes
diff --git a/bot.py b/bot.py
index 6031fd3574..46f1081e3b 100644
--- a/bot.py
+++ b/bot.py
@@ -22,6 +22,7 @@
from pymongo.errors import ConfigurationError
try:
+ # noinspection PyUnresolvedReferences
from colorama import init
init()
@@ -94,7 +95,17 @@ def __init__(self):
sys.exit(0)
self.plugin_db = PluginDatabaseClient(self)
+
+ logger.line()
+ logger.info("┌┬┐┌─┐┌┬┐┌┬┐┌─┐┬┬")
+ logger.info("││││ │ │││││├─┤││")
+ logger.info("┴ ┴└─┘─┴┘┴ ┴┴ ┴┴┴─┘")
+ logger.info("v%s", __version__)
+ logger.info("Authors: kyb3r, fourjr, Taaku18")
+ logger.line()
+
self._load_extensions()
+ logger.line()
@property
def uptime(self) -> str:
@@ -168,14 +179,6 @@ async def get_prefix(self, message=None):
def _load_extensions(self):
"""Adds commands automatically"""
- logger.line()
- logger.info("┌┬┐┌─┐┌┬┐┌┬┐┌─┐┬┬")
- logger.info("││││ │ │││││├─┤││")
- logger.info("┴ ┴└─┘─┴┘┴ ┴┴ ┴┴┴─┘")
- logger.info("v%s", __version__)
- logger.info("Authors: kyb3r, fourjr, Taaku18")
- logger.line()
-
for file in os.listdir("cogs"):
if not file.endswith(".py"):
continue
@@ -232,9 +235,12 @@ async def is_owner(self, user: discord.User) -> bool:
def log_channel(self) -> typing.Optional[discord.TextChannel]:
channel_id = self.config["log_channel_id"]
if channel_id is not None:
- channel = self.get_channel(int(channel_id))
- if channel is not None:
- return channel
+ try:
+ channel = self.get_channel(int(channel_id))
+ if channel is not None:
+ return channel
+ except ValueError:
+ pass
logger.debug("LOG_CHANNEL_ID was invalid, removed.")
self.config.remove("log_channel_id")
if self.main_category is not None:
@@ -255,10 +261,6 @@ def log_channel(self) -> typing.Optional[discord.TextChannel]:
)
return None
- @property
- def is_connected(self) -> bool:
- return self._connected.is_set()
-
async def wait_for_connected(self) -> None:
await self.wait_until_ready()
await self._connected.wait()
@@ -277,7 +279,7 @@ def token(self) -> str:
token = self.config["token"]
if token is None:
logger.critical(
- "TOKEN must be set, set this as bot token found on the Discord Dev Portal."
+ "TOKEN must be set, set this as bot token found on the Discord Developer Portal."
)
sys.exit(0)
return token
@@ -289,6 +291,7 @@ def guild_id(self) -> typing.Optional[int]:
try:
return int(str(guild_id))
except ValueError:
+ self.config.remove("guild_id")
logger.critical("Invalid GUILD_ID set.")
return None
@@ -309,9 +312,12 @@ def modmail_guild(self) -> typing.Optional[discord.Guild]:
modmail_guild_id = self.config["modmail_guild_id"]
if modmail_guild_id is None:
return self.guild
- guild = discord.utils.get(self.guilds, id=int(modmail_guild_id))
- if guild is not None:
- return guild
+ try:
+ guild = discord.utils.get(self.guilds, id=int(modmail_guild_id))
+ if guild is not None:
+ return guild
+ except ValueError:
+ pass
self.config.remove("modmail_guild_id")
logger.critical("Invalid MODMAIL_GUILD_ID set.")
return self.guild
@@ -325,11 +331,14 @@ def main_category(self) -> typing.Optional[discord.CategoryChannel]:
if self.modmail_guild is not None:
category_id = self.config["main_category_id"]
if category_id is not None:
- cat = discord.utils.get(
- self.modmail_guild.categories, id=int(category_id)
- )
- if cat is not None:
- return cat
+ try:
+ cat = discord.utils.get(
+ self.modmail_guild.categories, id=int(category_id)
+ )
+ if cat is not None:
+ return cat
+ except ValueError:
+ pass
self.config.remove("main_category_id")
logger.debug("MAIN_CATEGORY_ID was invalid, removed.")
cat = discord.utils.get(self.modmail_guild.categories, name="Modmail")
@@ -353,41 +362,34 @@ def blocked_whitelisted_users(self) -> typing.List[str]:
def prefix(self) -> str:
return str(self.config["prefix"])
- @property
- def mod_color(self) -> int:
- color = self.config["mod_color"]
+ def _parse_color(self, conf_name):
+ color = self.config[conf_name]
try:
return int(color.lstrip("#"), base=16)
except ValueError:
- logger.error("Invalid mod_color provided.")
- return int(self.config.remove("mod_color").lstrip("#"), base=16)
+ logger.error("Invalid %s provided.", conf_name)
+ return int(self.config.remove(conf_name).lstrip("#"), base=16)
+
+ @property
+ def mod_color(self) -> int:
+ return self._parse_color("mod_color")
@property
def recipient_color(self) -> int:
- color = self.config["recipient_color"]
- try:
- return int(color.lstrip("#"), base=16)
- except ValueError:
- logger.error("Invalid recipient_color provided.")
- return int(self.config.remove("recipient_color").lstrip("#"), base=16)
+ return self._parse_color("recipient_color")
@property
def main_color(self) -> int:
- color = self.config["main_color"]
- try:
- return int(color.lstrip("#"), base=16)
- except ValueError:
- logger.error("Invalid main_color provided.")
- return int(self.config.remove("main_color").lstrip("#"), base=16)
+ return self._parse_color("main_color")
async def on_connect(self):
logger.line()
try:
await self.validate_database_connection()
except Exception:
+ logger.debug("Logging out due to failed database connection.")
return await self.logout()
- logger.line()
logger.info("Connected to gateway.")
await self.config.refresh()
await self.setup_indexes()
@@ -436,6 +438,8 @@ async def on_ready(self):
logger.info("Prefix: %s", self.prefix)
logger.info("Guild Name: %s", self.guild.name)
logger.info("Guild ID: %s", self.guild.id)
+ if self.using_multiple_server_setup:
+ logger.info("Receiving guild ID: %s", self.modmail_guild.id)
logger.line()
await self.threads.populate_cache()
@@ -508,6 +512,7 @@ async def retrieve_emoji(self) -> typing.Tuple[str, str]:
except commands.BadArgument:
logger.warning("Removed sent emoji (%s).", sent_emoji)
sent_emoji = self.config.remove("sent_emoji")
+ await self.config.update()
if blocked_emoji != "disable":
try:
@@ -515,8 +520,8 @@ async def retrieve_emoji(self) -> typing.Tuple[str, str]:
except commands.BadArgument:
logger.warning("Removed blocked emoji (%s).", blocked_emoji)
blocked_emoji = self.config.remove("blocked_emoji")
+ await self.config.update()
- await self.config.update()
return sent_emoji, blocked_emoji
async def _process_blocked(self, message: discord.Message) -> bool:
@@ -531,7 +536,7 @@ async def _process_blocked(self, message: discord.Message) -> bool:
try:
await message.add_reaction(sent_emoji)
except (discord.HTTPException, discord.InvalidArgument):
- pass
+ logger.warning("Failed to add sent_emoji.", exc_info=True)
return False
@@ -761,6 +766,7 @@ async def get_context(self, message, *, cls=commands.Context):
async def update_perms(
self, name: typing.Union[PermissionLevel, str], value: int, add: bool = True
) -> None:
+ value = int(value)
if isinstance(name, PermissionLevel):
permissions = self.config["level_permissions"]
name = name.name
@@ -1043,19 +1049,6 @@ async def on_command_error(self, context, exception):
else:
logger.error("Unexpected exception:", exc_info=exception)
- @staticmethod
- def overwrites(ctx: commands.Context) -> dict:
- """Permission overwrites for the guild."""
- overwrites = {
- ctx.guild.default_role: discord.PermissionOverwrite(read_messages=False),
- ctx.guild.me: discord.PermissionOverwrite(read_messages=True),
- }
-
- for role in ctx.guild.roles:
- if role.permissions.administrator:
- overwrites[role] = discord.PermissionOverwrite(read_messages=True)
- return overwrites
-
async def validate_database_connection(self):
try:
await self.db.command("buildinfo")
@@ -1113,12 +1106,14 @@ async def before_post_metadata(self):
if not self.guild:
self.metadata_loop.cancel()
- async def after_post_metadata(self):
+ @staticmethod
+ async def after_post_metadata():
logger.info("Metadata loop has been cancelled.")
if __name__ == "__main__":
try:
+ # noinspection PyUnresolvedReferences
import uvloop
logger.debug("Setting up with uvloop.")
diff --git a/cogs/modmail.py b/cogs/modmail.py
index c3750d7900..486ddb303b 100644
--- a/cogs/modmail.py
+++ b/cogs/modmail.py
@@ -3,7 +3,7 @@
from datetime import datetime
from itertools import zip_longest
from typing import Optional, Union
-from types import SimpleNamespace as param
+from types import SimpleNamespace
import discord
from discord.ext import commands
@@ -39,6 +39,11 @@ async def setup(self, ctx):
once after configuring Modmail.
"""
+ if ctx.guild != self.bot.modmail_guild:
+ return await ctx.send(
+ f"You can only setup in the Modmail guild: {self.bot.modmail_guild}."
+ )
+
if self.bot.main_category is not None:
logger.debug("Can't re-setup server, main_category is found.")
return await ctx.send(f"{self.bot.modmail_guild} is already set up.")
@@ -51,8 +56,31 @@ async def setup(self, ctx):
)
return await ctx.send(embed=embed)
+ overwrites = {
+ self.bot.modmail_guild.default_role: discord.PermissionOverwrite(
+ read_messages=False
+ ),
+ self.bot.modmail_guild.me: discord.PermissionOverwrite(read_messages=True),
+ }
+
+ for level in PermissionLevel:
+ if level <= PermissionLevel.REGULAR:
+ continue
+ permissions = self.bot.config["level_permissions"].get(level.name, [])
+ for perm in permissions:
+ perm = int(perm)
+ if perm == -1:
+ key = self.bot.modmail_guild.default_role
+ else:
+ key = self.bot.modmail_guild.get_member(perm)
+ if key is None:
+ key = self.bot.modmail_guild.get_role(perm)
+ if key is not None:
+ logger.info("Granting %s access to Modmail category.", key.name)
+ overwrites[key] = discord.PermissionOverwrite(read_messages=True)
+
category = await self.bot.modmail_guild.create_category(
- name="Modmail", overwrites=self.bot.overwrites(ctx)
+ name="Modmail", overwrites=overwrites
)
await category.edit(position=0)
@@ -86,10 +114,12 @@ async def setup(self, ctx):
await self.bot.config.update()
await ctx.send(
- "Successfully set up server.\n"
- "Consider setting permission levels to give access "
- "to roles or users the ability to use Modmail.\n"
- f"Type `{self.bot.prefix}permissions` for more info."
+ "**Successfully set up server.**\n"
+ "Consider setting permission levels "
+ "to give access to roles or users the ability to use Modmail.\n\n"
+ f"Type:\n- `{self.bot.prefix}permissions` and `{self.bot.prefix}permissions add` "
+ "for more info on setting permissions.\n"
+ f"- `{self.bot.prefix}config help` for a list of available customizations."
)
if (
@@ -612,7 +642,7 @@ async def logs(self, ctx, *, user: User = None):
if not user:
thread = ctx.thread
if not thread:
- raise commands.MissingRequiredArgument(param(name="member"))
+ raise commands.MissingRequiredArgument(SimpleNamespace(name="member"))
user = thread.recipient
default_avatar = "https://cdn.discordapp.com/embed/avatars/0.png"
@@ -977,7 +1007,7 @@ async def block(
if thread:
user = thread.recipient
elif after is None:
- raise commands.MissingRequiredArgument(param(name="user"))
+ raise commands.MissingRequiredArgument(SimpleNamespace(name="user"))
else:
raise commands.BadArgument(f'User "{after.arg}" not found')
@@ -1058,7 +1088,7 @@ async def unblock(self, ctx, *, user: User = None):
if thread:
user = thread.recipient
else:
- raise commands.MissingRequiredArgument(param(name="user"))
+ raise commands.MissingRequiredArgument(SimpleNamespace(name="user"))
mention = getattr(user, "mention", f"`{user.id}`")
name = getattr(user, "name", f"`{user.id}`")
diff --git a/cogs/plugins.py b/cogs/plugins.py
index f871209bb6..4ea76503be 100644
--- a/cogs/plugins.py
+++ b/cogs/plugins.py
@@ -62,9 +62,9 @@ def parse_plugin(name):
if "@" in result[2]:
# branch is specified
# for example, fourjr/modmail-plugins/welcomer@develop is a valid name
- branchsplit_result = result[2].split("@")
- result.append(branchsplit_result[-1])
- result[2] = "@".join(branchsplit_result[:-1])
+ branch_split_result = result[2].split("@")
+ result.append(branch_split_result[-1])
+ result[2] = "@".join(branch_split_result[:-1])
else:
result.append("master")
diff --git a/cogs/utility.py b/cogs/utility.py
index f661cecb40..188da898bf 100644
--- a/cogs/utility.py
+++ b/cogs/utility.py
@@ -11,12 +11,12 @@
from itertools import zip_longest, takewhile
from json import JSONDecodeError, loads
from textwrap import indent
-from types import SimpleNamespace as param
+from types import SimpleNamespace
from typing import Union
from discord import Embed, Color, Activity, Role
from discord.enums import ActivityType, Status
-from discord.ext import commands
+from discord.ext import commands, tasks
from discord.utils import escape_markdown, escape_mentions
from aiohttp import ClientResponseError
@@ -27,14 +27,7 @@
from core.decorators import trigger_typing
from core.models import InvalidConfigError, PermissionLevel
from core.paginator import EmbedPaginatorSession, MessagePaginatorSession
-from core.utils import (
- cleanup_code,
- User,
- get_perm_level,
- create_not_found_embed,
- parse_alias,
- format_description,
-)
+from core import utils
logger = logging.getLogger("Modmail")
@@ -46,9 +39,11 @@ async def format_cog_help(self, cog, *, no_cog=False):
formats = [""]
for cmd in await self.filter_commands(
- cog.get_commands() if not no_cog else cog, sort=True, key=get_perm_level
+ cog.get_commands() if not no_cog else cog,
+ sort=True,
+ key=utils.get_perm_level,
):
- perm_level = get_perm_level(cmd)
+ perm_level = utils.get_perm_level(cmd)
if perm_level is PermissionLevel.INVALID:
format_ = f"`{prefix + cmd.qualified_name}` "
else:
@@ -127,7 +122,7 @@ async def send_cog_help(self, cog):
async def send_command_help(self, command):
if not await self.filter_commands([command]):
return
- perm_level = get_perm_level(command)
+ perm_level = utils.get_perm_level(command)
if perm_level is not PermissionLevel.INVALID:
perm_level = f"{perm_level.name} [{perm_level}]"
else:
@@ -145,7 +140,7 @@ async def send_group_help(self, group):
if not await self.filter_commands([group]):
return
- perm_level = get_perm_level(group)
+ perm_level = utils.get_perm_level(group)
if perm_level is not PermissionLevel.INVALID:
perm_level = f"{perm_level.name} [{perm_level}]"
else:
@@ -192,7 +187,7 @@ async def send_error_message(self, error):
val = self.context.bot.aliases.get(command)
if val is not None:
- values = parse_alias(val)
+ values = utils.parse_alias(val)
if len(values) == 1:
embed = Embed(
@@ -246,22 +241,14 @@ def __init__(self, bot):
self.bot = bot
self._original_help_command = bot.help_command
self.bot.help_command = ModmailHelpCommand(
- verify_checks=False, command_attrs={"help": "Shows this help message."}
+ verify_checks=False,
+ command_attrs={
+ "help": "Shows this help message.",
+ "checks": [checks.has_permissions_help(PermissionLevel.REGULAR)],
+ },
)
- # Looks a bit ugly
- self.bot.help_command._command_impl = checks.has_permissions( # pylint: disable=protected-access
- PermissionLevel.REGULAR
- )(
- self.bot.help_command._command_impl # pylint: disable=protected-access
- )
-
self.bot.help_command.cog = self
-
- # Class Variables
- self.presence = None
-
- # Tasks
- self.presence_task = self.bot.loop.create_task(self.loop_presence())
+ self.loop_presence.start()
def cog_unload(self):
self.bot.help_command = self._original_help_command
@@ -512,7 +499,7 @@ async def activity(self, ctx, activity_type: str.lower, *, message: str = ""):
return await ctx.send(embed=embed)
if not message:
- raise commands.MissingRequiredArgument(param(name="message"))
+ raise commands.MissingRequiredArgument(SimpleNamespace(name="message"))
activity, msg = (
await self.set_presence(
@@ -522,7 +509,7 @@ async def activity(self, ctx, activity_type: str.lower, *, message: str = ""):
)
)["activity"]
if activity is None:
- raise commands.MissingRequiredArgument(param(name="activity"))
+ raise commands.MissingRequiredArgument(SimpleNamespace(name="activity"))
self.bot.config["activity_type"] = activity.type.value
self.bot.config["activity_message"] = message
@@ -561,7 +548,7 @@ async def status(self, ctx, *, status_type: str.lower):
await self.set_presence(status_identifier=status_type, status_by_key=True)
)["status"]
if status is None:
- raise commands.MissingRequiredArgument(param(name="status"))
+ raise commands.MissingRequiredArgument(SimpleNamespace(name="status"))
self.bot.config["status"] = status.value
await self.bot.config.update()
@@ -631,7 +618,7 @@ async def set_presence(
activity = Activity(type=activity_type, name=activity_message, url=url)
else:
msg = "You must supply an activity message to use custom activity."
- logger.warning(msg)
+ logger.debug(msg)
await self.bot.change_presence(activity=activity, status=status)
@@ -649,19 +636,18 @@ async def set_presence(
presence["status"] = (status, msg)
return presence
- @commands.Cog.listener()
- async def on_ready(self):
- # Wait until config cache is populated with stuff from db
- await self.bot.wait_for_connected()
- logger.info(self.presence["activity"][1])
- logger.info(self.presence["status"][1])
-
+ @tasks.loop(minutes=45)
async def loop_presence(self):
- """Set presence to the configured value every hour."""
+ """Set presence to the configured value every 45 minutes."""
+ presence = await self.set_presence()
+ logger.line()
+ logger.info(presence["activity"][1])
+ logger.info(presence["status"][1])
+
+ @loop_presence.before_loop
+ async def before_loop_presence(self):
+ logger.info("Starting metadata loop.")
await self.bot.wait_for_connected()
- while not self.bot.is_closed():
- self.presence = await self.set_presence()
- await asyncio.sleep(600)
@commands.command()
@checks.has_permissions(PermissionLevel.ADMINISTRATOR)
@@ -867,11 +853,11 @@ async def config_get(self, ctx, key: str.lower = None):
@config.command(name="help", aliases=["info"])
@checks.has_permissions(PermissionLevel.OWNER)
- async def config_help(self, ctx, key: str.lower):
+ async def config_help(self, ctx, key: str.lower = None):
"""
Show information on a specified configuration.
"""
- if not (
+ if key is not None and not (
key in self.bot.config.public_keys or key in self.bot.config.protected_keys
):
embed = Embed(
@@ -883,7 +869,7 @@ async def config_help(self, ctx, key: str.lower):
config_help = self.bot.config.config_help
- if key not in config_help:
+ if key is not None and key not in config_help:
embed = Embed(
title="Error",
color=Color.red(),
@@ -955,10 +941,12 @@ async def alias(self, ctx, *, name: str.lower = None):
if name is not None:
val = self.bot.aliases.get(name)
if val is None:
- embed = create_not_found_embed(name, self.bot.aliases.keys(), "Alias")
+ embed = utils.create_not_found_embed(
+ name, self.bot.aliases.keys(), "Alias"
+ )
return await ctx.send(embed=embed)
- values = parse_alias(val)
+ values = utils.parse_alias(val)
if not values:
embed = Embed(
@@ -998,7 +986,7 @@ async def alias(self, ctx, *, name: str.lower = None):
embeds = []
for i, names in enumerate(zip_longest(*(iter(sorted(self.bot.aliases)),) * 15)):
- description = format_description(i, names)
+ description = utils.format_description(i, names)
embed = Embed(color=self.bot.main_color, description=description)
embed.set_author(name="Command Aliases", icon_url=ctx.guild.icon_url)
embeds.append(embed)
@@ -1014,7 +1002,7 @@ async def alias_raw(self, ctx, *, name: str.lower):
"""
val = self.bot.aliases.get(name)
if val is None:
- embed = create_not_found_embed(name, self.bot.aliases.keys(), "Alias")
+ embed = utils.create_not_found_embed(name, self.bot.aliases.keys(), "Alias")
return await ctx.send(embed=embed)
return await ctx.send(escape_markdown(escape_mentions(val)).replace("<", "\\<"))
@@ -1066,7 +1054,7 @@ async def alias_add(self, ctx, name: str.lower, *, value):
if embed is not None:
return await ctx.send(embed=embed)
- values = parse_alias(value)
+ values = utils.parse_alias(value)
if not values:
embed = Embed(
@@ -1133,7 +1121,7 @@ async def alias_remove(self, ctx, *, name: str.lower):
description=f"Successfully deleted `{name}`.",
)
else:
- embed = create_not_found_embed(name, self.bot.aliases.keys(), "Alias")
+ embed = utils.create_not_found_embed(name, self.bot.aliases.keys(), "Alias")
return await ctx.send(embed=embed)
@@ -1144,10 +1132,10 @@ async def alias_edit(self, ctx, name: str.lower, *, value):
Edit an alias.
"""
if name not in self.bot.aliases:
- embed = create_not_found_embed(name, self.bot.aliases.keys(), "Alias")
+ embed = utils.create_not_found_embed(name, self.bot.aliases.keys(), "Alias")
return await ctx.send(embed=embed)
- values = parse_alias(value)
+ values = utils.parse_alias(value)
if not values:
embed = Embed(
@@ -1222,10 +1210,13 @@ async def permissions(self, ctx):
@staticmethod
def _verify_user_or_role(user_or_role):
+ if isinstance(user_or_role, Role):
+ if user_or_role.is_default():
+ return -1
+ elif user_or_role in {"everyone", "all"}:
+ return -1
if hasattr(user_or_role, "id"):
return user_or_role.id
- if user_or_role in {"everyone", "all"}:
- return -1
raise commands.BadArgument(f'User or Role "{user_or_role}" not found')
@staticmethod
@@ -1247,7 +1238,12 @@ def _parse_level(name):
@permissions.command(name="add", usage="[command/level] [name] [user_or_role]")
@checks.has_permissions(PermissionLevel.OWNER)
async def permissions_add(
- self, ctx, type_: str.lower, name: str, *, user_or_role: Union[Role, User, str]
+ self,
+ ctx,
+ type_: str.lower,
+ name: str,
+ *,
+ user_or_role: Union[Role, utils.User, str],
):
"""
Add a permission to a command or a permission level.
@@ -1290,6 +1286,18 @@ async def permissions_add(
else:
await self.bot.update_perms(level, value)
name = level.name
+ if level > PermissionLevel.REGULAR:
+ if value == -1:
+ key = self.bot.modmail_guild.default_role
+ elif isinstance(user_or_role, Role):
+ key = user_or_role
+ else:
+ key = self.bot.modmail_guild.get_member(value)
+ if key is not None:
+ logger.info("Granting %s access to Modmail category.", key.name)
+ await self.bot.main_category.set_permissions(
+ key, read_messages=True
+ )
embed = Embed(
title="Success",
@@ -1305,7 +1313,12 @@ async def permissions_add(
)
@checks.has_permissions(PermissionLevel.OWNER)
async def permissions_remove(
- self, ctx, type_: str.lower, name: str, *, user_or_role: Union[Role, User, str]
+ self,
+ ctx,
+ type_: str.lower,
+ name: str,
+ *,
+ user_or_role: Union[Role, utils.User, str],
):
"""
Remove permission to use a command or permission level.
@@ -1341,6 +1354,30 @@ async def permissions_remove(
value = self._verify_user_or_role(user_or_role)
await self.bot.update_perms(level or name, value, add=False)
+ if type_ == "level":
+ if level > PermissionLevel.REGULAR:
+ if value == -1:
+ logger.info("Denying @everyone access to Modmail category.")
+ await self.bot.main_category.set_permissions(
+ self.bot.modmail_guild.default_role, read_messages=False
+ )
+ elif isinstance(user_or_role, Role):
+ logger.info(
+ "Denying %s access to Modmail category.", user_or_role.name
+ )
+ await self.bot.main_category.set_permissions(
+ user_or_role, overwrite=None
+ )
+ else:
+ member = self.bot.modmail_guild.get_member(value)
+ if member is not None and member != self.bot.modmail_guild.me:
+ logger.info(
+ "Denying %s access to Modmail category.", member.name
+ )
+ await self.bot.main_category.set_permissions(
+ member, overwrite=None
+ )
+
embed = Embed(
title="Success",
color=self.bot.main_color,
@@ -1389,7 +1426,7 @@ def _get_perm(self, ctx, name, type_):
@permissions.command(name="get", usage="[@user] or [command/level] [name]")
@checks.has_permissions(PermissionLevel.OWNER)
async def permissions_get(
- self, ctx, user_or_role: Union[Role, User, str], *, name: str = None
+ self, ctx, user_or_role: Union[Role, utils.User, str], *, name: str = None
):
"""
View the currently-set permissions.
@@ -1514,7 +1551,7 @@ async def oauth(self, ctx):
@oauth.command(name="whitelist")
@checks.has_permissions(PermissionLevel.OWNER)
- async def oauth_whitelist(self, ctx, target: Union[User, Role]):
+ async def oauth_whitelist(self, ctx, target: Union[Role, utils.User]):
"""
Whitelist or un-whitelist a user or role to have access to logs.
@@ -1593,7 +1630,7 @@ async def eval_(self, ctx, *, body: str):
env.update(globals())
- body = cleanup_code(body)
+ body = utils.cleanup_code(body)
stdout = StringIO()
to_compile = f'async def func():\n{indent(body, " ")}'
diff --git a/core/checks.py b/core/checks.py
index 503877b3a7..17d2b4353b 100644
--- a/core/checks.py
+++ b/core/checks.py
@@ -7,6 +7,16 @@
logger = logging.getLogger("Modmail")
+def has_permissions_help(permission_level: PermissionLevel = PermissionLevel.REGULAR):
+ async def predicate(ctx):
+ return await check_permissions(
+ ctx, ctx.command.qualified_name, permission_level
+ )
+
+ predicate.permission_level = permission_level
+ return predicate
+
+
def has_permissions(permission_level: PermissionLevel = PermissionLevel.REGULAR):
"""
A decorator that checks if the author has the required permissions.
diff --git a/core/clients.py b/core/clients.py
index 31a73be390..a8bb006e6d 100644
--- a/core/clients.py
+++ b/core/clients.py
@@ -99,9 +99,9 @@ async def get_log(self, channel_id: Union[str, int]) -> dict:
async def get_log_link(self, channel_id: Union[str, int]) -> str:
doc = await self.get_log(channel_id)
logger.debug("Retrieving log link for channel %s.", channel_id)
- prefix = self.bot.config['log_url_prefix'].strip('/')
- if prefix == 'NONE':
- prefix = ''
+ prefix = self.bot.config["log_url_prefix"].strip("/")
+ if prefix == "NONE":
+ prefix = ""
return f"{self.bot.config['log_url'].strip('/')}{'/' + prefix if prefix else ''}/{doc['key']}"
async def create_log_entry(
@@ -138,9 +138,9 @@ async def create_log_entry(
}
)
logger.debug("Created a log entry, key %s.", key)
- prefix = self.bot.config['log_url_prefix'].strip('/')
- if prefix == 'NONE':
- prefix = ''
+ prefix = self.bot.config["log_url_prefix"].strip("/")
+ if prefix == "NONE":
+ prefix = ""
return f"{self.bot.config['log_url'].strip('/')}{'/' + prefix if prefix else ''}/{key}"
async def get_config(self) -> dict:
diff --git a/core/config_help.json b/core/config_help.json
index f4e8eae36f..be499bb230 100644
--- a/core/config_help.json
+++ b/core/config_help.json
@@ -281,7 +281,7 @@
"notes": [
"When `recipient_thread_close` is enabled and the recipient closed their own thread, `thread_self_close_response` is used instead of this configuration.",
"You may use the `{{closer}}` variable for access to the [Member](https://discordpy.readthedocs.io/en/latest/api.html#discord.Member) that closed the thread.",
- "`{{loglink}}` can be used as a placeholder substitute for the full URL linked to the thread in the log viewer and `{{loglink}}` for the unique key (ie. 3ifdm9204) of the log.",
+ "`{{loglink}}` can be used as a placeholder substitute for the full URL linked to the thread in the log viewer and `{{loglink}}` for the unique key (ie. s3kf91a) of the log.",
"Discord flavoured markdown is fully supported in `thread_close_response`.",
"See also: `thread_close_title`, `thread_close_footer`, `thread_self_close_response`, `thread_creation_response`."
]
@@ -295,7 +295,7 @@
"notes": [
"When `recipient_thread_close` is disabled or the thread wasn't closed by the recipient, `thread_close_response` is used instead of this configuration.",
"You may use the `{{closer}}` variable for access to the [Member](https://discordpy.readthedocs.io/en/latest/api.html#discord.Member) that closed the thread.",
- "`{{loglink}}` can be used as a placeholder substitute for the full URL linked to the thread in the log viewer and `{{loglink}}` for the unique key (ie. 3ifdm9204) of the log.",
+ "`{{loglink}}` can be used as a placeholder substitute for the full URL linked to the thread in the log viewer and `{{loglink}}` for the unique key (ie. s3kf91a) of the log.",
"Discord flavoured markdown is fully supported in `thread_self_close_response`.",
"See also: `thread_close_title`, `thread_close_footer`, `thread_close_response`."
]
diff --git a/core/paginator.py b/core/paginator.py
index 0a8aa8b814..c2bed558ed 100644
--- a/core/paginator.py
+++ b/core/paginator.py
@@ -228,7 +228,6 @@ async def _show_page(self, page):
class MessagePaginatorSession(PaginatorSession):
-
def __init__(
self, ctx: commands.Context, *messages, embed: Embed = None, **options
):
diff --git a/core/thread.py b/core/thread.py
index 95eeadc3dd..00aa24c9c7 100644
--- a/core/thread.py
+++ b/core/thread.py
@@ -4,7 +4,7 @@
import string
import typing
from datetime import datetime, timedelta
-from types import SimpleNamespace as param
+from types import SimpleNamespace
import isodate
@@ -144,8 +144,8 @@ async def send_genesis_message():
msg = await channel.send(mention, embed=info_embed)
self.bot.loop.create_task(msg.pin())
self.genesis_message = msg
- except Exception as e:
- logger.error(str(e))
+ except Exception:
+ logger.error("Failed unexpectedly:", exc_info=True)
finally:
self.ready = True
@@ -539,7 +539,7 @@ async def delete_message(self, message_id):
async def note(self, message: discord.Message) -> None:
if not message.content and not message.attachments:
- raise MissingRequiredArgument(param(name="msg"))
+ raise MissingRequiredArgument(SimpleNamespace(name="msg"))
_, msg = await asyncio.gather(
self.bot.api.append_log(message, self.channel.id, type_="system"),
@@ -550,7 +550,7 @@ async def note(self, message: discord.Message) -> None:
async def reply(self, message: discord.Message, anonymous: bool = False) -> None:
if not message.content and not message.attachments:
- raise MissingRequiredArgument(param(name="msg"))
+ raise MissingRequiredArgument(SimpleNamespace(name="msg"))
if not any(g.get_member(self.id) for g in self.bot.guilds):
return await message.channel.send(
embed=discord.Embed(
From 2c046227970d145b2b01fe687b5d76c90646c85e Mon Sep 17 00:00:00 2001
From: Taaku18 <45324516+Taaku18@users.noreply.github.com>
Date: Tue, 6 Aug 2019 22:21:35 -0700
Subject: [PATCH 0364/1221] Updated README.md
---
README.md | 60 +++++++++++++++++++++++++----------------------
core/paginator.py | 2 +-
core/utils.py | 2 +-
3 files changed, 34 insertions(+), 30 deletions(-)
diff --git a/README.md b/README.md
index 514d0719ba..e9aabe9a76 100644
--- a/README.md
+++ b/README.md
@@ -25,10 +25,6 @@
-
-
-
-
@@ -53,9 +49,10 @@ Modmail is similar to Reddit's Modmail both in functionality and purpose. It ser
This bot is free for everyone and always will be. If you like this project and would like to show your appreciation, you can support us on **[Patreon](https://www.patreon.com/kyber)**, cool benefits included!
## How does it work?
-When a member sends a direct message to the bot, a channel or "thread" is created within an isolated category for that member. This channel is where messages will be relayed and where any available staff member can respond to that user.
-All threads are logged and you can view previous threads through the corresponding generated log link. Here is an [**example**](https://logs.modmail.tk/example).
+When a member sends a direct message to the bot, Modmail will create a channel or "thread" within an isolated category. All further DM messages will automatically relay to that channel, for any available staff can respond within the channel.
+
+All threads are logged and you can view previous threads through their corresponding log link. Here is an [**example**](https://logs.modmail.tk/example).
## Features
@@ -65,46 +62,51 @@ All threads are logged and you can view previous threads through the correspondi
* Interface elements (color, responses, reactions, etc).
* Snippets and *command aliases*.
* Minimum duration for accounts to be created before allowed to contact Modmail (`account_age`).
- * Minimum duration for member to be in the guild allowed to contact Modmail (`guild_age`).
+ * Minimum duration for members to be in the guild before allowed to contact Modmail (`guild_age`).
+
* **Advanced Logging Functionality:**
- * When you close a thread, a [log link](https://logs.modmail.tk/example) is generated and posted to your log channel.
+ * When you close a thread, Modmail will generate a [log link](https://logs.modmail.tk/example) and post it to your log channel.
* Native Discord dark-mode feel.
* Markdown/formatting support.
* Login via Discord to protect your logs ([premium Patreon feature](https://patreon.com/kyber)).
* See past logs of a user with `?logs`.
* Searchable by text queries using `?logs search`.
+
* **Robust implementation:**
- * Scheduled tasks in human time, e.g. `?close in 2 hours silently`.
- * Editing and deleting messages are synced on all channels.
- * Support for the full range of message content (multiple images, files).
+ * Schedule tasks in human time, e.g. `?close in 2 hours silently`.
+ * Editing and deleting messages are synced.
+ * Support for the diverse range of message contents (multiple images, files).
* Paginated commands interfaces via reactions.
-
+
This list is ever-growing thanks to active development and our exceptional contributors. See a full list of documented commands by using the `?help` command.
## Installation
-<<<<<<< HEAD
+Where is the Modmail bot invite link? Unfortunately, due to how this bot functions, it cannot be invited. This is to ensure the individuality to your server and grant you full control over your bot and data. Nonetheless, you can easily obtain a free copy of Modmail for your server by following one of the methods listed below (roughly takes 15 minutes of your time):
+
### Heroku
-This bot can be hosted on Heroku.
+This bot can be hosted on Heroku.
-Installation via Heroku is possible with only your web browser.
-The [**installation guide**](https://github.com/kyb3r/modmail/wiki/Installation) will guide you through the entire installation process. If you run into any problems, join the [development server](https://discord.gg/etJNHCQ) for help and support.
+Installation via Heroku is possible with your web browser alone.
+The [**installation guide**](https://github.com/kyb3r/modmail/wiki/Installation) (which includes a video tutorial!) will guide you through the entire installation process. If you run into any problems, join our [Modmail Discord Server](https://discord.gg/etJNHCQ) for help and support.
-You can also set up auto-update. To do this:
+To configure automatic updates:
+ - Login to [GitHub](https://github.com/) and verify your account.
- [Fork the repo](https://github.com/kyb3r/modmail/fork).
- - [Install the Pull app for your fork](https://github.com/apps/pull).
- - Then go to the Deploy tab in your Heroku account, select GitHub and connect your fork.
+ - Install the [Pull app](https://github.com/apps/pull) for your fork.
+ - Then go to the Deploy tab in your [Heroku account](https://dashboard.heroku.com/apps) of your bot app, select GitHub and connect your fork (usually by typing "Modmail").
- Turn on auto-deploy for the `master` branch.
### Hosting for patrons
-If you don't want to go through the trouble of setting up your own bot, and want to support this project as well, we offer installation, hosting and maintenance for Modmail bots for [**Patrons**](https://patreon.com/kyber). Join the support server for more info!
+If you don't want to go through the trouble of setting up your very own Modmail bot, and/or want to support this project, we offer the all inclusive installation, hosting and maintenance of your Modmail with [**Patron**](https://patreon.com/kyber). Join our [Modmail Discord Server](https://discord.gg/etJNHCQ) for more info!
### Locally
-Installation locally for development reasons or otherwise is as follows, you will need [`python 3.7`](https://www.python.org/downloads/).
-Follow the [**installation guide**](https://github.com/kyb3r/modmail/wiki/Installation) and disregard deploying Heroku apps. If you run into any problems, join the [development server](https://discord.gg/etJNHCQ) for help and support.
+Local hosting of Modmail is also possible, first you will need [`python 3.7`](https://www.python.org/downloads/).
+
+Follow the [**installation guide**](https://github.com/kyb3r/modmail/wiki/Installation) and disregard deploying the Heroku bot application. If you run into any problems, join our [Modmail Discord Server](https://discord.gg/etJNHCQ) for help and support.
Clone the repo:
@@ -119,9 +121,9 @@ Install dependencies:
$ pipenv install
```
-Rename the `.env.example` to `.env` and fill out the fields. If `.env.example` is hidden, create a text file named `.env` and copy the contents of [`.env.example`](https://raw.githubusercontent.com/kyb3r/modmail/master/.env.example) then modify the values.
+Rename the `.env.example` to `.env` and fill out the fields. If `.env.example` is nonexistent (hidden), create a text file named `.env` and copy the contents of [`.env.example`](https://raw.githubusercontent.com/kyb3r/modmail/master/.env.example) then modify the values.
-Finally, run the bot.
+Finally, start Modmail.
```console
$ pipenv run bot
@@ -135,14 +137,16 @@ Special thanks to our sponsors for supporting the project.
-Become a [sponsor](https://patreon.com/kyber).
+Become a sponsor on [Patreon](https://patreon.com/kyber).
## Plugins
-Modmail supports the use of third-party plugins to extend or add functionality to the bot. This allows the introduction of niche features as well as anything else outside of the scope of the core functionality of Modmail. A list of third party plugins can be found using the `plugins registry` command. To develop your own, check out the [documentation](https://github.com/kyb3r/modmail/wiki/Plugins) for plugins.
+Modmail supports the use of third-party plugins to extend or add functionalities to the bot. This allows niche features as well as anything else outside of the scope of the core functionality of Modmail. A list of third-party plugins can be found using the `plugins registry` command. To develop your own, check out the [plugins documentation](https://github.com/kyb3r/modmail/wiki/Plugins).
+
+Plugins requests and support is available in our (Modmail Plugins Server)[https://discord.gg/4JE4XSW].
## Contributing
-Contributions to Modmail are always welcome, whether it be improvements to the documentation or new functionality, please feel free to make the change. Check out our contribution [guidelines](https://github.com/kyb3r/modmail/blob/master/CONTRIBUTING.md) before you get started.
+Contributions to Modmail are always welcome, whether it be improvements to the documentation or new functionality, please feel free to make the change. Check out our contribution [guidelines](https://github.com/kyb3r/modmail/blob/master/CONTRIBUTING.md) before you get started.
-This bot is free for everyone and always will be. If you like this project and would like to show your appreciation, here's the link to our **[Patreon Page](https://www.patreon.com/kyber)**!
\ No newline at end of file
+If you like this project and would like to show your appreciation, support us on **[Patreon](https://www.patreon.com/kyber)**!
\ No newline at end of file
diff --git a/core/paginator.py b/core/paginator.py
index c2bed558ed..835337dbed 100644
--- a/core/paginator.py
+++ b/core/paginator.py
@@ -94,7 +94,7 @@ async def show_page(self, index: int) -> None:
page = self.pages[index]
if self.running:
- return await self._show_page(page)
+ await self._show_page(page)
else:
await self.create_base(page)
diff --git a/core/utils.py b/core/utils.py
index 89f9b55f81..585c0fba3d 100644
--- a/core/utils.py
+++ b/core/utils.py
@@ -2,7 +2,7 @@
import shlex
import typing
from difflib import get_close_matches
-from distutils.util import strtobool as _stb
+from distutils.util import strtobool as _stb # pylint: disable=import-error
from itertools import takewhile
from urllib import parse
From 6a7a60410b09650ae0c1c19e17208adf7e94210c Mon Sep 17 00:00:00 2001
From: Taaku18 <45324516+Taaku18@users.noreply.github.com>
Date: Thu, 8 Aug 2019 10:28:54 -0700
Subject: [PATCH 0365/1221] Fix a blocked bug
---
cogs/modmail.py | 9 ++++-----
1 file changed, 4 insertions(+), 5 deletions(-)
diff --git a/cogs/modmail.py b/cogs/modmail.py
index 486ddb303b..7f2bb4ce56 100644
--- a/cogs/modmail.py
+++ b/cogs/modmail.py
@@ -906,22 +906,21 @@ async def blocked(self, ctx):
pass
if users:
- embed = embeds[-1]
+ embed = embeds[0]
for mention, reason in users:
line = mention + f" - `{reason or 'No reason provided'}`\n"
if len(embed.description) + len(line) > 2048:
- embeds.append(
- discord.Embed(
+ embed = discord.Embed(
title="Blocked Users (Continued)",
color=self.bot.main_color,
description=line,
)
- )
+ embeds.append(embed)
else:
embed.description += line
else:
- embeds[-1].description = "Currently there are no blocked users."
+ embeds[0].description = "Currently there are no blocked users."
session = EmbedPaginatorSession(ctx, *embeds)
await session.run()
From b85faf2a7d43b3f3cb826dc86941c63bf175f5dd Mon Sep 17 00:00:00 2001
From: Taku 3 Animals <45324516+Taaku18@users.noreply.github.com>
Date: Fri, 9 Aug 2019 08:15:19 -0700
Subject: [PATCH 0366/1221] Fixed MD
---
README.md | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/README.md b/README.md
index e9aabe9a76..b43079e850 100644
--- a/README.md
+++ b/README.md
@@ -143,10 +143,10 @@ Become a sponsor on [Patreon](https://patreon.com/kyber).
Modmail supports the use of third-party plugins to extend or add functionalities to the bot. This allows niche features as well as anything else outside of the scope of the core functionality of Modmail. A list of third-party plugins can be found using the `plugins registry` command. To develop your own, check out the [plugins documentation](https://github.com/kyb3r/modmail/wiki/Plugins).
-Plugins requests and support is available in our (Modmail Plugins Server)[https://discord.gg/4JE4XSW].
+Plugins requests and support is available in our [Modmail Plugins Server](https://discord.gg/4JE4XSW).
## Contributing
Contributions to Modmail are always welcome, whether it be improvements to the documentation or new functionality, please feel free to make the change. Check out our contribution [guidelines](https://github.com/kyb3r/modmail/blob/master/CONTRIBUTING.md) before you get started.
-If you like this project and would like to show your appreciation, support us on **[Patreon](https://www.patreon.com/kyber)**!
\ No newline at end of file
+If you like this project and would like to show your appreciation, support us on **[Patreon](https://www.patreon.com/kyber)**!
From a99a0917cf5a8f4c3b0cdba9787f34cfc67ed65d Mon Sep 17 00:00:00 2001
From: Taku 3 Animals <45324516+Taaku18@users.noreply.github.com>
Date: Fri, 9 Aug 2019 08:15:19 -0700
Subject: [PATCH 0367/1221] Fix open issue on Windows
---
README.md | 4 ++--
core/config.py | 2 +-
2 files changed, 3 insertions(+), 3 deletions(-)
diff --git a/README.md b/README.md
index e9aabe9a76..b43079e850 100644
--- a/README.md
+++ b/README.md
@@ -143,10 +143,10 @@ Become a sponsor on [Patreon](https://patreon.com/kyber).
Modmail supports the use of third-party plugins to extend or add functionalities to the bot. This allows niche features as well as anything else outside of the scope of the core functionality of Modmail. A list of third-party plugins can be found using the `plugins registry` command. To develop your own, check out the [plugins documentation](https://github.com/kyb3r/modmail/wiki/Plugins).
-Plugins requests and support is available in our (Modmail Plugins Server)[https://discord.gg/4JE4XSW].
+Plugins requests and support is available in our [Modmail Plugins Server](https://discord.gg/4JE4XSW).
## Contributing
Contributions to Modmail are always welcome, whether it be improvements to the documentation or new functionality, please feel free to make the change. Check out our contribution [guidelines](https://github.com/kyb3r/modmail/blob/master/CONTRIBUTING.md) before you get started.
-If you like this project and would like to show your appreciation, support us on **[Patreon](https://www.patreon.com/kyber)**!
\ No newline at end of file
+If you like this project and would like to show your appreciation, support us on **[Patreon](https://www.patreon.com/kyber)**!
diff --git a/core/config.py b/core/config.py
index e7776cb39d..b682f32e04 100644
--- a/core/config.py
+++ b/core/config.py
@@ -153,7 +153,7 @@ def populate_cache(self) -> dict:
config_help_json = os.path.join(
os.path.dirname(os.path.abspath(__file__)), "config_help.json"
)
- with open(config_help_json, "r") as f:
+ with open(config_help_json, "r", encoding="utf-8") as f:
self.config_help = dict(sorted(json.load(f).items()))
return self._cache
From 2d89a854cdbb59dd3d02a77d69350ba7da8b50f5 Mon Sep 17 00:00:00 2001
From: Taku 3 Animals <45324516+Taaku18@users.noreply.github.com>
Date: Fri, 9 Aug 2019 08:15:19 -0700
Subject: [PATCH 0368/1221] Bump version with fixes for windows issue
---
CHANGELOG.md | 5 +++++
README.md | 4 ++--
bot.py | 2 +-
core/config.py | 2 +-
4 files changed, 9 insertions(+), 4 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 22dc37c980..fca34438db 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,6 +5,11 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
This project mostly adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html);
however, insignificant breaking changes does not guarantee a major version bump, see the reasoning [here](https://github.com/kyb3r/modmail/issues/319).
+# v3.1.1
+
+### Fixed
+
+- An issue when reading `config_help.json` for Windows users due to an encoding problem.
# v3.1.0
diff --git a/README.md b/README.md
index e9aabe9a76..b43079e850 100644
--- a/README.md
+++ b/README.md
@@ -143,10 +143,10 @@ Become a sponsor on [Patreon](https://patreon.com/kyber).
Modmail supports the use of third-party plugins to extend or add functionalities to the bot. This allows niche features as well as anything else outside of the scope of the core functionality of Modmail. A list of third-party plugins can be found using the `plugins registry` command. To develop your own, check out the [plugins documentation](https://github.com/kyb3r/modmail/wiki/Plugins).
-Plugins requests and support is available in our (Modmail Plugins Server)[https://discord.gg/4JE4XSW].
+Plugins requests and support is available in our [Modmail Plugins Server](https://discord.gg/4JE4XSW).
## Contributing
Contributions to Modmail are always welcome, whether it be improvements to the documentation or new functionality, please feel free to make the change. Check out our contribution [guidelines](https://github.com/kyb3r/modmail/blob/master/CONTRIBUTING.md) before you get started.
-If you like this project and would like to show your appreciation, support us on **[Patreon](https://www.patreon.com/kyber)**!
\ No newline at end of file
+If you like this project and would like to show your appreciation, support us on **[Patreon](https://www.patreon.com/kyber)**!
diff --git a/bot.py b/bot.py
index 46f1081e3b..50b8351474 100644
--- a/bot.py
+++ b/bot.py
@@ -1,4 +1,4 @@
-__version__ = "3.1.0"
+__version__ = "3.1.1"
import asyncio
import logging
diff --git a/core/config.py b/core/config.py
index e7776cb39d..b682f32e04 100644
--- a/core/config.py
+++ b/core/config.py
@@ -153,7 +153,7 @@ def populate_cache(self) -> dict:
config_help_json = os.path.join(
os.path.dirname(os.path.abspath(__file__)), "config_help.json"
)
- with open(config_help_json, "r") as f:
+ with open(config_help_json, "r", encoding="utf-8") as f:
self.config_help = dict(sorted(json.load(f).items()))
return self._cache
From e18af492fdaa34d09e3c93b1cf7369753940d899 Mon Sep 17 00:00:00 2001
From: Matrix <49132750+matrix2113@users.noreply.github.com>
Date: Mon, 12 Aug 2019 19:30:46 -0400
Subject: [PATCH 0369/1221] Removed "update"
---
app.json | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/app.json b/app.json
index e4f5032e81..76fe320a30 100644
--- a/app.json
+++ b/app.json
@@ -12,7 +12,7 @@
"required": true
},
"OWNERS": {
- "description": "Comma separated user IDs of people that are allowed to use owner only commands. (eval and update).",
+ "description": "Comma separated user IDs of people that are allowed to use owner only commands. (eval).",
"required": true
},
"MONGO_URI": {
From 9d4fb27ef296e38318e37b1a3b3a12a747612bd3 Mon Sep 17 00:00:00 2001
From: Taaku18 <45324516+Taaku18@users.noreply.github.com>
Date: Sat, 17 Aug 2019 00:19:40 -1000
Subject: [PATCH 0370/1221] v3.2.0 dev
---
CHANGELOG.md | 18 ++++
README.md | 6 +-
bot.py | 51 ++++++++--
cogs/modmail.py | 8 +-
cogs/utility.py | 230 +++++++++++++++++++++++++++++++++++-------
core/changelog.py | 60 ++++++-----
core/checks.py | 58 ++++-------
core/config.py | 1 +
core/config_help.json | 8 +-
core/utils.py | 16 +--
10 files changed, 331 insertions(+), 125 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index fca34438db..09998321ce 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,10 +1,28 @@
# Changelog
+
All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
This project mostly adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html);
however, insignificant breaking changes does not guarantee a major version bump, see the reasoning [here](https://github.com/kyb3r/modmail/issues/319).
+# v3.2.0
+
+### Added
+
+- Ability to change permission levels of individual commands.
+ - See `?permissions override` for more information.
+
+### Fixed
+
+- `?help `, will return `Perhaps you meant: `, now its fixed.
+ - For example, `?help add` used to return `Perhaps you meant: add`, now it wouldn't do this.
+- Aliases and Permissions command names are always saved lowercase now.
+
+### Internal
+
+- Use regex to parse Changes, Added, Fixed, etc and description.
+
# v3.1.1
### Fixed
diff --git a/README.md b/README.md
index b43079e850..46e41697e9 100644
--- a/README.md
+++ b/README.md
@@ -82,7 +82,9 @@ This list is ever-growing thanks to active development and our exceptional contr
## Installation
-Where is the Modmail bot invite link? Unfortunately, due to how this bot functions, it cannot be invited. This is to ensure the individuality to your server and grant you full control over your bot and data. Nonetheless, you can easily obtain a free copy of Modmail for your server by following one of the methods listed below (roughly takes 15 minutes of your time):
+Where can I find the Modmail bot invite link?
+
+Unfortunately, due to how this bot functions, it cannot be invited. This is to ensure the individuality to your server and grant you full control over your bot and data. Nonetheless, you can easily obtain a free copy of Modmail for your server by following one of the methods listed below (roughly takes 15 minutes of your time)...
### Heroku
@@ -104,7 +106,7 @@ If you don't want to go through the trouble of setting up your very own Modmail
### Locally
-Local hosting of Modmail is also possible, first you will need [`python 3.7`](https://www.python.org/downloads/).
+Local hosting of Modmail is also possible, first you will need [`Python 3.7`](https://www.python.org/downloads/).
Follow the [**installation guide**](https://github.com/kyb3r/modmail/wiki/Installation) and disregard deploying the Heroku bot application. If you run into any problems, join our [Modmail Discord Server](https://discord.gg/etJNHCQ) for help and support.
diff --git a/bot.py b/bot.py
index 50b8351474..3bb205c991 100644
--- a/bot.py
+++ b/bot.py
@@ -1,4 +1,4 @@
-__version__ = "3.1.1"
+__version__ = "3.2.0"
import asyncio
import logging
@@ -382,6 +382,34 @@ def recipient_color(self) -> int:
def main_color(self) -> int:
return self._parse_color("main_color")
+ def command_perm(self, command_name: str) -> PermissionLevel:
+ level = self.config["override_command_level"].get(command_name)
+ if level is not None:
+ try:
+ return PermissionLevel[level.upper()]
+ except KeyError:
+ logger.warning(
+ "Invalid override_command_level for command %s.", command_name
+ )
+ self.config["override_command_level"].pop(command_name)
+
+ command = self.get_command(command_name)
+ if command is None:
+ logger.debug("Command %s not found.", command_name)
+ return PermissionLevel.INVALID
+ level = next(
+ (
+ check.permission_level
+ for check in command.checks
+ if hasattr(check, "permission_level")
+ ),
+ None,
+ )
+ if level is None:
+ logger.debug("Command %s does not have a permission level.", command_name)
+ return PermissionLevel.INVALID
+ return level
+
async def on_connect(self):
logger.line()
try:
@@ -1039,12 +1067,23 @@ async def on_command_error(self, context, exception):
await context.send_help(context.command)
elif isinstance(exception, commands.CheckFailure):
for check in context.command.checks:
- if not await check(context) and hasattr(check, "fail_msg"):
- await context.send(
- embed=discord.Embed(
- color=discord.Color.red(), description=check.fail_msg
+ if not await check(context):
+ if hasattr(check, "fail_msg"):
+ await context.send(
+ embed=discord.Embed(
+ color=discord.Color.red(), description=check.fail_msg
+ )
+ )
+ if hasattr(check, 'permission_level'):
+ corrected_permission_level = self.command_perm(
+ context.command.qualified_name
+ )
+ logger.warning(
+ "User %s does not have permission to use this command: `%s` (%s).",
+ context.author.name,
+ context.command.qualified_name,
+ corrected_permission_level.name,
)
- )
logger.warning("CheckFailure: %s", exception)
else:
logger.error("Unexpected exception:", exc_info=exception)
diff --git a/cogs/modmail.py b/cogs/modmail.py
index 7f2bb4ce56..e3f57ffb7d 100644
--- a/cogs/modmail.py
+++ b/cogs/modmail.py
@@ -912,10 +912,10 @@ async def blocked(self, ctx):
line = mention + f" - `{reason or 'No reason provided'}`\n"
if len(embed.description) + len(line) > 2048:
embed = discord.Embed(
- title="Blocked Users (Continued)",
- color=self.bot.main_color,
- description=line,
- )
+ title="Blocked Users (Continued)",
+ color=self.bot.main_color,
+ description=line,
+ )
embeds.append(embed)
else:
embed.description += line
diff --git a/cogs/utility.py b/cogs/utility.py
index 188da898bf..00963b88b2 100644
--- a/cogs/utility.py
+++ b/cogs/utility.py
@@ -41,9 +41,9 @@ async def format_cog_help(self, cog, *, no_cog=False):
for cmd in await self.filter_commands(
cog.get_commands() if not no_cog else cog,
sort=True,
- key=utils.get_perm_level,
+ key=lambda c: bot.command_perm(c.qualified_name),
):
- perm_level = utils.get_perm_level(cmd)
+ perm_level = bot.command_perm(cmd.qualified_name)
if perm_level is PermissionLevel.INVALID:
format_ = f"`{prefix + cmd.qualified_name}` "
else:
@@ -122,11 +122,11 @@ async def send_cog_help(self, cog):
async def send_command_help(self, command):
if not await self.filter_commands([command]):
return
- perm_level = utils.get_perm_level(command)
+ perm_level = self.context.bot.command_perm(command.qualified_name)
if perm_level is not PermissionLevel.INVALID:
perm_level = f"{perm_level.name} [{perm_level}]"
else:
- perm_level = ""
+ perm_level = "NONE"
embed = Embed(
title=f"`{self.get_command_signature(command)}`",
@@ -140,11 +140,11 @@ async def send_group_help(self, group):
if not await self.filter_commands([group]):
return
- perm_level = utils.get_perm_level(group)
+ perm_level = self.context.bot.command_perm(group.qualified_name)
if perm_level is not PermissionLevel.INVALID:
perm_level = f"{perm_level.name} [{perm_level}]"
else:
- perm_level = ""
+ perm_level = "NONE"
embed = Embed(
title=f"`{self.get_command_signature(group)}`",
@@ -153,7 +153,7 @@ async def send_group_help(self, group):
)
if perm_level:
- embed.add_field(name="Permission level", value=perm_level, inline=False)
+ embed.add_field(name="Permission Level", value=perm_level, inline=False)
format_ = ""
length = len(group.commands)
@@ -169,7 +169,7 @@ async def send_group_help(self, group):
branch = "├─"
format_ += f"`{branch} {command.name}` - {command.short_doc}\n"
- embed.add_field(name="Sub Commands", value=format_[:1024], inline=False)
+ embed.add_field(name="Sub Command(s)", value=format_[:1024], inline=False)
embed.set_footer(
text=f'Type "{self.clean_prefix}{self.command_attrs["name"]} command" '
"for more info on a command."
@@ -218,7 +218,7 @@ async def send_error_message(self, error):
for cmd in self.context.bot.walk_commands():
if not cmd.hidden:
- choices.add(cmd.name)
+ choices.add(cmd.qualified_name)
closest = get_close_matches(command, choices)
if closest:
@@ -244,7 +244,7 @@ def __init__(self, bot):
verify_checks=False,
command_attrs={
"help": "Shows this help message.",
- "checks": [checks.has_permissions_help(PermissionLevel.REGULAR)],
+ "checks": [checks.has_permissions_predicate(PermissionLevel.REGULAR)],
},
)
self.bot.help_command.cog = self
@@ -271,14 +271,21 @@ async def changelog(self, ctx, version: str.lower = ""):
)
)
+ paginator = EmbedPaginatorSession(ctx, *changelog.embeds)
try:
- paginator = EmbedPaginatorSession(ctx, *changelog.embeds)
paginator.current = index
await paginator.run()
except asyncio.CancelledError:
pass
except Exception:
- await ctx.send(changelog.CHANGELOG_URL)
+ try:
+ await paginator.close()
+ except Exception:
+ pass
+ logger.warning("Failed to display changelog.", exc_info=True)
+ await ctx.send(
+ f"View the changelog here: {changelog.CHANGELOG_URL}#v{version[::2]}"
+ )
@commands.command(aliases=["bot", "info"])
@checks.has_permissions(PermissionLevel.REGULAR)
@@ -640,14 +647,17 @@ async def set_presence(
async def loop_presence(self):
"""Set presence to the configured value every 45 minutes."""
presence = await self.set_presence()
- logger.line()
- logger.info(presence["activity"][1])
- logger.info(presence["status"][1])
+ logger.debug(f'{presence["activity"][1]} {presence["status"][1]}')
@loop_presence.before_loop
async def before_loop_presence(self):
logger.info("Starting metadata loop.")
await self.bot.wait_for_connected()
+ logger.line()
+ presence = await self.set_presence()
+ logger.info(presence["activity"][1])
+ logger.info(presence["status"][1])
+ await asyncio.sleep(2700)
@commands.command()
@checks.has_permissions(PermissionLevel.ADMINISTRATOR)
@@ -1066,7 +1076,7 @@ async def alias_add(self, ctx, name: str.lower, *, value):
return await ctx.send(embed=embed)
if len(values) == 1:
- linked_command = values[0].split()[0]
+ linked_command = values[0].split()[0].lower()
if not self.bot.get_command(linked_command):
embed = Embed(
title="Error",
@@ -1147,7 +1157,7 @@ async def alias_edit(self, ctx, name: str.lower, *, value):
return await ctx.send(embed=embed)
if len(values) == 1:
- linked_command = values[0].split()[0]
+ linked_command = values[0].split()[0].lower()
if not self.bot.get_command(linked_command):
embed = Embed(
title="Error",
@@ -1203,6 +1213,9 @@ async def permissions(self, ctx):
By default, owner is set to the absolute bot owner and regular is `@everyone`.
+ To set permissions, see `{prefix}help permissions add`; and to change permission level for specific
+ commands see `{prefix}help permissions override`.
+
Note: You will still have to manually give/take permission to the Modmail
category to users/roles.
"""
@@ -1233,9 +1246,64 @@ def _parse_level(name):
"4": PermissionLevel.ADMINISTRATOR,
"5": PermissionLevel.OWNER,
}
- return transform.get(name)
+ return transform.get(name, PermissionLevel.INVALID)
+
+ @permissions.command(name="override")
+ @checks.has_permissions(PermissionLevel.OWNER)
+ async def permissions_override(
+ self, ctx, command_name: str.lower, *, level_name: str
+ ):
+ """
+ Change a permission level for a specific command.
+
+ Examples:
+ - `{prefix}perms override reply administrator`
+ - `{prefix}perms override "plugin enabled" moderator`
+
+ To undo a permission override, see `{prefix}help permissions remove`.
+
+ Example:
+ - `{prefix}perms remove override reply`
+ - `{prefix}perms remove override plugin enabled`
+
+ You can retrieve a single or all command level override(s), see`{prefix}help permissions get`.
+ """
+
+ command = self.bot.get_command(command_name)
+ if command is None:
+ embed = Embed(
+ title="Error",
+ color=Color.red(),
+ description=f"The referenced command does not exist: `{command_name}`.",
+ )
+ return await ctx.send(embed=embed)
+
+ level = self._parse_level(level_name)
+ if level is PermissionLevel.INVALID:
+ embed = Embed(
+ title="Error",
+ color=Color.red(),
+ description=f"The referenced level does not exist: `{level_name}`.",
+ )
+ else:
+ logger.info(
+ "Updated command permission level for `%s` to `%s`.",
+ command.qualified_name,
+ level.name,
+ )
+ self.bot.config["override_command_level"][
+ command.qualified_name
+ ] = level.name
+ await self.bot.config.update()
+ embed = Embed(
+ title="Success",
+ color=self.bot.main_color,
+ description="Successfully set command permission level for "
+ f"`{command.qualified_name}` to `{level.name}`.",
+ )
+ return await ctx.send(embed=embed)
- @permissions.command(name="add", usage="[command/level] [name] [user_or_role]")
+ @permissions.command(name="add", usage="[command/level] [name] [user/role]")
@checks.has_permissions(PermissionLevel.OWNER)
async def permissions_add(
self,
@@ -1265,11 +1333,12 @@ async def permissions_add(
command = level = None
if type_ == "command":
- command = self.bot.get_command(name.lower())
+ name = name.lower()
+ command = self.bot.get_command(name)
check = command is not None
else:
level = self._parse_level(name)
- check = level is not None
+ check = level is not PermissionLevel.INVALID
if not check:
embed = Embed(
@@ -1281,8 +1350,8 @@ async def permissions_add(
value = self._verify_user_or_role(user_or_role)
if type_ == "command":
- await self.bot.update_perms(command.qualified_name, value)
name = command.qualified_name
+ await self.bot.update_perms(name, value)
else:
await self.bot.update_perms(level, value)
name = level.name
@@ -1309,7 +1378,7 @@ async def permissions_add(
@permissions.command(
name="remove",
aliases=["del", "delete", "revoke"],
- usage="[command/level] [name] [user_or_role]",
+ usage="[command/level] [name] [user/role] or [override] [command name]",
)
@checks.has_permissions(PermissionLevel.OWNER)
async def permissions_remove(
@@ -1318,10 +1387,10 @@ async def permissions_remove(
type_: str.lower,
name: str,
*,
- user_or_role: Union[Role, utils.User, str],
+ user_or_role: Union[Role, utils.User, str] = None,
):
"""
- Remove permission to use a command or permission level.
+ Remove permission to use a command, permission level, or command level override.
For sub commands, wrap the complete command name with quotes.
To find a list of permission levels, see `{prefix}help perms`.
@@ -1331,22 +1400,54 @@ async def permissions_remove(
- `{prefix}perms remove command reply @user`
- `{prefix}perms remove command "plugin enabled" @role`
- `{prefix}perms remove command help 984301093849028`
+ - `{prefix}perms remove override block`
+ - `{prefix}perms remove override "snippet add"`
Do not ping `@everyone` for granting permission to everyone, use "everyone" or "all" instead,
"""
- if type_ not in {"command", "level"}:
+ if type_ not in {"command", "level", "override"} or (
+ type_ != "override" and user_or_role is None
+ ):
return await ctx.send_help(ctx.command)
+ if type_ == "override":
+ extension = ctx.kwargs["user_or_role"]
+ if extension is not None:
+ name += f" {extension}"
+ name = name.lower()
+ name = getattr(self.bot.get_command(name), "qualified_name", name)
+ level = self.bot.config["override_command_level"].get(name)
+ if level is None:
+ perm = self.bot.command_perm(name)
+ embed = Embed(
+ title="Error",
+ color=Color.red(),
+ description=f"The command permission level was never overridden: `{name}`, "
+ f"current permission level is {perm.name}.",
+ )
+ else:
+ logger.info("Restored command permission level for `%s`.", name)
+ self.bot.config["override_command_level"].pop(name)
+ await self.bot.config.update()
+ perm = self.bot.command_perm(name)
+ embed = Embed(
+ title="Success",
+ color=self.bot.main_color,
+ description=f"Command permission level for `{name}` was successfully restored to {perm.name}.",
+ )
+ return await ctx.send(embed=embed)
+
level = None
if type_ == "command":
- name = getattr(self.bot.get_command(name.lower()), "qualified_name", name)
+ name = name.lower()
+ name = getattr(self.bot.get_command(name), "qualified_name", name)
else:
level = self._parse_level(name)
- if level is None:
+ if level is PermissionLevel.INVALID:
embed = Embed(
title="Error",
color=Color.red(),
- description=f"The referenced {type_} does not exist: `{name}`.",
+ description=f"The referenced level does not exist: `{name}`.",
)
return await ctx.send(embed=embed)
name = level.name
@@ -1423,7 +1524,7 @@ def _get_perm(self, ctx, name, type_):
)
return embed
- @permissions.command(name="get", usage="[@user] or [command/level] [name]")
+ @permissions.command(name="get", usage="[@user] or [command/level/override] [name]")
@checks.has_permissions(PermissionLevel.OWNER)
async def permissions_get(
self, ctx, user_or_role: Union[Role, utils.User, str], *, name: str = None
@@ -1446,10 +1547,16 @@ async def permissions_get(
- `{prefix}perms get command plugin remove`
- `{prefix}perms get level SUPPORTER`
+ To view command level overrides:
+
+ Examples:
+ - `{prefix}perms get override block`
+ - `{prefix}perms get override permissions add`
+
Do not ping `@everyone` for granting permission to everyone, use "everyone" or "all" instead,
"""
- if name is None and user_or_role not in {"command", "level"}:
+ if name is None and user_or_role not in {"command", "level", "override"}:
value = self._verify_user_or_role(user_or_role)
cmds = []
@@ -1497,6 +1604,59 @@ async def permissions_get(
),
]
else:
+ user_or_role = (user_or_role or "").lower()
+ if user_or_role == "override":
+ if name is None:
+ done = set()
+
+ overrides = {}
+ for command in self.bot.walk_commands():
+ if command not in done:
+ done.add(command)
+ level = self.bot.config["override_command_level"].get(command.qualified_name)
+ if level is not None:
+ overrides[command.qualified_name] = level
+
+ embeds = []
+ if not overrides:
+ embeds.append(Embed(
+ title="Permission Overrides",
+ description="You don't have any command level overrides at the moment.",
+ color=Color.red()
+ ))
+ else:
+ for items in zip_longest(*(iter(sorted(overrides.items())),) * 15):
+ description = "\n".join(
+ ": ".join((f"`{name}`", level))
+ for name, level in takewhile(lambda x: x is not None, items)
+ )
+ embed = Embed(color=self.bot.main_color, description=description)
+ embed.set_author(name="Permission Overrides", icon_url=ctx.guild.icon_url)
+ embeds.append(embed)
+
+ session = EmbedPaginatorSession(ctx, *embeds)
+ return await session.run()
+
+ name = name.lower()
+ name = getattr(self.bot.get_command(name), "qualified_name", name)
+ level = self.bot.config["override_command_level"].get(name)
+ perm = self.bot.command_perm(name)
+ if level is None:
+ embed = Embed(
+ title="Error",
+ color=Color.red(),
+ description=f"The command permission level was never overridden: `{name}`, "
+ f"current permission level is {perm.name}.",
+ )
+ else:
+ embed = Embed(
+ title="Success",
+ color=self.bot.main_color,
+ description=f'Permission override for command "{name}" is "{perm.name}".',
+ )
+
+ return await ctx.send(embed=embed)
+
if user_or_role not in {"command", "level"}:
return await ctx.send_help(ctx.command)
embeds = []
@@ -1504,11 +1664,12 @@ async def permissions_get(
name = name.strip('"')
command = level = None
if user_or_role == "command":
- command = self.bot.get_command(name.lower())
+ name = name.lower()
+ command = self.bot.get_command(name)
check = command is not None
else:
level = self._parse_level(name)
- check = level is not None
+ check = level is not PermissionLevel.INVALID
if not check:
embed = Embed(
@@ -1543,7 +1704,8 @@ async def permissions_get(
@commands.group(invoke_without_command=True)
@checks.has_permissions(PermissionLevel.OWNER)
async def oauth(self, ctx):
- """Commands relating to logviewer oauth2 login authentication.
+ """
+ Commands relating to logviewer oauth2 login authentication.
This functionality on your logviewer site is a [**Patron**](https://patreon.com/kyber) only feature.
"""
diff --git a/core/changelog.py b/core/changelog.py
index 386baf176f..3bf4ad865b 100644
--- a/core/changelog.py
+++ b/core/changelog.py
@@ -1,9 +1,10 @@
import logging
import re
-from collections import defaultdict
-from typing import List, Union
+from typing import List
-from discord import Embed, Color
+from discord import Embed
+
+from core.utils import truncate
logger = logging.getLogger("Modmail")
@@ -27,22 +28,29 @@ class Version:
The Modmail bot.
version : str
The version string (ie. "v2.12.0").
- lines : List[str]
+ lines : str
A list of lines of changelog messages for this version.
- fields : defaultdict[str, str]
+ fields : Dict[str, str]
A dict of fields separated by "Fixed", "Changed", etc sections.
description : str
General description of the version.
+
+ Class Attributes
+ ----------------
+ ACTION_REGEX : str
+ The regex used to parse the actions.
+ DESCRIPTION_REGEX: str
+ The regex used to parse the description.
"""
- def __init__(self, bot, version: str, lines: Union[List[str], str]):
+ ACTION_REGEX = r"###\s*(.+?)\s*\n(.*?)(?=###\s*.+?|$)"
+ DESCRIPTION_REGEX = r"^(.*?)(?=###\s*.+?|$)"
+
+ def __init__(self, bot, version: str, lines: str):
self.bot = bot
self.version = version.lstrip("vV")
- if isinstance(lines, list):
- self.lines = lines
- else:
- self.lines = [x for x in lines.splitlines() if x]
- self.fields = defaultdict(str)
+ self.lines = lines.strip()
+ self.fields = {}
self.description = ""
self.parse()
@@ -53,26 +61,32 @@ def parse(self) -> None:
"""
Parse the lines and split them into `description` and `fields`.
"""
- curr_action = None
+ self.description = re.match(self.DESCRIPTION_REGEX, self.lines, re.DOTALL)
+ self.description = (
+ self.description.group(1).strip() if self.description is not None else ""
+ )
- for line in self.lines:
- if line.startswith("### "):
- curr_action = line[4:]
- elif curr_action is None:
- self.description += line + "\n"
- else:
- self.fields[curr_action] += line + "\n"
+ matches = re.finditer(self.ACTION_REGEX, self.lines, re.DOTALL)
+ for m in matches:
+ try:
+ self.fields[m.group(1).strip()] = m.group(2).strip()
+ except AttributeError:
+ logger.error(
+ "Something went wrong when parsing the changelog for version %s.",
+ self.version,
+ exc_info=True,
+ )
@property
def url(self) -> str:
- return Changelog.CHANGELOG_URL + "#v" + self.version.replace(".", "")
+ return f"{Changelog.CHANGELOG_URL}#v{self.version[::2]}"
@property
def embed(self) -> Embed:
"""
Embed: the formatted `Embed` of this `Version`.
"""
- embed = Embed(color=Color.green(), description=self.description)
+ embed = Embed(color=self.bot.main_color, description=self.description)
embed.set_author(
name=f"v{self.version} - Changelog",
icon_url=self.bot.user.avatar_url,
@@ -80,7 +94,7 @@ def embed(self) -> Embed:
)
for name, value in self.fields.items():
- embed.add_field(name=name, value=value)
+ embed.add_field(name=name, value=truncate(value, 1024))
embed.set_footer(text=f"Current version: v{self.bot.version}")
embed.set_thumbnail(url=self.bot.user.avatar_url)
return embed
@@ -121,7 +135,7 @@ class Changelog:
)
CHANGELOG_URL = "https://github.com/kyb3r/modmail/blob/master/CHANGELOG.md"
VERSION_REGEX = re.compile(
- r"# ([vV]\d+\.\d+(?:\.\d+)?)(.*?(?=# (?:[vV]\d+\.\d+(?:\.\d+)?)|$))",
+ r"#\s*([vV]\d+\.\d+(?:\.\d+)?)\s+(.*?)(?=#\s*[vV]\d+\.\d+(?:\.\d+)?|$)",
flags=re.DOTALL,
)
diff --git a/core/checks.py b/core/checks.py
index 17d2b4353b..2e3dbe58d9 100644
--- a/core/checks.py
+++ b/core/checks.py
@@ -7,11 +7,9 @@
logger = logging.getLogger("Modmail")
-def has_permissions_help(permission_level: PermissionLevel = PermissionLevel.REGULAR):
+def has_permissions_predicate(permission_level: PermissionLevel = PermissionLevel.REGULAR):
async def predicate(ctx):
- return await check_permissions(
- ctx, ctx.command.qualified_name, permission_level
- )
+ return await check_permissions(ctx, ctx.command.qualified_name)
predicate.permission_level = permission_level
return predicate
@@ -36,62 +34,46 @@ async def setup(ctx):
print("Success")
"""
- async def predicate(ctx):
- has_perm = await check_permissions(
- ctx, ctx.command.qualified_name, permission_level
- )
-
- if not has_perm and ctx.command.qualified_name != "help":
- logger.error(
- "You do not have permission to use this command: `%s` (%s).",
- str(ctx.command.qualified_name),
- str(permission_level.name),
- )
- return has_perm
+ return commands.check(has_permissions_predicate(permission_level))
- predicate.permission_level = permission_level
- return commands.check(predicate)
-
-async def check_permissions( # pylint: disable=too-many-return-statements
- ctx, command_name, permission_level
-) -> bool:
+async def check_permissions(ctx, command_name) -> bool:
"""Logic for checking permissions for a command for a user"""
if await ctx.bot.is_owner(ctx.author):
- # Direct bot owner (creator) has absolute power over the bot
+ # Bot owner(s) (and creator) has absolute power over the bot
+ return True
+
+ permission_level = ctx.bot.command_perm(command_name)
+
+ if permission_level is PermissionLevel.INVALID:
+ logger.warning("Invalid permission level for command %s.", command_name)
return True
if (
- permission_level != PermissionLevel.OWNER
+ permission_level is not PermissionLevel.OWNER
and ctx.channel.permissions_for(ctx.author).administrator
):
# Administrators have permission to all non-owner commands
+ logger.debug("Allowed due to administrator.")
return True
command_permissions = ctx.bot.config["command_permissions"]
- author_roles = ctx.author.roles
+ checkables = {*ctx.author.roles, ctx.author}
if command_name in command_permissions:
# -1 is for @everyone
- if -1 in command_permissions[command_name]:
- return True
- has_perm_role = any(
- role.id in command_permissions[command_name] for role in author_roles
+ return -1 in command_permissions[command_name] or any(
+ check.id in command_permissions[command_name] for check in checkables
)
- has_perm_id = ctx.author.id in command_permissions[command_name]
- return has_perm_role or has_perm_id
level_permissions = ctx.bot.config["level_permissions"]
for level in PermissionLevel:
if level >= permission_level and level.name in level_permissions:
- if -1 in level_permissions[level.name]:
- return True
- has_perm_role = any(
- role.id in level_permissions[level.name] for role in author_roles
- )
- has_perm_id = ctx.author.id in level_permissions[level.name]
- if has_perm_role or has_perm_id:
+ # -1 is for @everyone
+ if -1 in level_permissions[level.name] or any(
+ check.id in level_permissions[level.name] for check in checkables
+ ):
return True
return False
diff --git a/core/config.py b/core/config.py
index b682f32e04..d07b87ea3b 100644
--- a/core/config.py
+++ b/core/config.py
@@ -74,6 +74,7 @@ class ConfigManager:
"blocked_whitelist": [],
"command_permissions": {},
"level_permissions": {},
+ "override_command_level": {},
# threads
"snippets": {},
"notification_squad": {},
diff --git a/core/config_help.json b/core/config_help.json
index be499bb230..2d9a9d68e2 100644
--- a/core/config_help.json
+++ b/core/config_help.json
@@ -412,7 +412,7 @@
},
"mongo_uri": {
"default": "None, required",
- "description": "A MongoDB SRV connection string.",
+ "description": "A MongoDB connection string.",
"examples": [
],
"notes": [
@@ -425,7 +425,7 @@
"examples": [
],
"notes": [
- "This configuration can only to be set through `.env` file, ~~`config.json` file~~ (removed), or environment (config) variables."
+ "This configuration can only to be set through `.env` file or environment (config) variables."
]
},
"token": {
@@ -434,7 +434,7 @@
"examples": [
],
"notes": [
- "This configuration can only to be set through `.env` file, ~~`config.json` file~~ (removed), or environment (config) variables."
+ "This configuration can only to be set through `.env` file or environment (config) variables."
]
},
"log_level": {
@@ -443,7 +443,7 @@
"examples": [
],
"notes": [
- "This configuration can only to be set through `.env` file, ~~`config.json` file~~ (removed), or environment (config) variables."
+ "This configuration can only to be set through `.env` file or environment (config) variables."
]
}
}
\ No newline at end of file
diff --git a/core/utils.py b/core/utils.py
index 585c0fba3d..de17e18419 100644
--- a/core/utils.py
+++ b/core/utils.py
@@ -55,6 +55,7 @@ def truncate(text: str, max: int = 50) -> str: # pylint: disable=redefined-buil
str
The truncated text.
"""
+ text = text.strip()
return text[: max - 3].strip() + "..." if len(text) > max else text
@@ -75,7 +76,7 @@ def format_preview(messages: typing.List[typing.Dict[str, typing.Any]]):
messages = messages[:3]
out = ""
for message in messages:
- if message.get("type") in ("note", "internal"):
+ if message.get("type") in {"note", "internal"}:
continue
author = message["author"]
content = str(message["content"]).replace("\n", " ")
@@ -193,19 +194,6 @@ def match_user_id(text: str) -> int:
return -1
-def get_perm_level(cmd):
- from core.models import PermissionLevel
-
- for check in cmd.checks:
- perm = getattr(check, "permission_level", None)
- if perm is not None:
- return perm
- for check in cmd.checks:
- if "is_owner" in str(check):
- return PermissionLevel.OWNER
- return PermissionLevel.INVALID
-
-
async def ignore(coro):
try:
await coro
From 259ba09954aeda73dc5d4eef7727c7c75f1ff2e5 Mon Sep 17 00:00:00 2001
From: Taaku18 <45324516+Taaku18@users.noreply.github.com>
Date: Sat, 17 Aug 2019 10:43:08 -1000
Subject: [PATCH 0371/1221] Add checks when changing perm level
---
cogs/utility.py | 5 +++++
1 file changed, 5 insertions(+)
diff --git a/cogs/utility.py b/cogs/utility.py
index 00963b88b2..f3a543336e 100644
--- a/cogs/utility.py
+++ b/cogs/utility.py
@@ -1294,6 +1294,11 @@ async def permissions_override(
self.bot.config["override_command_level"][
command.qualified_name
] = level.name
+
+ if not any(check for check in command.checks if hasattr(check, "permission_level")):
+ logger.debug('Command %s has no permissions check, adding invalid.', command.qualified_name)
+ checks.has_permissions(PermissionLevel.INVALID)(command)
+
await self.bot.config.update()
embed = Embed(
title="Success",
From f066b32e14c8f158ba5822e01255665552274cb4 Mon Sep 17 00:00:00 2001
From: Lasse Niermann
Date: Tue, 20 Aug 2019 00:11:43 +0200
Subject: [PATCH 0372/1221] Added Moderator to blocking reason (#349)
Co-Authored-By: Jia Rong Yee <28086837+fourjr@users.noreply.github.com>
---
cogs/modmail.py | 10 ++++++----
1 file changed, 6 insertions(+), 4 deletions(-)
diff --git a/cogs/modmail.py b/cogs/modmail.py
index 7f2bb4ce56..3bedd23477 100644
--- a/cogs/modmail.py
+++ b/cogs/modmail.py
@@ -1012,6 +1012,8 @@ async def block(
mention = getattr(user, "mention", f"`{user.id}`")
+ moderator = ctx.author.name
+
if str(user.id) in self.bot.blocked_whitelisted_users:
embed = discord.Embed(
title="Error",
@@ -1021,7 +1023,7 @@ async def block(
return await ctx.send(embed=embed)
if after is not None:
- reason = after.arg
+ reason = f"{after.arg} by {moderator}"
if reason.startswith("System Message: "):
raise commands.BadArgument(
"The reason cannot start with `System Message:`."
@@ -1029,10 +1031,10 @@ async def block(
if "%" in reason:
raise commands.BadArgument('The reason contains illegal character "%".')
if after.dt > after.now:
- reason = f"{reason} %{after.dt.isoformat()}%"
+ reason = f"{reason} %{after.dt.isoformat()}% by {moderator}"
if not reason:
- reason = None
+ reason = f"Blocked by {moderator}"
extend = f" for `{reason}`" if reason is not None else ""
msg = self.bot.blocked_users.get(str(user.id))
@@ -1046,7 +1048,7 @@ async def block(
):
if str(user.id) in self.bot.blocked_users:
- old_reason = msg.strip().rstrip(".") or "no reason"
+ old_reason = msg.strip().rstrip(".") or f"Blocked by {moderator}"
embed = discord.Embed(
title="Success",
description=f"{mention} was previously blocked for "
From 0032a9dda426db2e3e586ab04ce3d5fb2cc38112 Mon Sep 17 00:00:00 2001
From: Taaku18 <45324516+Taaku18@users.noreply.github.com>
Date: Fri, 23 Aug 2019 02:23:50 -0700
Subject: [PATCH 0373/1221] Fix override
---
CHANGELOG.md | 1 +
bot.py | 14 +++++++++++++-
cogs/utility.py | 37 +++++++++++++++++++++++--------------
core/checks.py | 4 +++-
4 files changed, 40 insertions(+), 16 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 09998321ce..8d89d2d73b 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -22,6 +22,7 @@ however, insignificant breaking changes does not guarantee a major version bump,
### Internal
- Use regex to parse Changes, Added, Fixed, etc and description.
+- Adds `PermissionLevel.INVALID` when commands doesn't have a permission level.
# v3.1.1
diff --git a/bot.py b/bot.py
index 3bb205c991..62803d525d 100644
--- a/bot.py
+++ b/bot.py
@@ -29,6 +29,7 @@
except ImportError:
pass
+from core import checks
from core.clients import ApiClient, PluginDatabaseClient
from core.config import ConfigManager
from core.utils import human_join, strtobool, parse_alias
@@ -840,6 +841,17 @@ async def process_commands(self, message):
ctxs = await self.get_contexts(message)
for ctx in ctxs:
if ctx.command:
+ if not any(
+ 1
+ for check in ctx.command.checks
+ if hasattr(check, "permission_level")
+ ):
+ logger.debug(
+ "Command %s has no permissions check, adding invalid level.",
+ ctx.command.qualified_name,
+ )
+ checks.has_permissions(PermissionLevel.INVALID)(ctx.command)
+
await self.invoke(ctx)
continue
@@ -1074,7 +1086,7 @@ async def on_command_error(self, context, exception):
color=discord.Color.red(), description=check.fail_msg
)
)
- if hasattr(check, 'permission_level'):
+ if hasattr(check, "permission_level"):
corrected_permission_level = self.command_perm(
context.command.qualified_name
)
diff --git a/cogs/utility.py b/cogs/utility.py
index f3a543336e..c10f845e72 100644
--- a/cogs/utility.py
+++ b/cogs/utility.py
@@ -646,6 +646,7 @@ async def set_presence(
@tasks.loop(minutes=45)
async def loop_presence(self):
"""Set presence to the configured value every 45 minutes."""
+ # TODO: Does this even work?
presence = await self.set_presence()
logger.debug(f'{presence["activity"][1]} {presence["status"][1]}')
@@ -1295,10 +1296,6 @@ async def permissions_override(
command.qualified_name
] = level.name
- if not any(check for check in command.checks if hasattr(check, "permission_level")):
- logger.debug('Command %s has no permissions check, adding invalid.', command.qualified_name)
- checks.has_permissions(PermissionLevel.INVALID)(command)
-
await self.bot.config.update()
embed = Embed(
title="Success",
@@ -1618,25 +1615,37 @@ async def permissions_get(
for command in self.bot.walk_commands():
if command not in done:
done.add(command)
- level = self.bot.config["override_command_level"].get(command.qualified_name)
+ level = self.bot.config["override_command_level"].get(
+ command.qualified_name
+ )
if level is not None:
overrides[command.qualified_name] = level
embeds = []
if not overrides:
- embeds.append(Embed(
- title="Permission Overrides",
- description="You don't have any command level overrides at the moment.",
- color=Color.red()
- ))
+ embeds.append(
+ Embed(
+ title="Permission Overrides",
+ description="You don't have any command level overrides at the moment.",
+ color=Color.red(),
+ )
+ )
else:
- for items in zip_longest(*(iter(sorted(overrides.items())),) * 15):
+ for items in zip_longest(
+ *(iter(sorted(overrides.items())),) * 15
+ ):
description = "\n".join(
": ".join((f"`{name}`", level))
- for name, level in takewhile(lambda x: x is not None, items)
+ for name, level in takewhile(
+ lambda x: x is not None, items
+ )
+ )
+ embed = Embed(
+ color=self.bot.main_color, description=description
+ )
+ embed.set_author(
+ name="Permission Overrides", icon_url=ctx.guild.icon_url
)
- embed = Embed(color=self.bot.main_color, description=description)
- embed.set_author(name="Permission Overrides", icon_url=ctx.guild.icon_url)
embeds.append(embed)
session = EmbedPaginatorSession(ctx, *embeds)
diff --git a/core/checks.py b/core/checks.py
index 2e3dbe58d9..6ccf1647b8 100644
--- a/core/checks.py
+++ b/core/checks.py
@@ -7,7 +7,9 @@
logger = logging.getLogger("Modmail")
-def has_permissions_predicate(permission_level: PermissionLevel = PermissionLevel.REGULAR):
+def has_permissions_predicate(
+ permission_level: PermissionLevel = PermissionLevel.REGULAR
+):
async def predicate(ctx):
return await check_permissions(ctx, ctx.command.qualified_name)
From 244dd9490d1ee73c6987b4830d9f3ce2116cf293 Mon Sep 17 00:00:00 2001
From: weirdllamas
Date: Fri, 23 Aug 2019 20:18:57 -0700
Subject: [PATCH 0374/1221] Added to Config: thread_move_notify and
thread_move_response Closes Issue #189
---
cogs/modmail.py | 7 +++++++
core/config.py | 3 +++
core/config_help.json | 21 +++++++++++++++++++++
3 files changed, 31 insertions(+)
diff --git a/cogs/modmail.py b/cogs/modmail.py
index e3f57ffb7d..78f847d7f9 100644
--- a/cogs/modmail.py
+++ b/cogs/modmail.py
@@ -294,6 +294,13 @@ async def move(self, ctx, *, category: discord.CategoryChannel):
"""
thread = ctx.thread
await thread.channel.edit(category=category, sync_permissions=True)
+ if self.bot.config["thread_move_notify"]:
+ embed = discord.Embed(
+ title="Thread Moved",
+ description=self.bot.config["thread_move_response"],
+ color=discord.Color.red())
+
+ await thread.recipient.send(embed=embed)
sent_emoji, _ = await self.bot.retrieve_emoji()
try:
await ctx.message.add_reaction(sent_emoji)
diff --git a/core/config.py b/core/config.py
index d07b87ea3b..b2109a94da 100644
--- a/core/config.py
+++ b/core/config.py
@@ -53,6 +53,8 @@ class ConfigManager:
"thread_close_title": "Thread Closed",
"thread_close_response": "{closer.mention} has closed this Modmail thread.",
"thread_self_close_response": "You have closed this Modmail thread.",
+ "thread_move_notify": False,
+ "thread_move_response": "This thread has been moved.",
# moderation
"recipient_color": str(discord.Color.gold()),
"mod_color": str(discord.Color.green()),
@@ -109,6 +111,7 @@ class ConfigManager:
"reply_without_command",
"recipient_thread_close",
"thread_auto_close_silently",
+ "thread_move_notify",
}
defaults = {**public_keys, **private_keys, **protected_keys}
diff --git a/core/config_help.json b/core/config_help.json
index 2d9a9d68e2..fd8649859c 100644
--- a/core/config_help.json
+++ b/core/config_help.json
@@ -300,6 +300,27 @@
"See also: `thread_close_title`, `thread_close_footer`, `thread_close_response`."
]
},
+ "thread_move_notify": {
+ "default": "No",
+ "description": "Notify the recipient if the thread was moved.",
+ "examples": [
+ "`{prefix}config set thread_move_notify yes`",
+ "`{prefix}config set thread_move_notify no`"
+ ],
+ "notes": [
+ "See also: `thread_move_response`"
+ ]
+ },
+ "thread_move_response": {
+ "default": "This thread has been moved.",
+ "description": "This is the message to display to the user when the thread is moved.",
+ "examples": [
+ "`{prefix}config set thread_move_response This thread has been moved to another category for review!`"
+ ],
+ "notes": [
+ "Only has an effect when thread_move_notify is on."
+ ]
+ },
"recipient_color": {
"default": "Discord Gold [#F1C40F](https://placehold.it/100/f1c40f?text=+)",
"description": "This is the color of the messages sent by the recipient, this applies to messages received in the thread channel.",
From 8c5ff588812b3ce31f4b01b169fb295167e34b69 Mon Sep 17 00:00:00 2001
From: weirdllamas
Date: Fri, 23 Aug 2019 20:25:41 -0700
Subject: [PATCH 0375/1221] Show Message ID Updated CHANGELOG.md Closes issue
#344
---
CHANGELOG.md | 2 ++
core/thread.py | 2 +-
2 files changed, 3 insertions(+), 1 deletion(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 09998321ce..c3aae3cefd 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -12,6 +12,8 @@ however, insignificant breaking changes does not guarantee a major version bump,
- Ability to change permission levels of individual commands.
- See `?permissions override` for more information.
+- `thread_move_notify` and `thread_move_response` to notify recipients if a thread is moved
+- IDs of messages sent to Modmail are now viewable
### Fixed
diff --git a/core/thread.py b/core/thread.py
index 00aa24c9c7..b1ad46086d 100644
--- a/core/thread.py
+++ b/core/thread.py
@@ -763,7 +763,7 @@ async def send(
elif note:
embed.colour = discord.Color.blurple()
else:
- embed.set_footer(text=f"Recipient")
+ embed.set_footer(text=f"Message ID: {message.id}")
embed.colour = self.bot.recipient_color
try:
From 85fdadd3a8d427fbcae7252cc29011cf54732123 Mon Sep 17 00:00:00 2001
From: Stephen <48072084+StephenDaDev@users.noreply.github.com>
Date: Sat, 24 Aug 2019 02:16:06 -0400
Subject: [PATCH 0376/1221] Update recipient_thread_close
---
core/config_help.json | 8 ++++----
1 file changed, 4 insertions(+), 4 deletions(-)
diff --git a/core/config_help.json b/core/config_help.json
index 2d9a9d68e2..c24b5868fa 100644
--- a/core/config_help.json
+++ b/core/config_help.json
@@ -39,7 +39,7 @@
"`{prefix}mention Yo~ Here's a new thread for ya!`"
],
"notes": [
- "Unfortunately, its not currently possible to disable mention."
+ "Unfortunately, its not currently possible to disable mention. You do not have to include a mention."
]
},
"main_color": {
@@ -163,8 +163,8 @@
"default": "Disabled",
"description": "Setting this configuration will allow recipients to use the `close_emoji` to close the thread themselves.",
"examples": [
- "`{prefix}config set reply_without_command yes`",
- "`{prefix}config set reply_without_command no`"
+ "`{prefix}config set recipient_thread_close yes`",
+ "`{prefix}config set recipient_thread_close no`"
],
"notes": [
"The close emoji is dictated by the configuration `close_emoji`.",
@@ -446,4 +446,4 @@
"This configuration can only to be set through `.env` file or environment (config) variables."
]
}
-}
\ No newline at end of file
+}
From 45c42a012a16229d92a7615022c376dd6935f68c Mon Sep 17 00:00:00 2001
From: Weirdllamas <51797295+Weirdllamas@users.noreply.github.com>
Date: Sat, 24 Aug 2019 21:21:41 -0600
Subject: [PATCH 0377/1221] Update CONTRIBUTING.md (#360)
* Update CONTRIBUTING.md
Fixed the bit about Git Flow
Fixed the license name and link
Just some general stuff
---
CONTRIBUTING.md | 17 +++++++++++------
1 file changed, 11 insertions(+), 6 deletions(-)
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 6d001f9004..5d7078e485 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -13,20 +13,25 @@ If you are proposing new features, please discuss them with us in the [developme
## We Develop with Github
We use github to host code, to track issues and feature requests, as well as accept pull requests.
-## We Use [Github Flow](https://guides.github.com/introduction/flow/index.html), So All Code Changes Happen Through Pull Requests
-Pull requests are the best way to propose changes to the codebase (we use [Github Flow](https://guides.github.com/introduction/flow/index.html)). We actively welcome your pull requests:
+## We Use [Git Flow](https://atlassian.com/git/tutorials/comparing-workflows/gitflow-workflow)
+
+When contributing to this project please make sure you follow this and name your branches appropriately!
-1. Fork the repo and create your branch from `master`.
+## All Code Changes Happen Through Pull Requests
+Make sure you know how Git Flow works before contributing!
+Pull requests are the best way to propose changes to the codebase. We actively welcome your pull requests:
+
+1. Fork the repo and create your branch from `master` or `development` according to Git Flow.
2. If you've added code that should be tested, add tests.
3. If you've changed APIs, update the documentation.
4. Ensure the test suite passes.
5. Make sure your code lints.
6. Issue that pull request!
-## Any contributions you make will be under the MIT Software License
-In short, when you submit code changes, your submissions are understood to be under the same [MIT License](http://choosealicense.com/licenses/mit/) that covers the project. Feel free to contact the maintainers if that's a concern.
+## Any contributions you make will be under the GNU Affero General Public License v3.0
+In short, when you submit code changes, your submissions are understood to be under the same [GNU Affero General Public License v3.0](https://www.gnu.org/licenses/agpl-3.0.en.html) that covers the project. Feel free to contact the maintainers if that's a concern.
-## Report bugs using Github's [issues](https://github.com/kyb3r/modmail/issues)
+## Report bugs using [Github Issues](https://github.com/kyb3r/modmail/issues)
We use GitHub issues to track public bugs. Report a bug by [opening a new issue](https://github.com/kyb3r/modmail/issues/new); it's that easy!
## Write bug reports with detail, background, and sample code
From 562214eea21e1065b930caccd9a84a1bdae0f0fb Mon Sep 17 00:00:00 2001
From: Weirdllamas <51797295+Weirdllamas@users.noreply.github.com>
Date: Sun, 25 Aug 2019 17:50:44 -0600
Subject: [PATCH 0378/1221] Update cogs/modmail.py
Co-Authored-By: Taku 3 Animals <45324516+Taaku18@users.noreply.github.com>
---
cogs/modmail.py | 6 +++++-
1 file changed, 5 insertions(+), 1 deletion(-)
diff --git a/cogs/modmail.py b/cogs/modmail.py
index 78f847d7f9..92e4b14c84 100644
--- a/cogs/modmail.py
+++ b/cogs/modmail.py
@@ -294,7 +294,11 @@ async def move(self, ctx, *, category: discord.CategoryChannel):
"""
thread = ctx.thread
await thread.channel.edit(category=category, sync_permissions=True)
- if self.bot.config["thread_move_notify"]:
+ try:
+ thread_move_notify = strtobool(self.config["thread_move_notify"])
+ except ValueError:
+ thread_move_notify = self.config.remove("thread_move_notify")
+ if thread_move_notify:
embed = discord.Embed(
title="Thread Moved",
description=self.bot.config["thread_move_response"],
From 223c33f4148931e708cabb6ec4962f9a12fe25a8 Mon Sep 17 00:00:00 2001
From: Weirdllamas <51797295+Weirdllamas@users.noreply.github.com>
Date: Sun, 25 Aug 2019 17:51:16 -0600
Subject: [PATCH 0379/1221] Update core/config_help.json
Co-Authored-By: Taku 3 Animals <45324516+Taaku18@users.noreply.github.com>
---
core/config_help.json | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/core/config_help.json b/core/config_help.json
index fd8649859c..44f211003b 100644
--- a/core/config_help.json
+++ b/core/config_help.json
@@ -308,7 +308,7 @@
"`{prefix}config set thread_move_notify no`"
],
"notes": [
- "See also: `thread_move_response`"
+ "See also: `thread_move_response`."
]
},
"thread_move_response": {
@@ -467,4 +467,4 @@
"This configuration can only to be set through `.env` file or environment (config) variables."
]
}
-}
\ No newline at end of file
+}
From bd6bb708d77b0f25772a128d47597f889efa512b Mon Sep 17 00:00:00 2001
From: Weirdllamas <51797295+Weirdllamas@users.noreply.github.com>
Date: Sun, 25 Aug 2019 17:51:26 -0600
Subject: [PATCH 0380/1221] Update core/config_help.json
Co-Authored-By: Taku 3 Animals <45324516+Taaku18@users.noreply.github.com>
---
core/config_help.json | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/core/config_help.json b/core/config_help.json
index 44f211003b..50d0a9fad9 100644
--- a/core/config_help.json
+++ b/core/config_help.json
@@ -318,7 +318,8 @@
"`{prefix}config set thread_move_response This thread has been moved to another category for review!`"
],
"notes": [
- "Only has an effect when thread_move_notify is on."
+ "Only has an effect when `thread_move_notify` is on.",
+ "See also: `thread_move_notify`."
]
},
"recipient_color": {
From 6bf83c8bee13ce4f70f7f651a711764a39d2b30b Mon Sep 17 00:00:00 2001
From: Weirdllamas <51797295+Weirdllamas@users.noreply.github.com>
Date: Sun, 25 Aug 2019 17:53:29 -0600
Subject: [PATCH 0381/1221] Update CHANGELOG.md
---
CHANGELOG.md | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index c3aae3cefd..4ba3ebf67a 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -12,8 +12,8 @@ however, insignificant breaking changes does not guarantee a major version bump,
- Ability to change permission levels of individual commands.
- See `?permissions override` for more information.
-- `thread_move_notify` and `thread_move_response` to notify recipients if a thread is moved
-- IDs of messages sent to Modmail are now viewable
+- `thread_move_notify` and `thread_move_response` to notify recipients if a thread is moved.
+- IDs of messages sent to Modmail are now viewable.
### Fixed
From 5f77f8e18f0f90ad861ee064b263bc2756028d40 Mon Sep 17 00:00:00 2001
From: weirdllamas
Date: Sun, 25 Aug 2019 17:39:41 -0700
Subject: [PATCH 0382/1221] Updating modmail.move Added specifics to arguments
of move, allowing for an easily expandable set of specifics for the command
Added a specific for silent moving for modmail.move
---
cogs/modmail.py | 19 ++++++++++++++-----
1 file changed, 14 insertions(+), 5 deletions(-)
diff --git a/cogs/modmail.py b/cogs/modmail.py
index 92e4b14c84..f390ace1c7 100644
--- a/cogs/modmail.py
+++ b/cogs/modmail.py
@@ -17,7 +17,7 @@
from core.models import PermissionLevel
from core.paginator import EmbedPaginatorSession
from core.time import UserFriendlyTime, human_timedelta
-from core.utils import format_preview, User, create_not_found_embed, format_description
+from core.utils import format_preview, User, create_not_found_embed, format_description, strtobool
logger = logging.getLogger("Modmail")
@@ -286,25 +286,34 @@ async def snippet_edit(self, ctx, name: str.lower, *, value):
@commands.command()
@checks.has_permissions(PermissionLevel.MODERATOR)
@checks.thread_only()
- async def move(self, ctx, *, category: discord.CategoryChannel):
+ async def move(self, ctx, category: discord.CategoryChannel, *, specifics: str = None):
"""
Move a thread to another category.
`category` may be a category ID, mention, or name.
"""
thread = ctx.thread
+ silent = False
+
+ if specifics:
+ silent_words = ['silent', 'quiet']
+ silent = any(word in specifics for word in silent_words)
+
await thread.channel.edit(category=category, sync_permissions=True)
+
try:
- thread_move_notify = strtobool(self.config["thread_move_notify"])
+ thread_move_notify = strtobool(self.bot.config["thread_move_notify"])
except ValueError:
- thread_move_notify = self.config.remove("thread_move_notify")
- if thread_move_notify:
+ thread_move_notify = self.bot.config.remove("thread_move_notify")
+
+ if thread_move_notify and not silent:
embed = discord.Embed(
title="Thread Moved",
description=self.bot.config["thread_move_response"],
color=discord.Color.red())
await thread.recipient.send(embed=embed)
+
sent_emoji, _ = await self.bot.retrieve_emoji()
try:
await ctx.message.add_reaction(sent_emoji)
From c32f44a2796e0aca54af60482c45c9edc3079710 Mon Sep 17 00:00:00 2001
From: Taaku18 <45324516+Taaku18@users.noreply.github.com>
Date: Sun, 25 Aug 2019 19:35:54 -0700
Subject: [PATCH 0383/1221] Updated docker
---
.dockerignore | 154 ++++++++++++++++++++++++++++++++++++++++++++++++++
Dockerfile | 13 ++---
README.md | 35 ++++++++++++
3 files changed, 195 insertions(+), 7 deletions(-)
create mode 100644 .dockerignore
diff --git a/.dockerignore b/.dockerignore
new file mode 100644
index 0000000000..78f469063d
--- /dev/null
+++ b/.dockerignore
@@ -0,0 +1,154 @@
+# Byte-compiled / optimized / DLL files
+__pycache__/
+*.py[cod]
+*$py.class
+
+# C extensions
+*.so
+
+# Distribution / packaging
+.Python
+build/
+develop-eggs/
+dist/
+downloads/
+eggs/
+.eggs/
+lib/
+lib64/
+parts/
+sdist/
+var/
+wheels/
+pip-wheel-metadata/
+share/python-wheels/
+*.egg-info/
+.installed.cfg
+*.egg
+MANIFEST
+
+# PyInstaller
+# Usually these files are written by a python script from a template
+# before PyInstaller builds the exe, so as to inject date/other infos into it.
+*.manifest
+*.spec
+
+# Installer logs
+pip-log.txt
+pip-delete-this-directory.txt
+
+# Unit test / coverage reports
+htmlcov/
+.tox/
+.nox/
+.coverage
+.coverage.*
+.cache
+nosetests.xml
+coverage.xml
+*.cover
+.hypothesis/
+.pytest_cache/
+
+# Translations
+*.mo
+*.pot
+
+# Django stuff:
+*.log
+local_settings.py
+db.sqlite3
+
+# Flask stuff:
+instance/
+.webassets-cache
+
+# Scrapy stuff:
+.scrapy
+
+# Sphinx documentation
+docs/_build/
+
+# PyBuilder
+target/
+
+# Jupyter Notebook
+.ipynb_checkpoints
+
+# IPython
+profile_default/
+ipython_config.py
+
+# pyenv
+.python-version
+
+# celery beat schedule file
+celerybeat-schedule
+
+# SageMath parsed files
+*.sage.py
+
+# Environments
+.env
+.venv
+env/
+venv/
+ENV/
+env.bak/
+venv.bak/
+
+# Spyder project settings
+.spyderproject
+.spyproject
+
+# Rope project settings
+.ropeproject
+
+# mkdocs documentation
+/site
+
+# mypy
+.mypy_cache/
+.dmypy.json
+dmypy.json
+
+# Pyre type checker
+.pyre/
+
+# PyCharm
+.idea/
+
+# MacOS
+.DS_Store
+
+# VS Code
+.vscode/
+
+# Node
+package-lock.json
+node_modules/
+
+# Modmail
+config.json
+plugins/
+!plugins/registry.json
+temp/
+test.py
+
+# Other stuff
+.env.example
+.gitignore
+.lint.py
+.pylintrc
+.travis.yml
+app.json
+CHANGELOG.md
+CODE_OF_CONDUCT.md
+CONTRIBUTING.md
+Pipfile
+Pipfile.lock
+Procfile
+pyproject.toml
+README.md
+runtime.txt
+SPONSORS.json
diff --git a/Dockerfile b/Dockerfile
index 851c65b4cc..9735f92d08 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,7 +1,6 @@
-FROM library/python:latest
-RUN apt update && apt install -y pipenv
-RUN mkdir -p /bot && cd /bot && git clone https://github.com/kyb3r/modmail .
-WORKDIR /bot
-RUN pipenv install
-
-CMD ["pipenv", "run", "bot"]
\ No newline at end of file
+FROM python:3.7.4-alpine
+RUN apk add --no-cache git
+WORKDIR /modmailbot
+COPY . /modmailbot
+RUN pip install --no-cache-dir -r requirements.min.txt
+CMD ["python", "bot.py"]
\ No newline at end of file
diff --git a/README.md b/README.md
index 46e41697e9..71b17e8e30 100644
--- a/README.md
+++ b/README.md
@@ -131,6 +131,41 @@ Finally, start Modmail.
$ pipenv run bot
```
+#### Docker
+
+This repo supplies a Dockerfile for simplified deployment.
+
+You can build your own Docker image:
+
+```console
+$ docker build . --tag=modmail
+```
+
+or run directly from a pre-built version from https://hub.docker.com/. Currently there are two community release of Modmail:
+
+- Kyber's:
+
+```console
+$ docker pull kyb3rr/modmail
+```
+
+- Taku's:
+
+```console
+$ docker pull taaku18/modmail
+# You can also choose one of the following:
+$ docker pull taaku18/modmail:dev
+$ docker pull taaku18/modmail: ( ex: 3.2.0, 3.2, etc.)
+```
+
+And to run your docker image:
+
+```console
+$ docker run --env-file .env user/modmail
+```
+- Replace `user/modmail` with `kyb3rr/modmail`, `taaku18/modmail`, `taaku18/modmail:3.2`, etc as above.
+- `.env` should be the path to your env file, you can also supply a path: `/path/to/.env`.
+
## Sponsors
Special thanks to our sponsors for supporting the project.
From 2e53b7c7c38727fcb4bbded3fb1ac50c8dff9757 Mon Sep 17 00:00:00 2001
From: Weirdllamas <51797295+Weirdllamas@users.noreply.github.com>
Date: Mon, 26 Aug 2019 14:30:02 -0600
Subject: [PATCH 0384/1221] Update modmail.py
---
cogs/modmail.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/cogs/modmail.py b/cogs/modmail.py
index f390ace1c7..42ce97f5a3 100644
--- a/cogs/modmail.py
+++ b/cogs/modmail.py
@@ -310,7 +310,7 @@ async def move(self, ctx, category: discord.CategoryChannel, *, specifics: str =
embed = discord.Embed(
title="Thread Moved",
description=self.bot.config["thread_move_response"],
- color=discord.Color.red())
+ color=self.bot.main_color
await thread.recipient.send(embed=embed)
From 133a565ba76428b5d0db2f4134064cd4dedc2cbb Mon Sep 17 00:00:00 2001
From: Weirdllamas <51797295+Weirdllamas@users.noreply.github.com>
Date: Mon, 26 Aug 2019 14:55:35 -0600
Subject: [PATCH 0385/1221] Update modmail.py
---
cogs/modmail.py | 5 +++--
1 file changed, 3 insertions(+), 2 deletions(-)
diff --git a/cogs/modmail.py b/cogs/modmail.py
index 42ce97f5a3..d63a9e8b59 100644
--- a/cogs/modmail.py
+++ b/cogs/modmail.py
@@ -291,13 +291,14 @@ async def move(self, ctx, category: discord.CategoryChannel, *, specifics: str =
Move a thread to another category.
`category` may be a category ID, mention, or name.
+ `specifics` is a string which takes in arguments on how to perform the move. Ex: "silently"
"""
thread = ctx.thread
silent = False
if specifics:
- silent_words = ['silent', 'quiet']
- silent = any(word in specifics for word in silent_words)
+ silent_words = ['silent', 'silently']
+ silent = any(word in silent_words for word in specifics.split())
await thread.channel.edit(category=category, sync_permissions=True)
From d3bab1ac8caa7f75c8bcf3d96f823d2355a239d1 Mon Sep 17 00:00:00 2001
From: Taaku18 <45324516+Taaku18@users.noreply.github.com>
Date: Mon, 26 Aug 2019 14:12:15 -0700
Subject: [PATCH 0386/1221] Syntax error in previous commit
---
cogs/modmail.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/cogs/modmail.py b/cogs/modmail.py
index d63a9e8b59..a55b637ddb 100644
--- a/cogs/modmail.py
+++ b/cogs/modmail.py
@@ -312,7 +312,7 @@ async def move(self, ctx, category: discord.CategoryChannel, *, specifics: str =
title="Thread Moved",
description=self.bot.config["thread_move_response"],
color=self.bot.main_color
-
+ )
await thread.recipient.send(embed=embed)
sent_emoji, _ = await self.bot.retrieve_emoji()
From 872d3044a0f73ce59b0d5784799a9bf9cd9f2bce Mon Sep 17 00:00:00 2001
From: Taaku18 <45324516+Taaku18@users.noreply.github.com>
Date: Tue, 27 Aug 2019 11:08:57 -0700
Subject: [PATCH 0387/1221] Add attribution
---
CHANGELOG.md | 7 ++++---
1 file changed, 4 insertions(+), 3 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index c83a9ab37e..6129498119 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -6,20 +6,21 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
This project mostly adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html);
however, insignificant breaking changes does not guarantee a major version bump, see the reasoning [here](https://github.com/kyb3r/modmail/issues/319).
-# v3.2.0
+# UNRELEASED
### Added
- Ability to change permission levels of individual commands.
- See `?permissions override` for more information.
-- `thread_move_notify` and `thread_move_response` to notify recipients if a thread is moved.
-- IDs of messages sent to Modmail are now viewable.
+- `thread_move_notify` and `thread_move_response` to notify recipients if a thread is moved. (Thanks to Flufster PR#360)
+- IDs of messages sent to Modmail are now viewable. (Thanks to Flufster PR#360)
### Fixed
- `?help `, will return `Perhaps you meant: `, now its fixed.
- For example, `?help add` used to return `Perhaps you meant: add`, now it wouldn't do this.
- Aliases and Permissions command names are always saved lowercase now.
+- An improved Dockerfile.
### Internal
From 4968de4093db7862dae62e1cd469ed3deb5e40f1 Mon Sep 17 00:00:00 2001
From: lorenzo132 <50767078+lorenzo132@users.noreply.github.com>
Date: Sun, 8 Sep 2019 02:02:56 +0200
Subject: [PATCH 0388/1221] sponsor.json update (#365)
---
SPONSORS.json | 103 ++++++++++++++++++++++++++++++++++++++++++++++++++
1 file changed, 103 insertions(+)
diff --git a/SPONSORS.json b/SPONSORS.json
index 5f79ff80ea..4f65673165 100644
--- a/SPONSORS.json
+++ b/SPONSORS.json
@@ -1,28 +1,131 @@
[{
+
"embed": {
+
"description": "This is a youtube channel that provides high quality reddit content. Videos are uploaded regularly so you are never out of funny, interesting or even creepy content!",
+
"color": 15114050,
+
"footer": {
+
"icon_url": "https://i.imgur.com/fvNKUks.png",
+
"text": "I am a robot, son of Daniel (UK)"
+
},
+
"thumbnail": {
+
"url": "https://i.imgur.com/WyzaPKY.png"
+
},
+
"author": {
+
"name": "Sir Reddit",
+
"url": "https://www.youtube.com/channel/UCgSmBJD9imASmJRleycTCwQ?sub_confirmation=1",
+
"icon_url": "https://i.imgur.com/WyzaPKY.png"
+
},
+
"fields": [
+
{
+
"name": "Subscribe!",
+
"value": "[**Click Here**](https://www.youtube.com/channel/UCgSmBJD9imASmJRleycTCwQ?sub_confirmation=1)"
+
},
+
{
+
"name": "Discord Server!",
+
"value": "[**Click Here**](https://discord.gg/V8ErqHb)"
+
}
+
]
+
}
+
+},{
+
+ "embed": {
+
+ "title":"Hey there! ",
+
+ "description": "Nice to see you here! u can support us by subscribing on youtube -> [Youtube](https://www.youtube.com/user/RoomieOfficial) <-and also join our discord! [Discord](https://discord.gg/djwMSQ)",
+
+ "url": "https://discord.gg/djwMSQ",
+
+ "color": 13003681,
+
+ "timestamp": "2019-09-07T14:09:41.072Z",
+
+ "footer": {
+
+ "icon_url": "https://imgur.com/Mrc9pLd.gif",
+
+ "text": "footer text"
+
+ },
+
+ "thumbnail": {
+
+ "url": "https://imgur.com/Mrc9pLd.gif"
+
+ },
+
+ "image": {
+
+ "url": "https://imgur.com/ZUFiL6b.gif"
+
+ },
+
+ "author": {
+
+ "name": "Roomieofficial",
+
+ "url": "https://discord.gg/djwMSQ",
+
+ "icon_url": "https://imgur.com/6hBkt7Z.png"
+
+ },
+
+ "fields": [
+
+ {
+
+ "name": "What is all this about🤔 ",
+
+ "value": "We are mainly focused on everything that has anything to do with music or singing! "
+
+ },
+
+
+
+ {
+
+ "name": "Youtube🙄",
+
+ "value": "U will get great content if you follow our [Youtube](https://www.youtube.com/user/RoomieOfficial) with weekly uploads"
+
+ },{
+
+ "name": "Discord😁",
+
+ "value": "Make sure to join our [Discord](https://discord.gg/djwMSQ) We have weekly events, 24/7 chats and more! "
+
+
+
+ }
+
+ ]
+
+ }
+
}]
From 5cabada96ee4e54b7fe55bf5965625f6cbb5933f Mon Sep 17 00:00:00 2001
From: Kyber
Date: Sun, 8 Sep 2019 10:11:54 +1000
Subject: [PATCH 0389/1221] Update SPONSORS.json
---
SPONSORS.json | 194 +++++++++++++++++---------------------------------
1 file changed, 65 insertions(+), 129 deletions(-)
diff --git a/SPONSORS.json b/SPONSORS.json
index 4f65673165..9b9aabcbe7 100644
--- a/SPONSORS.json
+++ b/SPONSORS.json
@@ -1,131 +1,67 @@
-[{
-
- "embed": {
-
- "description": "This is a youtube channel that provides high quality reddit content. Videos are uploaded regularly so you are never out of funny, interesting or even creepy content!",
-
- "color": 15114050,
-
- "footer": {
-
- "icon_url": "https://i.imgur.com/fvNKUks.png",
-
- "text": "I am a robot, son of Daniel (UK)"
-
- },
-
- "thumbnail": {
-
- "url": "https://i.imgur.com/WyzaPKY.png"
-
- },
-
- "author": {
-
- "name": "Sir Reddit",
-
- "url": "https://www.youtube.com/channel/UCgSmBJD9imASmJRleycTCwQ?sub_confirmation=1",
-
- "icon_url": "https://i.imgur.com/WyzaPKY.png"
-
- },
-
- "fields": [
-
- {
-
- "name": "Subscribe!",
-
- "value": "[**Click Here**](https://www.youtube.com/channel/UCgSmBJD9imASmJRleycTCwQ?sub_confirmation=1)"
-
- },
-
- {
-
- "name": "Discord Server!",
-
- "value": "[**Click Here**](https://discord.gg/V8ErqHb)"
-
+[
+ {
+ "embed":{
+ "description":"This is a youtube channel that provides high quality reddit content. Videos are uploaded regularly so you are never out of funny, interesting or even creepy content!",
+ "color":15114050,
+ "footer":{
+ "icon_url":"https://i.imgur.com/fvNKUks.png",
+ "text":"I am a robot, son of Daniel (UK)"
+ },
+ "thumbnail":{
+ "url":"https://i.imgur.com/WyzaPKY.png"
+ },
+ "author":{
+ "name":"Sir Reddit",
+ "url":"https://www.youtube.com/channel/UCgSmBJD9imASmJRleycTCwQ?sub_confirmation=1",
+ "icon_url":"https://i.imgur.com/WyzaPKY.png"
+ },
+ "fields":[
+ {
+ "name":"Subscribe!",
+ "value":"[**Click Here**](https://www.youtube.com/channel/UCgSmBJD9imASmJRleycTCwQ?sub_confirmation=1)"
+ },
+ {
+ "name":"Discord Server!",
+ "value":"[**Click Here**](https://discord.gg/V8ErqHb)"
+ }
+ ]
}
-
- ]
-
- }
-
-},{
-
- "embed": {
-
- "title":"Hey there! ",
-
- "description": "Nice to see you here! u can support us by subscribing on youtube -> [Youtube](https://www.youtube.com/user/RoomieOfficial) <-and also join our discord! [Discord](https://discord.gg/djwMSQ)",
-
- "url": "https://discord.gg/djwMSQ",
-
- "color": 13003681,
-
- "timestamp": "2019-09-07T14:09:41.072Z",
-
- "footer": {
-
- "icon_url": "https://imgur.com/Mrc9pLd.gif",
-
- "text": "footer text"
-
- },
-
- "thumbnail": {
-
- "url": "https://imgur.com/Mrc9pLd.gif"
-
- },
-
- "image": {
-
- "url": "https://imgur.com/ZUFiL6b.gif"
-
- },
-
- "author": {
-
- "name": "Roomieofficial",
-
- "url": "https://discord.gg/djwMSQ",
-
- "icon_url": "https://imgur.com/6hBkt7Z.png"
-
- },
-
- "fields": [
-
- {
-
- "name": "What is all this about🤔 ",
-
- "value": "We are mainly focused on everything that has anything to do with music or singing! "
-
- },
-
-
-
- {
-
- "name": "Youtube🙄",
-
- "value": "U will get great content if you follow our [Youtube](https://www.youtube.com/user/RoomieOfficial) with weekly uploads"
-
- },{
-
- "name": "Discord😁",
-
- "value": "Make sure to join our [Discord](https://discord.gg/djwMSQ) We have weekly events, 24/7 chats and more! "
-
-
-
+ },
+ {
+ "embed":{
+ "title":"Hey there! ",
+ "description":"Nice to see you here! u can support us by subscribing on youtube -> [Youtube](https://www.youtube.com/user/RoomieOfficial) <-and also join our discord! [Discord](https://discord.gg/djwMSQ)",
+ "url":"https://discord.gg/djwMSQ",
+ "color":13003681,
+ "footer":{
+ "icon_url":"https://imgur.com/Mrc9pLd.gif",
+ "text":"footer text"
+ },
+ "thumbnail":{
+ "url":"https://imgur.com/Mrc9pLd.gif"
+ },
+ "image":{
+ "url":"https://imgur.com/ZUFiL6b.gif"
+ },
+ "author":{
+ "name":"Roomieofficial",
+ "url":"https://discord.gg/djwMSQ",
+ "icon_url":"https://imgur.com/6hBkt7Z.png"
+ },
+ "fields":[
+ {
+ "name":"What is all this about🤔 ",
+ "value":"We are mainly focused on everything that has anything to do with music or singing! "
+ },
+ {
+ "name":"Youtube🙄",
+ "value":"U will get great content if you follow our [Youtube](https://www.youtube.com/user/RoomieOfficial) with weekly uploads"
+ },
+ {
+ "name":"Discord😁",
+ "value":"Make sure to join our [Discord](https://discord.gg/djwMSQ) We have weekly events, 24/7 chats and more! "
+ }
+ ]
}
-
- ]
-
- }
-
-}]
+ }
+]
From 7ac20f7309300dde8fa6471c280cc23c9b072554 Mon Sep 17 00:00:00 2001
From: lorenzo132 <50767078+lorenzo132@users.noreply.github.com>
Date: Sun, 8 Sep 2019 02:22:10 +0200
Subject: [PATCH 0390/1221] Update SPONSORS.json (#366)
---
SPONSORS.json | 8 ++++----
1 file changed, 4 insertions(+), 4 deletions(-)
diff --git a/SPONSORS.json b/SPONSORS.json
index 9b9aabcbe7..6a4b439907 100644
--- a/SPONSORS.json
+++ b/SPONSORS.json
@@ -30,8 +30,8 @@
{
"embed":{
"title":"Hey there! ",
- "description":"Nice to see you here! u can support us by subscribing on youtube -> [Youtube](https://www.youtube.com/user/RoomieOfficial) <-and also join our discord! [Discord](https://discord.gg/djwMSQ)",
- "url":"https://discord.gg/djwMSQ",
+ "description":"Nice to see you here! u can support us by subscribing on youtube -> [Youtube](https://www.youtube.com/user/RoomieOfficial) <-and also join our discord! [Discord](https://discord.gg/qdujXC8)",
+ "url":"https://discord.gg/qdujXC8",
"color":13003681,
"footer":{
"icon_url":"https://imgur.com/Mrc9pLd.gif",
@@ -45,7 +45,7 @@
},
"author":{
"name":"Roomieofficial",
- "url":"https://discord.gg/djwMSQ",
+ "url":"https://discord.gg/qdujXC8",
"icon_url":"https://imgur.com/6hBkt7Z.png"
},
"fields":[
@@ -59,7 +59,7 @@
},
{
"name":"Discord😁",
- "value":"Make sure to join our [Discord](https://discord.gg/djwMSQ) We have weekly events, 24/7 chats and more! "
+ "value":"Make sure to join our [Discord](https://discord.gg/qdujXC8) We have weekly events, 24/7 chats and more! "
}
]
}
From 2b73044ae316adbd1035d456d7dceff8b26da8c9 Mon Sep 17 00:00:00 2001
From: codeinteger6 <44692189+codeinteger6@users.noreply.github.com>
Date: Thu, 12 Sep 2019 23:24:15 +0600
Subject: [PATCH 0391/1221] Use Python 3.7.4
---
runtime.txt | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/runtime.txt b/runtime.txt
index 6f651a3b1c..42731f2fbe 100644
--- a/runtime.txt
+++ b/runtime.txt
@@ -1 +1 @@
-python-3.7.3
+python-3.7.4
From ff9554ed401ffb762dd742b39672d65abfd4ddd1 Mon Sep 17 00:00:00 2001
From: Taaku18 <45324516+Taaku18@users.noreply.github.com>
Date: Fri, 13 Sep 2019 20:31:01 -0700
Subject: [PATCH 0392/1221] Merge
---
.dockerignore | 1 +
.gitignore | 1 +
Pipfile | 2 +-
Pipfile.lock | 132 +++++++++++++++++++++++-------------------------
bot.py | 3 +-
cogs/utility.py | 2 +-
6 files changed, 70 insertions(+), 71 deletions(-)
diff --git a/.dockerignore b/.dockerignore
index 78f469063d..57b6d8718c 100644
--- a/.dockerignore
+++ b/.dockerignore
@@ -152,3 +152,4 @@ pyproject.toml
README.md
runtime.txt
SPONSORS.json
+stack.yml
\ No newline at end of file
diff --git a/.gitignore b/.gitignore
index 8e80af7c26..a7fb960a6b 100644
--- a/.gitignore
+++ b/.gitignore
@@ -134,3 +134,4 @@ plugins/
!plugins/registry.json
temp/
test.py
+stack.yml
\ No newline at end of file
diff --git a/Pipfile b/Pipfile
index 0b541bd697..cfb4f8ba59 100644
--- a/Pipfile
+++ b/Pipfile
@@ -19,7 +19,7 @@ dnspython = "~=1.16.0"
parsedatetime = "==2.4"
aiohttp = "<3.6.0,>=3.3.0"
python-dotenv = ">=0.10.3"
-pipenv = "==2018.11.26"
+pipenv = "*"
"discord.py" = "==1.2.3"
[requires]
diff --git a/Pipfile.lock b/Pipfile.lock
index 5fbfe85392..ee4681eb19 100644
--- a/Pipfile.lock
+++ b/Pipfile.lock
@@ -1,7 +1,7 @@
{
"_meta": {
"hash": {
- "sha256": "46092f8b1032f2ac529f9b0df1aa725a4c6135d04826cd4cc4844af01879a4c2"
+ "sha256": "564773a120ff7ba8214e5efd98fab8daf98230eb4b27f8af2dd63afcad39994c"
},
"pipfile-spec": 6,
"requires": {
@@ -97,11 +97,10 @@
},
"emoji": {
"hashes": [
- "sha256:9ae01495fc3fcc04e9136ca1af8cae58726ec5dfaaa92f61f0732cbae9a12fa9",
- "sha256:cecf8ed7cc535c999dc96e3fe7e7aacd176deb9ef62b3ef022c535e155084c44"
+ "sha256:b68112d40482a05e5da5d53da33d0aba3cce96891282c9c179cc340003c6c64e"
],
"index": "pypi",
- "version": "==0.5.2"
+ "version": "==0.5.3"
},
"future": {
"hashes": [
@@ -192,36 +191,36 @@
},
"pymongo": {
"hashes": [
- "sha256:32421df60d06f479d71b6b539642e410ece3006e8910688e68df962c8eb40a21",
- "sha256:324b22a8443e11faca44c96b20e7ec8a9e59a1e664457edeeb4f796080b31cde",
- "sha256:4505ff8b7923dd7a8bed1bf25c9c4d0df5ab0b8b2821f2296533f2149a55f401",
- "sha256:460b224681ea711e48e3638d15be2249024031b7dcb9622ba19c2e85bd5a26cc",
- "sha256:47473b70c5f3cd5ddd2c49ab3b9ceafdafbbed5bc963f147df22a9343d7978f5",
- "sha256:49375839af76834e9c5c3cc78c78386873fd0b2ad9a0860a7dc4ec9fe73af9dd",
- "sha256:4a65f0f71ece86c860d30a1436b646db8ea32aec518845ef2903ca569faec32e",
- "sha256:530621906c5dd6d27305b39c4e017701e5f4299aa68b93cde70eb985f94ca26f",
- "sha256:54f4770b5810e8dc3cbeed675874195f02bb2bc4e95a9d665068edfb3baff4f7",
- "sha256:5ed9382410e938b0ff76041c34018210504729a83bcf4f6a70c7092c28169f6f",
- "sha256:61cad83637ae12c1c825130d7f9325cd6c162e3a64e8747a8144866020be3ff4",
- "sha256:61e8e1c58b4fdf47ab79b7c7db8bb022c1e40b3b5fcbbaeea5fc94dc5c75638d",
- "sha256:6e04e496af7d156b66cce70460011c621ecbadf5dcdce325c7acbb3cd6ea245d",
- "sha256:7ef89ec435e89da902451dde6845066fe2770befaf0301fe2a1ac426b51fced3",
- "sha256:854e8425e5eb775ccfffad04ecd094c99923d60a2c2d49babb5c435e836a91fa",
- "sha256:9569796d48498e4db4e1d56284b626a8ed15f641ce3a8b2085f06bb03f4c2c88",
- "sha256:9d50c99c6388863cbfdc5db9bad62e3a7c2e5fc151554a07c7f3c2530334a34f",
- "sha256:9ea016c2c011df21f77c1f806ce45129a344ba2d414bd50f9e065b13a4a134be",
- "sha256:a8421f0823174888fb12a5fa675322e756499d71e77ff712b4412d4b8f3c6503",
- "sha256:aef7d88384ada699976350a285c7a333f96ebc959e98e7d2c98589f47bbf3b7f",
- "sha256:b4d7ff9957ee770cf03bd7156a68a2f2e838e60712d9608eadc8741c15d01e72",
- "sha256:c1db85c39e6a60588f855dbc7bd68fb0dab796096148ab5aa4abecaff19e1c6e",
- "sha256:cee2fc0b94e66e7230da12fc4b3d34793c49957e16ee04f6468a94e264a1e41d",
- "sha256:cf1dea28379a16b23e47db312883f07b3ba8d9d6abc1c59e51d4c8ae1820ab43",
- "sha256:d1cd175df7c8b5fc976bade78bf4d9fb5aa7ab465c0f59931e380bbe188ef8fc",
- "sha256:d48a94edf3cdd34524936a72ea01b352682b337f33a42db10ba29a96c37147d3",
- "sha256:d9cc103a4e97f78bc77a1d72759ab3722f6cdf0374ad4fb4b0c53bd3238bdf98",
- "sha256:fcb9ae8aa9158106c5d98a4349ec0d90b68f052d620b2d24622ba03b91e4d81d"
- ],
- "version": "==3.8.0"
+ "sha256:09f8196e1cb081713aa3face08d1806dc0a5dd64cb9f67fefc568519253a7ff2",
+ "sha256:1be549c0ce2ba8242c149156ae2064b12a5d4704448d49f630b4910606efd474",
+ "sha256:1f9fe869e289210250cba4ea20fbd169905b1793e1cd2737f423e107061afa98",
+ "sha256:3653cea82d1e35edd0a2355150daf8a27ebf12cf55182d5ad1046bfa288f5140",
+ "sha256:4249c6ba45587b959292a727532826c5032d59171f923f7f823788f413c2a5a3",
+ "sha256:4ff8f5e7c0a78983c1ee07894fff1b21c0e0ad3a122d9786cc3745fd60e4a2ce",
+ "sha256:56b29c638ab924716b48a3e94e3d7ac00b04acec1daa8190c36d61fc714c3629",
+ "sha256:56ec9358bbfe5ae3b25e785f8a14619d6799c855a44734c9098bb457174019bf",
+ "sha256:5dca250cbf1183c3e7b7b18c882c2b2199bfb20c74c4c68dbf11596808a296da",
+ "sha256:61101d1cc92881fac1f9ac7e99b033062f4c210178dc33193c8f5567feecb069",
+ "sha256:86624c0205a403fb4fbfedef79c5b4ab27e21fd018fdb6a27cf03b3c32a9e2b9",
+ "sha256:88ac09e1b197c3b4531e43054d49c022a3ea1281431b2f4980abafa35d2a5ce2",
+ "sha256:8b0339809b12ea292d468524dd1777f1a9637d9bdc0353a9261b88f82537d606",
+ "sha256:93dbf7388f6bf9af48dbb32f265b75b3dbc743a7a2ce98e44c88c049c58d85d3",
+ "sha256:9b705daec636c560dd2d63935f428a6b3cddfe903fffc0f349e0e91007c893d6",
+ "sha256:a090a819fe6fefadc2901d3911c07c76c0935ec5c790a50e9f3c3c47bacd5978",
+ "sha256:a102b346f1921237eaa9a31ee89eda57ad3c3973d79be3a456d92524e7df8fec",
+ "sha256:a13363869f2f36291d6367069c65d51d7b8d1b2fb410266b0b6b1f3c90d6deb0",
+ "sha256:a409a43c76da50881b70cc9ee70a1744f882848e8e93a68fb434254379777fa3",
+ "sha256:a76475834a978058425b0163f1bad35a5f70e45929a543075633c3fc1df564c5",
+ "sha256:ad474e93525baa6c58d75d63a73143af24c9f93c8e26e8d382f32c4da637901a",
+ "sha256:b268c7fa03ac77a8662fab3b2ab0be4beecb82f60f4c24b584e69565691a107f",
+ "sha256:cca4e1ab5ba0cd7877d3938167ee8ae9c2986cc0e10d3dcc3243d664d3a83fec",
+ "sha256:cef61de3f0f4441ec40266ff2ab42e5c16eaba1dc1fc6e1036f274621c52adc1",
+ "sha256:e28153b5d5ca33d4ba0c3bbc0e1ff161b9016e5e5f3f8ca10d6fa49106eb9e04",
+ "sha256:f30d7b37804daf0bab1143abc71666c630d7e270f5c14c5a7c300a6699c21108",
+ "sha256:f70f0133301cccf9bfd68fd20f67184ef991be578b646e78441106f9e27cc44d",
+ "sha256:fa75c21c1d82f20cce62f6fc4a68c2b0f33572ab406df1b17cd77a947d0b2993"
+ ],
+ "version": "==3.9.0"
},
"python-dateutil": {
"hashes": [
@@ -248,27 +247,24 @@
},
"uvloop": {
"hashes": [
- "sha256:0fcd894f6fc3226a962ee7ad895c4f52e3f5c3c55098e21efb17c071849a0573",
- "sha256:2f31de1742c059c96cb76b91c5275b22b22b965c886ee1fced093fa27dde9e64",
- "sha256:459e4649fcd5ff719523de33964aa284898e55df62761e7773d088823ccbd3e0",
- "sha256:67867aafd6e0bc2c30a079603a85d83b94f23c5593b3cc08ec7e58ac18bf48e5",
- "sha256:8c200457e6847f28d8bb91c5e5039d301716f5f2fce25646f5fb3fd65eda4a26",
- "sha256:958906b9ca39eb158414fbb7d6b8ef1b7aee4db5c8e8e5d00fcbb69a1ce9dca7",
- "sha256:ac1dca3d8f3ef52806059e81042ee397ac939e5a86c8a3cea55d6b087db66115",
- "sha256:b284c22d8938866318e3b9d178142b8be316c52d16fcfe1560685a686718a021",
- "sha256:c48692bf4587ce281d641087658eca275a5ad3b63c78297bbded96570ae9ce8f",
- "sha256:fefc3b2b947c99737c348887db2c32e539160dcbeb7af9aa6b53db7a283538fe"
+ "sha256:0deb6c97c5807c792dd9024bab90e6ca49e981862103cb2ea37b430c1ca0a267",
+ "sha256:155b34d513655e753d07f499a7e811970e2d397f240dfcbec0b32a9587159c99",
+ "sha256:1df3ddfa280206e9999ae1c777a20836eb895bcec6dc9fae2cbb6eecfafb099e",
+ "sha256:5b19361c8767e1dc61f6367f948d4f3dc5504b9f2eba488641b3d26ec14498ba",
+ "sha256:942cd07035510b149d6160796f4e972137130ae953871b6a98c2cf5d5ab68c2e",
+ "sha256:c63b6c0bf33144c604dd72f7eecf2d3a3ac7405c503c67bd98128cf306efe18a",
+ "sha256:e698a20a3b4ccb380d207f9d491d4085d7c38d364f6a0bae98684a1612a9607a"
],
"index": "pypi",
"markers": "sys_platform != 'win32'",
- "version": "==0.12.2"
+ "version": "==0.13.0"
},
"virtualenv": {
"hashes": [
- "sha256:861bbce3a418110346c70f5c7a696fdcf23a261424e1d28aa4f9362fc2ccbc19",
- "sha256:ba8ce6a961d842320681fb90a3d564d0e5134f41dacd0e2bae7f02441dde2d52"
+ "sha256:94a6898293d07f84a98add34c4df900f8ec64a570292279f6d91c781d37fd305",
+ "sha256:f6fc312c031f2d2344f885de114f1cb029dfcffd26aa6e57d2ee2296935c4e7d"
],
- "version": "==16.6.2"
+ "version": "==16.7.4"
},
"virtualenv-clone": {
"hashes": [
@@ -366,26 +362,26 @@
},
"lazy-object-proxy": {
"hashes": [
- "sha256:159a745e61422217881c4de71f9eafd9d703b93af95618635849fe469a283661",
- "sha256:23f63c0821cc96a23332e45dfaa83266feff8adc72b9bcaef86c202af765244f",
- "sha256:3b11be575475db2e8a6e11215f5aa95b9ec14de658628776e10d96fa0b4dac13",
- "sha256:3f447aff8bc61ca8b42b73304f6a44fa0d915487de144652816f950a3f1ab821",
- "sha256:4ba73f6089cd9b9478bc0a4fa807b47dbdb8fad1d8f31a0f0a5dbf26a4527a71",
- "sha256:4f53eadd9932055eac465bd3ca1bd610e4d7141e1278012bd1f28646aebc1d0e",
- "sha256:64483bd7154580158ea90de5b8e5e6fc29a16a9b4db24f10193f0c1ae3f9d1ea",
- "sha256:6f72d42b0d04bfee2397aa1862262654b56922c20a9bb66bb76b6f0e5e4f9229",
- "sha256:7c7f1ec07b227bdc561299fa2328e85000f90179a2f44ea30579d38e037cb3d4",
- "sha256:7c8b1ba1e15c10b13cad4171cfa77f5bb5ec2580abc5a353907780805ebe158e",
- "sha256:8559b94b823f85342e10d3d9ca4ba5478168e1ac5658a8a2f18c991ba9c52c20",
- "sha256:a262c7dfb046f00e12a2bdd1bafaed2408114a89ac414b0af8755c696eb3fc16",
- "sha256:acce4e3267610c4fdb6632b3886fe3f2f7dd641158a843cf6b6a68e4ce81477b",
- "sha256:be089bb6b83fac7f29d357b2dc4cf2b8eb8d98fe9d9ff89f9ea6012970a853c7",
- "sha256:bfab710d859c779f273cc48fb86af38d6e9210f38287df0069a63e40b45a2f5c",
- "sha256:c10d29019927301d524a22ced72706380de7cfc50f767217485a912b4c8bd82a",
- "sha256:dd6e2b598849b3d7aee2295ac765a578879830fb8966f70be8cd472e6069932e",
- "sha256:e408f1eacc0a68fed0c08da45f31d0ebb38079f043328dce69ff133b95c29dc1"
- ],
- "version": "==1.4.1"
+ "sha256:02b260c8deb80db09325b99edf62ae344ce9bc64d68b7a634410b8e9a568edbf",
+ "sha256:18f9c401083a4ba6e162355873f906315332ea7035803d0fd8166051e3d402e3",
+ "sha256:1f2c6209a8917c525c1e2b55a716135ca4658a3042b5122d4e3413a4030c26ce",
+ "sha256:2f06d97f0ca0f414f6b707c974aaf8829c2292c1c497642f63824119d770226f",
+ "sha256:616c94f8176808f4018b39f9638080ed86f96b55370b5a9463b2ee5c926f6c5f",
+ "sha256:63b91e30ef47ef68a30f0c3c278fbfe9822319c15f34b7538a829515b84ca2a0",
+ "sha256:77b454f03860b844f758c5d5c6e5f18d27de899a3db367f4af06bec2e6013a8e",
+ "sha256:83fe27ba321e4cfac466178606147d3c0aa18e8087507caec78ed5a966a64905",
+ "sha256:84742532d39f72df959d237912344d8a1764c2d03fe58beba96a87bfa11a76d8",
+ "sha256:874ebf3caaf55a020aeb08acead813baf5a305927a71ce88c9377970fe7ad3c2",
+ "sha256:9f5caf2c7436d44f3cec97c2fa7791f8a675170badbfa86e1992ca1b84c37009",
+ "sha256:a0c8758d01fcdfe7ae8e4b4017b13552efa7f1197dd7358dc9da0576f9d0328a",
+ "sha256:a4def978d9d28cda2d960c279318d46b327632686d82b4917516c36d4c274512",
+ "sha256:ad4f4be843dace866af5fc142509e9b9817ca0c59342fdb176ab6ad552c927f5",
+ "sha256:ae33dd198f772f714420c5ab698ff05ff900150486c648d29951e9c70694338e",
+ "sha256:b4a2b782b8a8c5522ad35c93e04d60e2ba7f7dcb9271ec8e8c3e08239be6c7b4",
+ "sha256:c462eb33f6abca3b34cdedbe84d761f31a60b814e173b98ede3c81bb48967c4f",
+ "sha256:fd135b8d35dfdcdb984828c84d695937e58cc5f49e1c854eb311c4d6aa03f4f1"
+ ],
+ "version": "==1.4.2"
},
"mccabe": {
"hashes": [
diff --git a/bot.py b/bot.py
index 62803d525d..bea7adadbb 100644
--- a/bot.py
+++ b/bot.py
@@ -1152,8 +1152,9 @@ async def post_metadata(self):
logger.debug("Uploading metadata to Modmail server.")
async def before_post_metadata(self):
- logger.info("Starting metadata loop.")
await self.wait_for_connected()
+ logger.debug("Starting metadata loop.")
+ logger.line()
if not self.guild:
self.metadata_loop.cancel()
diff --git a/cogs/utility.py b/cogs/utility.py
index c10f845e72..8f0986f06b 100644
--- a/cogs/utility.py
+++ b/cogs/utility.py
@@ -652,8 +652,8 @@ async def loop_presence(self):
@loop_presence.before_loop
async def before_loop_presence(self):
- logger.info("Starting metadata loop.")
await self.bot.wait_for_connected()
+ logger.debug("Starting metadata loop.")
logger.line()
presence = await self.set_presence()
logger.info(presence["activity"][1])
From cd063609edf21853e2208d712ea895e9f6c28d6c Mon Sep 17 00:00:00 2001
From: Taaku18 <45324516+Taaku18@users.noreply.github.com>
Date: Fri, 13 Sep 2019 20:38:50 -0700
Subject: [PATCH 0393/1221] Bump version v3.2.0
---
CHANGELOG.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 6129498119..bf0820128e 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -6,7 +6,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
This project mostly adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html);
however, insignificant breaking changes does not guarantee a major version bump, see the reasoning [here](https://github.com/kyb3r/modmail/issues/319).
-# UNRELEASED
+# v3.2.0
### Added
From 56da633a255b6db80ffe0df7727d5c0b7fdc1305 Mon Sep 17 00:00:00 2001
From: Taaku18 <45324516+Taaku18@users.noreply.github.com>
Date: Fri, 13 Sep 2019 21:29:09 -0700
Subject: [PATCH 0394/1221] Fixed color hex issue
---
CHANGELOG.md | 10 ++++++++++
bot.py | 2 +-
core/_color_data.py | 32 ++++++++++++++++++++++++++++++++
core/config.py | 25 +++++++++++++++++--------
4 files changed, 60 insertions(+), 9 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index bf0820128e..7d601f839e 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -6,6 +6,16 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
This project mostly adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html);
however, insignificant breaking changes does not guarantee a major version bump, see the reasoning [here](https://github.com/kyb3r/modmail/issues/319).
+# v3.2.1
+
+### Fixed
+
+- Can't set hex for main_color, recipient_color, etc.
+
+### Added
+
+- Discord colors by default when addressing them by names.
+
# v3.2.0
### Added
diff --git a/bot.py b/bot.py
index bea7adadbb..afe7a547cc 100644
--- a/bot.py
+++ b/bot.py
@@ -1,4 +1,4 @@
-__version__ = "3.2.0"
+__version__ = "3.2.1"
import asyncio
import logging
diff --git a/core/_color_data.py b/core/_color_data.py
index 13346e903e..997a6a1d25 100644
--- a/core/_color_data.py
+++ b/core/_color_data.py
@@ -15,6 +15,36 @@
"w": "#ffffff",
}
+# Discord native colors
+DISCORD_COLORS = {
+ "default": "#000000",
+ "teal": "#1abc9c",
+ "dark teal": "#11806a",
+ "green": "#2ecc71",
+ "dark green": "#1f8b4c",
+ "blue": "#3498db",
+ "dark blue": "#206694",
+ "purple": "#9b59b6",
+ "dark purple": "#71368a",
+ "magenta": "#e91e63",
+ "dark magenta": "#ad1457",
+ "gold": "#f1c40f",
+ "dark gold": "#c27c0e",
+ "orange": "#e67e22",
+ "dark orange": "#a84300",
+ "red": "#e74c3c",
+ "dark red": "#992d22",
+ "lighter gray": "#95a5a6",
+ "darker gray": "#546e7a",
+ "light gray": "#979c9f",
+ "dark gray": "#607d8b",
+ "blurple": "#7289da",
+ "grayple": "#99aab5"
+}
+
+# Normalize name to "discord:" to avoid name collisions.
+DISCORD_COLORS_NORM = {"discord:" + name: value for name, value in DISCORD_COLORS.items()}
+
# These colors are from Tableau
TABLEAU_COLORS = {
@@ -1160,3 +1190,5 @@
ALL_COLORS.update(XKCD_COLORS_NORM)
ALL_COLORS.update(TABLEAU_COLORS)
ALL_COLORS.update(TABLEAU_COLORS_NORM)
+ALL_COLORS.update(DISCORD_COLORS)
+ALL_COLORS.update(DISCORD_COLORS_NORM)
diff --git a/core/config.py b/core/config.py
index b2109a94da..3c8393d732 100644
--- a/core/config.py
+++ b/core/config.py
@@ -2,6 +2,7 @@
import json
import logging
import os
+import re
import typing
from copy import deepcopy
@@ -168,10 +169,9 @@ async def clean_data(self, key: str, val: typing.Any) -> typing.Tuple[str, str]:
# when setting a color
if key in self.colors:
- hex_ = ALL_COLORS.get(val)
+ try:
+ hex_ = str(val)
- if hex_ is None:
- hex_ = str(hex_)
if hex_.startswith("#"):
hex_ = hex_[1:]
if len(hex_) == 3:
@@ -179,14 +179,23 @@ async def clean_data(self, key: str, val: typing.Any) -> typing.Tuple[str, str]:
if len(hex_) != 6:
raise InvalidConfigError("Invalid color name or hex.")
try:
- int(val, 16)
+ int(hex_, 16)
except ValueError:
raise InvalidConfigError("Invalid color name or hex.")
- clean_value = "#" + val
- value_text = clean_value
- else:
+ hex_ = "#" + hex_
+ value_text = clean_value = hex_
+
+ except InvalidConfigError:
+ name = re.sub(r"[\-. ]+", " ", str(val))
+ hex_ = ALL_COLORS.get(name)
+ if hex_ is None:
+ name = re.sub(r"\W", "", str(val))
+ hex_ = ALL_COLORS.get(name)
+ if hex_ is None:
+ raise
+
clean_value = hex_
- value_text = f"{val} ({clean_value})"
+ value_text = f"{hex_} ({clean_value})"
elif key in self.time_deltas:
try:
From f94a81285071bc48c1117f440f56cbdd372d8256 Mon Sep 17 00:00:00 2001
From: Taaku18 <45324516+Taaku18@users.noreply.github.com>
Date: Fri, 13 Sep 2019 21:32:21 -0700
Subject: [PATCH 0395/1221] Small typo
---
core/config.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/core/config.py b/core/config.py
index 3c8393d732..57944c15e6 100644
--- a/core/config.py
+++ b/core/config.py
@@ -195,7 +195,7 @@ async def clean_data(self, key: str, val: typing.Any) -> typing.Tuple[str, str]:
raise
clean_value = hex_
- value_text = f"{hex_} ({clean_value})"
+ value_text = f"{name} ({clean_value})"
elif key in self.time_deltas:
try:
From 62847444dca539c6f25c59e88d63df90b3f76d31 Mon Sep 17 00:00:00 2001
From: Taaku18 <45324516+Taaku18@users.noreply.github.com>
Date: Fri, 13 Sep 2019 21:37:04 -0700
Subject: [PATCH 0396/1221] Small typo
---
core/config.py | 5 +++--
1 file changed, 3 insertions(+), 2 deletions(-)
diff --git a/core/config.py b/core/config.py
index 57944c15e6..ad1a42d2f9 100644
--- a/core/config.py
+++ b/core/config.py
@@ -186,10 +186,11 @@ async def clean_data(self, key: str, val: typing.Any) -> typing.Tuple[str, str]:
value_text = clean_value = hex_
except InvalidConfigError:
- name = re.sub(r"[\-. ]+", " ", str(val))
+ name = str(val).lower()
+ name = re.sub(r"[\-+|. ]+", " ", name)
hex_ = ALL_COLORS.get(name)
if hex_ is None:
- name = re.sub(r"\W", "", str(val))
+ name = re.sub(r"[\-+|. ]+", "", name)
hex_ = ALL_COLORS.get(name)
if hex_ is None:
raise
From 6faf4c310a321509b2ce063bbeb5d6e3c540b6a8 Mon Sep 17 00:00:00 2001
From: Taaku18 <45324516+Taaku18@users.noreply.github.com>
Date: Sat, 14 Sep 2019 00:29:08 -0700
Subject: [PATCH 0397/1221] Added utf 8 encoding to config
---
bot.py | 4 +++-
core/config.py | 2 +-
2 files changed, 4 insertions(+), 2 deletions(-)
diff --git a/bot.py b/bot.py
index afe7a547cc..9642cce2b3 100644
--- a/bot.py
+++ b/bot.py
@@ -294,6 +294,8 @@ def guild_id(self) -> typing.Optional[int]:
except ValueError:
self.config.remove("guild_id")
logger.critical("Invalid GUILD_ID set.")
+ else:
+ logger.debug("No GUILD_ID set.")
return None
@property
@@ -456,7 +458,7 @@ async def on_ready(self):
await self.wait_for_connected()
if self.guild is None:
- logger.debug("Logging out due to invalid GUILD_ID.")
+ logger.error("Logging out due to invalid GUILD_ID.")
return await self.logout()
logger.line()
diff --git a/core/config.py b/core/config.py
index ad1a42d2f9..ed9adead23 100644
--- a/core/config.py
+++ b/core/config.py
@@ -139,7 +139,7 @@ def populate_cache(self) -> dict:
)
if os.path.exists(config_json):
logger.debug("Loading envs from config.json.")
- with open(config_json, "r") as f:
+ with open(config_json, "r", encoding="utf-8") as f:
# Config json should override env vars
try:
data.update(
From e3d0f3e9db9aaa593c3120d0feff6dbf7abb1496 Mon Sep 17 00:00:00 2001
From: Taaku18 <45324516+Taaku18@users.noreply.github.com>
Date: Sun, 15 Sep 2019 22:33:53 -0700
Subject: [PATCH 0398/1221] Update security 3.2.2
---
CHANGELOG.md | 12 ++++++++++++
bot.py | 9 +++++----
core/models.py | 32 ++++++++++++++++++++++++++++++++
core/thread.py | 7 ++++---
4 files changed, 53 insertions(+), 7 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 7d601f839e..d9532ce987 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -6,6 +6,18 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
This project mostly adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html);
however, insignificant breaking changes does not guarantee a major version bump, see the reasoning [here](https://github.com/kyb3r/modmail/issues/319).
+# v3.2.2
+
+Security update!
+
+### Important
+
+- Supporter permission users used to be able to "hack" snippets to reveal all your config vars, including your token and MongoURI.
+- Implemented some changes to address this bug:
+ - All customizable variables used in snippets, close messages, etc, using the `{}` syntax, now forbids chaining 2 or more attributes and attributes that starts with `_`.
+- It is advised to update to this version.
+- If you felt your credentials have been leaked, consider changing your bot token / mongo uri.
+
# v3.2.1
### Fixed
diff --git a/bot.py b/bot.py
index 9642cce2b3..a25777160d 100644
--- a/bot.py
+++ b/bot.py
@@ -1,4 +1,4 @@
-__version__ = "3.2.1"
+__version__ = "3.2.2"
import asyncio
import logging
@@ -33,7 +33,7 @@
from core.clients import ApiClient, PluginDatabaseClient
from core.config import ConfigManager
from core.utils import human_join, strtobool, parse_alias
-from core.models import PermissionLevel, ModmailLogger
+from core.models import PermissionLevel, ModmailLogger, SafeFormatter
from core.thread import ThreadManager
from core.time import human_timedelta
@@ -69,6 +69,7 @@ def __init__(self):
self._session = None
self._api = None
self.metadata_loop = None
+ self.formatter = SafeFormatter()
self._connected = asyncio.Event()
self.start_time = datetime.utcnow()
@@ -120,7 +121,7 @@ def uptime(self) -> str:
if days:
fmt = "{d}d " + fmt
- return fmt.format(d=days, h=hours, m=minutes, s=seconds)
+ return self.formatter.format(fmt, d=days, h=hours, m=minutes, s=seconds)
def _configure_logging(self):
level_text = self.config["log_level"].upper()
@@ -837,7 +838,7 @@ async def process_commands(self, message):
thread = await self.threads.find(channel=message.channel)
snippet = self.snippets[cmd]
if thread:
- snippet = snippet.format(recipient=thread.recipient)
+ snippet = self.formatter.format(snippet, recipient=thread.recipient)
message.content = f"{self.prefix}reply {snippet}"
ctxs = await self.get_contexts(message)
diff --git a/core/models.py b/core/models.py
index 58ef344115..8b21c10eb0 100644
--- a/core/models.py
+++ b/core/models.py
@@ -1,5 +1,7 @@
+import _string
import logging
from enum import IntEnum
+from string import Formatter
from discord import Color, Embed
from discord.ext import commands
@@ -85,3 +87,33 @@ class _Default:
Default = _Default()
+
+
+class SafeFormatter(Formatter):
+
+ def get_field(self, field_name, args, kwargs):
+ first, rest = _string.formatter_field_name_split(field_name)
+
+ try:
+ obj = self.get_value(first, args, kwargs)
+ except (IndexError, KeyError):
+ return "", first
+
+ # loop through the rest of the field_name, doing
+ # getattr or getitem as needed
+ # stops when reaches the depth of 2 or starts with _.
+ try:
+ for n, (is_attr, i) in enumerate(rest):
+ if n >= 2:
+ break
+ if is_attr:
+ if str(i).startswith('_'):
+ break
+ obj = getattr(obj, i)
+ else:
+ obj = obj[i]
+ else:
+ return obj, first
+ except (IndexError, KeyError):
+ pass
+ return "", first
diff --git a/core/thread.py b/core/thread.py
index b1ad46086d..bdf0bc7aa2 100644
--- a/core/thread.py
+++ b/core/thread.py
@@ -393,8 +393,8 @@ async def _close(
else:
message = self.bot.config["thread_close_response"]
- message = message.format(
- closer=closer, loglink=log_url, logkey=log_data["key"] if log_data else None
+ message = self.bot.formatter.format(
+ message, closer=closer, loglink=log_url, logkey=log_data["key"] if log_data else None
)
embed.description = message
@@ -492,7 +492,8 @@ async def _restart_close_timer(self):
)
# Grab message
- close_message = self.bot.config["thread_auto_close_response"].format(
+ close_message = self.bot.formatter.format(
+ self.bot.config["thread_auto_close_response"],
timeout=human_time
)
From 9ec5cb79c2d154bf0b827894556435db509655ec Mon Sep 17 00:00:00 2001
From: DAzVise <52792999+DAzVise@users.noreply.github.com>
Date: Tue, 17 Sep 2019 14:04:16 +0300
Subject: [PATCH 0399/1221] Update modmail.py
---
cogs/modmail.py | 8 ++++++--
1 file changed, 6 insertions(+), 2 deletions(-)
diff --git a/cogs/modmail.py b/cogs/modmail.py
index f8246e17e2..a407232182 100644
--- a/cogs/modmail.py
+++ b/cogs/modmail.py
@@ -894,12 +894,16 @@ async def contact(
await thread.wait_until_ready()
embed = discord.Embed(
title="Created thread",
- description=f"Thread started in {thread.channel.mention} "
+ description=f"Thread started by {ctx.author.mention} "
f"for {user.mention}.",
color=self.bot.main_color,
)
- await ctx.send(embed=embed)
+ try:
+ await thread.channel.send(embed=embed)
+ except:
+ await ctx.send(embed=embed)
+ await ctx.message.delete()
@commands.group(invoke_without_command=True)
@checks.has_permissions(PermissionLevel.MODERATOR)
From 1340d733d51c7c0f57f8091d1a587d2aa794aa2c Mon Sep 17 00:00:00 2001
From: Taaku18 <45324516+Taaku18@users.noreply.github.com>
Date: Tue, 17 Sep 2019 20:36:55 -0700
Subject: [PATCH 0400/1221] Added changelog for v3.2.3-pre and slighty changed
contact logic
---
CHANGELOG.md | 7 +++++++
bot.py | 2 +-
cogs/modmail.py | 11 ++++-------
3 files changed, 12 insertions(+), 8 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index d9532ce987..1f6c4c92da 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -6,6 +6,13 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
This project mostly adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html);
however, insignificant breaking changes does not guarantee a major version bump, see the reasoning [here](https://github.com/kyb3r/modmail/issues/319).
+
+# [UNRELEASED]
+
+### Changed
+
+- `?contact` no longer send the "thread created" message to where the command is ran, instead, it's now sent to the newly created thread channel. (Thanks to DAzVise)
+
# v3.2.2
Security update!
diff --git a/bot.py b/bot.py
index a25777160d..4d7014c4d2 100644
--- a/bot.py
+++ b/bot.py
@@ -1,4 +1,4 @@
-__version__ = "3.2.2"
+__version__ = "3.2.3-pre"
import asyncio
import logging
diff --git a/cogs/modmail.py b/cogs/modmail.py
index a407232182..1ccdcc4a5a 100644
--- a/cogs/modmail.py
+++ b/cogs/modmail.py
@@ -886,24 +886,21 @@ async def contact(
description="A thread for this user already "
f"exists in {exists.channel.mention}.",
)
+ await ctx.channel.send(embed=embed)
else:
thread = self.bot.threads.create(
user, creator=ctx.author, category=category
)
- await thread.wait_until_ready()
embed = discord.Embed(
- title="Created thread",
+ title="Created Thread",
description=f"Thread started by {ctx.author.mention} "
f"for {user.mention}.",
color=self.bot.main_color,
)
-
- try:
+ await thread.wait_until_ready()
await thread.channel.send(embed=embed)
- except:
- await ctx.send(embed=embed)
- await ctx.message.delete()
+ await ctx.message.delete()
@commands.group(invoke_without_command=True)
@checks.has_permissions(PermissionLevel.MODERATOR)
From c236b609ec59b108139a57cacf24696f207c2e4c Mon Sep 17 00:00:00 2001
From: Taaku18 <45324516+Taaku18@users.noreply.github.com>
Date: Thu, 19 Sep 2019 21:14:35 -0700
Subject: [PATCH 0401/1221] 3.3.0-dev
---
CHANGELOG.md | 13 +
Dockerfile | 1 -
bot.py | 132 +--
cogs/modmail.py | 79 +-
cogs/plugins.py | 688 +++++++------
cogs/utility.py | 78 +-
core/_color_data.py | 2276 ++++++++++++++++++++---------------------
core/changelog.py | 27 +-
core/config.py | 157 +--
core/config_help.json | 9 +
core/thread.py | 35 +-
plugins/registry.json | 2 +-
12 files changed, 1753 insertions(+), 1744 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 1f6c4c92da..738f7a3927 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -12,6 +12,19 @@ however, insignificant breaking changes does not guarantee a major version bump,
### Changed
- `?contact` no longer send the "thread created" message to where the command is ran, instead, it's now sent to the newly created thread channel. (Thanks to DAzVise)
+- Plugins update (mostly internal).
+ - `git` is no longer used to install plugins, it now downloads through zip files.
+ - `?plugins enabled` renamed to `?plugins loaded` while `enabled` is still an alias to that command.
+ - Reorganised plugins folder structure.
+ - Logging / plugin-related messages changes.
+ - Updating one plugin will not update all other plugins (plugins are no longer separated by repos, but the plugin name itself).
+- Help command is in alphabetical order grouped by permissions.
+
+### Internal
+
+- Reworked `config.get` and `config.set`, it feeds through the converters before setting/getting.
+ - To get/set the raw value, access through `config[]`.
+- Prerelease naming scheme is now `x.x.x-dev`.
# v3.2.2
diff --git a/Dockerfile b/Dockerfile
index 9735f92d08..eadf05e4e2 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,5 +1,4 @@
FROM python:3.7.4-alpine
-RUN apk add --no-cache git
WORKDIR /modmailbot
COPY . /modmailbot
RUN pip install --no-cache-dir -r requirements.min.txt
diff --git a/bot.py b/bot.py
index 4d7014c4d2..9108ddf583 100644
--- a/bot.py
+++ b/bot.py
@@ -1,4 +1,4 @@
-__version__ = "3.2.3-pre"
+__version__ = "3.3.0-dev"
import asyncio
import logging
@@ -19,6 +19,7 @@
from aiohttp import ClientSession
from emoji import UNICODE_EMOJI
from motor.motor_asyncio import AsyncIOMotorClient
+from pkg_resources import parse_version
from pymongo.errors import ConfigurationError
try:
@@ -32,7 +33,7 @@
from core import checks
from core.clients import ApiClient, PluginDatabaseClient
from core.config import ConfigManager
-from core.utils import human_join, strtobool, parse_alias
+from core.utils import human_join, parse_alias
from core.models import PermissionLevel, ModmailLogger, SafeFormatter
from core.thread import ThreadManager
from core.time import human_timedelta
@@ -70,6 +71,7 @@ def __init__(self):
self._api = None
self.metadata_loop = None
self.formatter = SafeFormatter()
+ self.loaded_cogs = ['cogs.modmail', 'cogs.plugins', 'cogs.utility']
self._connected = asyncio.Event()
self.start_time = datetime.utcnow()
@@ -97,17 +99,7 @@ def __init__(self):
sys.exit(0)
self.plugin_db = PluginDatabaseClient(self)
-
- logger.line()
- logger.info("┌┬┐┌─┐┌┬┐┌┬┐┌─┐┬┬")
- logger.info("││││ │ │││││├─┤││")
- logger.info("┴ ┴└─┘─┴┘┴ ┴┴ ┴┴┴─┘")
- logger.info("v%s", __version__)
- logger.info("Authors: kyb3r, fourjr, Taaku18")
- logger.line()
-
- self._load_extensions()
- logger.line()
+ self.startup()
@property
def uptime(self) -> str:
@@ -123,6 +115,24 @@ def uptime(self) -> str:
return self.formatter.format(fmt, d=days, h=hours, m=minutes, s=seconds)
+ def startup(self):
+ logger.line()
+ logger.info("┌┬┐┌─┐┌┬┐┌┬┐┌─┐┬┬")
+ logger.info("││││ │ │││││├─┤││")
+ logger.info("┴ ┴└─┘─┴┘┴ ┴┴ ┴┴┴─┘")
+ logger.info("v%s", __version__)
+ logger.info("Authors: kyb3r, fourjr, Taaku18")
+ logger.line()
+
+ for cog in self.loaded_cogs:
+ logger.info("Loading %s.", cog)
+ try:
+ self.load_extension(cog)
+ logger.info("Successfully loaded %s.", cog)
+ except Exception:
+ logger.exception("Failed to load %s.", cog)
+ logger.line()
+
def _configure_logging(self):
level_text = self.config["log_level"].upper()
logging_levels = {
@@ -161,8 +171,8 @@ def _configure_logging(self):
logger.debug("Successfully configured logging.")
@property
- def version(self) -> str:
- return __version__
+ def version(self):
+ return parse_version(__version__)
@property
def session(self) -> ClientSession:
@@ -179,18 +189,6 @@ def api(self):
async def get_prefix(self, message=None):
return [self.prefix, f"<@{self.user.id}> ", f"<@!{self.user.id}> "]
- def _load_extensions(self):
- """Adds commands automatically"""
- for file in os.listdir("cogs"):
- if not file.endswith(".py"):
- continue
- cog = f"cogs.{file[:-3]}"
- logger.info("Loading %s.", cog)
- try:
- self.load_extension(cog)
- except Exception:
- logger.exception("Failed to load %s.", cog)
-
def run(self, *args, **kwargs):
try:
self.loop.run_until_complete(self.start(self.token))
@@ -366,25 +364,17 @@ def blocked_whitelisted_users(self) -> typing.List[str]:
def prefix(self) -> str:
return str(self.config["prefix"])
- def _parse_color(self, conf_name):
- color = self.config[conf_name]
- try:
- return int(color.lstrip("#"), base=16)
- except ValueError:
- logger.error("Invalid %s provided.", conf_name)
- return int(self.config.remove(conf_name).lstrip("#"), base=16)
-
@property
def mod_color(self) -> int:
- return self._parse_color("mod_color")
+ return self.config.get("mod_color")
@property
def recipient_color(self) -> int:
- return self._parse_color("recipient_color")
+ return self.config.get("recipient_color")
@property
def main_color(self) -> int:
- return self._parse_color("main_color")
+ return self.config.get("main_color")
def command_perm(self, command_name: str) -> PermissionLevel:
level = self.config["override_command_level"].get(command_name)
@@ -518,7 +508,6 @@ async def on_ready(self):
loop=None,
)
self.metadata_loop.before_loop(self.before_post_metadata)
- self.metadata_loop.after_loop(self.after_post_metadata)
self.metadata_loop.start()
async def convert_emoji(self, name: str) -> str:
@@ -574,38 +563,14 @@ async def _process_blocked(self, message: discord.Message) -> bool:
now = datetime.utcnow()
- account_age = self.config["account_age"]
- guild_age = self.config["guild_age"]
+ account_age = self.config.get("account_age")
+ guild_age = self.config.get("guild_age")
if account_age is None:
account_age = isodate.Duration()
if guild_age is None:
guild_age = isodate.Duration()
- if not isinstance(account_age, isodate.Duration):
- try:
- account_age = isodate.parse_duration(account_age)
- except isodate.ISO8601Error:
- logger.warning(
- "The account age limit needs to be a "
- "ISO-8601 duration formatted duration string "
- 'greater than 0 days, not "%s".',
- str(account_age),
- )
- account_age = self.config.remove("account_age")
-
- if not isinstance(guild_age, isodate.Duration):
- try:
- guild_age = isodate.parse_duration(guild_age)
- except isodate.ISO8601Error:
- logger.warning(
- "The guild join age limit needs to be a "
- "ISO-8601 duration formatted duration string "
- 'greater than 0 days, not "%s".',
- str(guild_age),
- )
- guild_age = self.config.remove("guild_age")
-
reason = self.blocked_users.get(str(message.author.id)) or ""
min_guild_age = min_account_age = now
@@ -860,14 +825,7 @@ async def process_commands(self, message):
thread = await self.threads.find(channel=ctx.channel)
if thread is not None:
- try:
- reply_without_command = strtobool(
- self.config["reply_without_command"]
- )
- except ValueError:
- reply_without_command = self.config.remove("reply_without_command")
-
- if reply_without_command:
+ if self.config.get('reply_without_command'):
await thread.reply(message)
else:
await self.api.append_log(message, type_="internal")
@@ -887,11 +845,7 @@ async def _void(*_args, **_kwargs):
pass
if isinstance(channel, discord.DMChannel):
- try:
- user_typing = strtobool(self.config["user_typing"])
- except ValueError:
- user_typing = self.config.remove("user_typing")
- if not user_typing:
+ if not self.config.get("user_typing"):
return
thread = await self.threads.find(recipient=user)
@@ -899,11 +853,7 @@ async def _void(*_args, **_kwargs):
if thread:
await thread.channel.trigger_typing()
else:
- try:
- mod_typing = strtobool(self.config["mod_typing"])
- except ValueError:
- mod_typing = self.config.remove("mod_typing")
- if not mod_typing:
+ if not self.config.get('mod_typing'):
return
thread = await self.threads.find(channel=channel)
@@ -941,15 +891,7 @@ async def on_raw_reaction_add(self, payload):
if isinstance(channel, discord.DMChannel):
if str(reaction) == str(close_emoji): # closing thread
- try:
- recipient_thread_close = strtobool(
- self.config["recipient_thread_close"]
- )
- except ValueError:
- recipient_thread_close = self.config.remove(
- "recipient_thread_close"
- )
- if not recipient_thread_close:
+ if not self.config.get('recipient_thread_close'):
return
thread = await self.threads.find(recipient=user)
ts = message.embeds[0].timestamp if message.embeds else None
@@ -1146,7 +1088,7 @@ async def post_metadata(self):
"member_count": len(self.guild.members),
"uptime": (datetime.utcnow() - self.start_time).total_seconds(),
"latency": f"{self.ws.latency * 1000:.4f}",
- "version": self.version,
+ "version": str(self.version),
"selfhosted": True,
"last_updated": str(datetime.utcnow()),
}
@@ -1161,10 +1103,6 @@ async def before_post_metadata(self):
if not self.guild:
self.metadata_loop.cancel()
- @staticmethod
- async def after_post_metadata():
- logger.info("Metadata loop has been cancelled.")
-
if __name__ == "__main__":
try:
diff --git a/cogs/modmail.py b/cogs/modmail.py
index 1ccdcc4a5a..f51c131b67 100644
--- a/cogs/modmail.py
+++ b/cogs/modmail.py
@@ -17,7 +17,7 @@
from core.models import PermissionLevel
from core.paginator import EmbedPaginatorSession
from core.time import UserFriendlyTime, human_timedelta
-from core.utils import format_preview, User, create_not_found_embed, format_description, strtobool
+from core.utils import format_preview, User, create_not_found_embed, format_description
logger = logging.getLogger("Modmail")
@@ -302,12 +302,7 @@ async def move(self, ctx, category: discord.CategoryChannel, *, specifics: str =
await thread.channel.edit(category=category, sync_permissions=True)
- try:
- thread_move_notify = strtobool(self.bot.config["thread_move_notify"])
- except ValueError:
- thread_move_notify = self.bot.config.remove("thread_move_notify")
-
- if thread_move_notify and not silent:
+ if self.bot.config('thread_move_notify') and not silent:
embed = discord.Embed(
title="Thread Moved",
description=self.bot.config["thread_move_response"],
@@ -398,6 +393,17 @@ async def close(self, ctx, *, after: UserFriendlyTime = None):
closer=ctx.author, after=close_after, message=message, silent=silent
)
+ @staticmethod
+ def parse_user_or_role(ctx, user_or_role):
+ mention = None
+ if user_or_role is None:
+ mention = ctx.author.mention
+ elif hasattr(user_or_role, "mention"):
+ mention = user_or_role.mention
+ elif user_or_role in {"here", "everyone", "@here", "@everyone"}:
+ mention = "@" + user_or_role.lstrip("@")
+ return mention
+
@commands.command(aliases=["alert"])
@checks.has_permissions(PermissionLevel.SUPPORTER)
@checks.thread_only()
@@ -413,17 +419,12 @@ async def notify(
`@here` and `@everyone` can be substituted with `here` and `everyone`.
`user_or_role` may be a user ID, mention, name. role ID, mention, name, "everyone", or "here".
"""
- thread = ctx.thread
-
- if user_or_role is None:
- mention = ctx.author.mention
- elif hasattr(user_or_role, "mention"):
- mention = user_or_role.mention
- elif user_or_role in {"here", "everyone", "@here", "@everyone"}:
- mention = "@" + user_or_role.lstrip("@")
- else:
+ mention = self.parse_user_or_role(ctx, user_or_role)
+ if mention is None:
raise commands.BadArgument(f"{user_or_role} is not a valid role.")
+ thread = ctx.thread
+
if str(thread.id) not in self.bot.config["notification_squad"]:
self.bot.config["notification_squad"][str(thread.id)] = []
@@ -457,17 +458,12 @@ async def unnotify(
`@here` and `@everyone` can be substituted with `here` and `everyone`.
`user_or_role` may be a user ID, mention, name, role ID, mention, name, "everyone", or "here".
"""
- thread = ctx.thread
-
- if user_or_role is None:
- mention = ctx.author.mention
- elif hasattr(user_or_role, "mention"):
- mention = user_or_role.mention
- elif user_or_role in {"here", "everyone", "@here", "@everyone"}:
- mention = "@" + user_or_role.lstrip("@")
- else:
+ mention = self.parse_user_or_role(ctx, user_or_role)
+ if mention is None:
mention = f"`{user_or_role}`"
+ thread = ctx.thread
+
if str(thread.id) not in self.bot.config["notification_squad"]:
self.bot.config["notification_squad"][str(thread.id)] = []
@@ -502,17 +498,12 @@ async def subscribe(
`@here` and `@everyone` can be substituted with `here` and `everyone`.
`user_or_role` may be a user ID, mention, name, role ID, mention, name, "everyone", or "here".
"""
- thread = ctx.thread
-
- if user_or_role is None:
- mention = ctx.author.mention
- elif hasattr(user_or_role, "mention"):
- mention = user_or_role.mention
- elif user_or_role in {"here", "everyone", "@here", "@everyone"}:
- mention = "@" + user_or_role.lstrip("@")
- else:
+ mention = self.parse_user_or_role(ctx, user_or_role)
+ if mention is None:
raise commands.BadArgument(f"{user_or_role} is not a valid role.")
+ thread = ctx.thread
+
if str(thread.id) not in self.bot.config["subscriptions"]:
self.bot.config["subscriptions"][str(thread.id)] = []
@@ -546,17 +537,12 @@ async def unsubscribe(
`@here` and `@everyone` can be substituted with `here` and `everyone`.
`user_or_role` may be a user ID, mention, name, role ID, mention, name, "everyone", or "here".
"""
- thread = ctx.thread
-
- if user_or_role is None:
- mention = ctx.author.mention
- elif hasattr(user_or_role, "mention"):
- mention = user_or_role.mention
- elif user_or_role in {"here", "everyone", "@here", "@everyone"}:
- mention = "@" + user_or_role.lstrip("@")
- else:
+ mention = self.parse_user_or_role(ctx, user_or_role)
+ if mention is None:
mention = f"`{user_or_role}`"
+ thread = ctx.thread
+
if str(thread.id) not in self.bot.config["subscriptions"]:
self.bot.config["subscriptions"][str(thread.id)] = []
@@ -854,7 +840,6 @@ async def edit(self, ctx, message_id: Optional[int] = None, *, message: str):
@commands.command()
@checks.has_permissions(PermissionLevel.SUPPORTER)
- @trigger_typing
async def contact(
self,
ctx,
@@ -900,6 +885,12 @@ async def contact(
)
await thread.wait_until_ready()
await thread.channel.send(embed=embed)
+ sent_emoji, _ = await self.bot.retrieve_emoji()
+ try:
+ await ctx.message.add_reaction(sent_emoji)
+ except (discord.HTTPException, discord.InvalidArgument):
+ pass
+ await asyncio.sleep(3)
await ctx.message.delete()
@commands.group(invoke_without_command=True)
diff --git a/cogs/plugins.py b/cogs/plugins.py
index 4ea76503be..e15b47e29b 100644
--- a/cogs/plugins.py
+++ b/cogs/plugins.py
@@ -1,15 +1,19 @@
import asyncio
-import importlib
+import io
import json
import logging
import os
import shutil
-import site
-import stat
-import subprocess
import sys
import typing
+import zipfile
+
+from importlib import invalidate_caches
from difflib import get_close_matches
+from pathlib import Path, PurePath
+from re import match
+from site import USER_SITE
+from subprocess import PIPE
import discord
from discord.ext import commands
@@ -18,14 +22,68 @@
from core import checks
from core.models import PermissionLevel
from core.paginator import EmbedPaginatorSession
+from core.utils import truncate
logger = logging.getLogger("Modmail")
-class DownloadError(Exception):
+class InvalidPluginError(commands.BadArgument):
pass
+class Plugin:
+ def __init__(self, user, repo, name, branch=None):
+ self.user = user
+ self.repo = repo
+ self.name = name
+ self.branch = branch if branch is not None else 'master'
+ self.url = f"https://github.com/{user}/{repo}/archive/{self.branch}.zip"
+ self.link = f"https://github.com/{user}/{repo}/tree/{self.branch}/{name}"
+
+ @property
+ def path(self):
+ return PurePath('plugins') / self.user / self.repo / f'{self.name}-{self.branch}'
+
+ @property
+ def abs_path(self):
+ return Path(__file__).absolute().parent.parent / self.path
+
+ @property
+ def cache_path(self):
+ return Path(__file__).absolute().parent.parent / 'temp' / \
+ 'plugins-cache' / f'{self.user}-{self.repo}-{self.branch}.zip'
+
+ @property
+ def ext_string(self):
+ return f'plugins.{self.user}.{self.repo}.{self.name}-{self.branch}.{self.name}'
+
+ def __str__(self):
+ return f'{self.user}/{self.repo}/{self.name}@{self.branch}'
+
+ def __lt__(self, other):
+ return self.name.lower() < other.name.lower()
+
+ @classmethod
+ def from_string(cls, s, strict=False):
+ if not strict:
+ m = match(r'^(.+?)/(.+?)/(.+?)(?:@(.+?))?$', s)
+ else:
+ m = match(r'^(.+?)/(.+?)/(.+?)@(.+?)$', s)
+ if m is not None:
+ return Plugin(*m.groups())
+ else:
+ raise InvalidPluginError('Cannot decipher %s.', s)
+
+ def __hash__(self):
+ return hash((self.user, self.repo, self.name, self.branch))
+
+ def __repr__(self):
+ return f''
+
+ def __eq__(self, other):
+ return isinstance(other, Plugin) and self.__str__() == other.__str__()
+
+
class Plugins(commands.Cog):
"""
Plugins expand Modmail functionality by allowing third-party addons.
@@ -39,7 +97,10 @@ class Plugins(commands.Cog):
def __init__(self, bot):
self.bot = bot
self.registry = {}
- self.bot.loop.create_task(self.download_initial_plugins())
+ self.loaded_plugins = set()
+ self._ready_event = asyncio.Event()
+
+ self.bot.loop.create_task(self.initial_load_plugins())
self.bot.loop.create_task(self.populate_registry())
async def populate_registry(self):
@@ -47,357 +108,344 @@ async def populate_registry(self):
async with self.bot.session.get(url) as resp:
self.registry = json.loads(await resp.text())
- @staticmethod
- def _asubprocess_run(cmd):
- return subprocess.run(cmd, shell=True, check=True, capture_output=True)
+ async def initial_load_plugins(self):
+ await self.bot.wait_for_connected()
- @staticmethod
- def parse_plugin(name):
- # returns: (username, repo, plugin_name, branch)
- # default branch = master
- try:
- # when names are formatted with inline code
- result = name.split("/")
- result[2] = "/".join(result[2:])
- if "@" in result[2]:
- # branch is specified
- # for example, fourjr/modmail-plugins/welcomer@develop is a valid name
- branch_split_result = result[2].split("@")
- result.append(branch_split_result[-1])
- result[2] = "@".join(branch_split_result[:-1])
- else:
- result.append("master")
+ for plugin_name in list(self.bot.config["plugins"]):
+ try:
+ plugin = Plugin.from_string(plugin_name, strict=True)
+ except InvalidPluginError:
+ self.bot.config["plugins"].remove(plugin_name)
+ try:
+ # For backwards compat
+ plugin = Plugin.from_string(plugin_name)
+ except InvalidPluginError:
+ logger.error("Failed to parse plugin name: %s.", plugin_name, exc_info=True)
+ continue
- except IndexError:
- return None
+ logger.info("Migrated legacy plugin name: %s, now %s.", plugin_name, str(plugin))
+ self.bot.config["plugins"].append(str(plugin))
- return tuple(result)
+ try:
+ await self.download_plugin(plugin)
+ await self.load_plugin(plugin)
+ except Exception:
+ logger.error("Error when loading plugin %s.", plugin, exc_info=True)
+ continue
- async def download_initial_plugins(self):
- await self.bot.wait_for_connected()
+ logger.info("Finished loading all plugins.")
+ self._ready_event.set()
+ await self.bot.config.update()
- for i in self.bot.config["plugins"]:
- username, repo, name, branch = self.parse_plugin(i)
+ async def download_plugin(self, plugin, force=False):
+ if plugin.abs_path.exists() and not force:
+ return
- try:
- await self.download_plugin_repo(username, repo, branch)
- except DownloadError as exc:
- msg = f"{username}/{repo}@{branch} - {exc}"
- logger.error(msg, exc_info=True)
- else:
- try:
- await self.load_plugin(username, repo, name, branch)
- except DownloadError as exc:
- msg = f"{username}/{repo}@{branch}[{name}] - {exc}"
- logger.error(msg, exc_info=True)
+ plugin.abs_path.mkdir(parents=True, exist_ok=True)
- async def download_plugin_repo(self, username, repo, branch):
- try:
- cmd = f"git clone https://github.com/{username}/{repo} "
- cmd += f"plugins/{username}-{repo}-{branch} "
- cmd += f"-b {branch} -q"
-
- await self.bot.loop.run_in_executor(None, self._asubprocess_run, cmd)
- # -q (quiet) so there's no terminal output unless there's an error
- except subprocess.CalledProcessError as exc:
- err = exc.stderr.decode("utf-8").strip()
-
- if not err.endswith("already exists and is not an empty directory."):
- # don't raise error if the plugin folder exists
- msg = f"Download Error: {username}/{repo}@{branch}"
- logger.error(msg)
- raise DownloadError(err) from exc
-
- async def load_plugin(self, username, repo, plugin_name, branch):
- ext = f"plugins.{username}-{repo}-{branch}.{plugin_name}.{plugin_name}"
- dirname = f"plugins/{username}-{repo}-{branch}/{plugin_name}"
-
- if "requirements.txt" in os.listdir(dirname):
+ if plugin.cache_path.exists() and not force:
+ plugin_io = plugin.cache_path.open('rb')
+ logger.debug('Loading cached %s.', plugin.cache_path)
+
+ else:
+ async with self.bot.session.get(plugin.url) as resp:
+ logger.debug('Downloading %s.', plugin.url)
+ raw = await resp.read()
+ plugin_io = io.BytesIO(raw)
+ if not plugin.cache_path.parent.exists():
+ plugin.cache_path.parent.mkdir(parents=True)
+
+ with plugin.cache_path.open('wb') as f:
+ f.write(raw)
+
+ with zipfile.ZipFile(plugin_io) as zipf:
+ for member in zipf.namelist():
+ path = PurePath(member)
+ if len(path.parts) >= 3 and path.parts[1] == plugin.name:
+ with zipf.open(member) as src, (plugin.abs_path / Path(*path.parts[2:])).open("wb") as dst:
+ shutil.copyfileobj(src, dst)
+
+ plugin_io.close()
+
+ async def load_plugin(self, plugin):
+ if not (plugin.abs_path / f'{plugin.name}.py').exists():
+ raise InvalidPluginError(f'{plugin.name}.py not found.')
+
+ req_txt = plugin.abs_path / 'requirements.txt'
+
+ if req_txt.exists():
# Install PIP requirements
venv = hasattr(sys, "real_prefix") # in a virtual env
- user_install = "--user" if not venv else ""
+ user_install = ' --user' if not venv else ''
+ proc = await asyncio.create_subprocess_shell(
+ f"{sys.executable} -m pip install --upgrade{user_install} -r {req_txt} -q -q",
+ stderr=PIPE,
+ stdout=PIPE
+ )
- try:
- if os.name == "nt": # Windows
- await self.bot.loop.run_in_executor(
- None,
- self._asubprocess_run,
- f"pip install -r {dirname}/requirements.txt {user_install} -q -q",
- )
- else:
- await self.bot.loop.run_in_executor(
- None,
- self._asubprocess_run,
- f"python3 -m pip install -U -r {dirname}/"
- f"requirements.txt {user_install} -q -q",
- )
- # -q -q (quiet)
- # so there's no terminal output unless there's an error
- except subprocess.CalledProcessError as exc:
- err = exc.stderr.decode("utf8").strip()
- logger.error("Error.", exc_info=True)
- if err:
- msg = f"Requirements Download Error: {username}/{repo}@{branch}[{plugin_name}]"
- logger.error(msg)
- raise DownloadError(
- f"Unable to download requirements: ```\n{err}\n```"
- ) from exc
- else:
- if not os.path.exists(site.USER_SITE):
- os.makedirs(site.USER_SITE)
+ logger.debug('Downloading requirements for %s.', plugin.ext_string)
+
+ stdout, stderr = await proc.communicate()
- sys.path.insert(0, site.USER_SITE)
+ if stdout:
+ logger.debug('[stdout]\n%s.', stdout.decode())
+
+ if stderr:
+ logger.debug('[stderr]\n%s.', stderr.decode())
+ logger.error("Failed to download requirements for %s.", plugin.ext_string, exc_info=True)
+ raise InvalidPluginError(
+ f"Unable to download requirements: ```\n{stderr.decode()}\n```"
+ )
+
+ if os.path.exists(USER_SITE):
+ sys.path.insert(0, USER_SITE)
- await asyncio.sleep(0.5)
try:
- self.bot.load_extension(ext)
+ self.bot.load_extension(plugin.ext_string)
+ logger.info('Loaded plugin: %s', plugin.ext_string)
+ self.loaded_plugins.add(plugin)
+
except commands.ExtensionError as exc:
- msg = f"Plugin Load Failure: {username}/{repo}@{branch}[{plugin_name}]"
- logger.error(msg, exc_info=True)
- raise DownloadError("Invalid plugin") from exc
+ logger.error('Plugin load failure: %s', plugin.ext_string, exc_info=True)
+ raise InvalidPluginError("Cannot load extension, plugin invalid.") from exc
+
+ async def parse_user_input(self, ctx, plugin_name, check_version=False):
+
+ if plugin_name in self.registry:
+ details = self.registry[plugin_name]
+ user, repo = details["repository"].split('/', maxsplit=1)
+ branch = details.get("branch")
+
+ if check_version:
+ required_version = details.get("bot_version", False)
+
+ if required_version and self.bot.version < parse_version(required_version):
+ embed = discord.Embed(
+ description="Your bot's version is too low. "
+ f"This plugin requires version `{required_version}`.",
+ color=discord.Color.red(),
+ )
+ await ctx.send(embed=embed)
+ return
+
+ plugin = Plugin(user, repo, plugin_name, branch)
+
else:
- msg = f"Loaded Plugin: {username}/{repo}@{branch}[{plugin_name}]"
- logger.info(msg)
+ try:
+ plugin = Plugin.from_string(plugin_name)
+ except InvalidPluginError:
+ embed = discord.Embed(
+ description="Invalid plugin name, double check the plugin name "
+ "or use one of the following formats: "
+ "username/repo/plugin, username/repo/plugin@branch.",
+ color=discord.Color.red(),
+ )
+ await ctx.send(embed=embed)
+ return
+ return plugin
- @commands.group(aliases=["plugins"], invoke_without_command=True)
+ @commands.group(aliases=["plugin"], invoke_without_command=True)
@checks.has_permissions(PermissionLevel.OWNER)
- async def plugin(self, ctx):
- """Plugin handler. Controls the plugins in the bot."""
+ async def plugins(self, ctx):
+ """
+ Manage plugins for Modmail.
+ """
await ctx.send_help(ctx.command)
- @plugin.command(name="add", aliases=["install"])
+ @plugins.command(name="add", aliases=["install", "load"])
@checks.has_permissions(PermissionLevel.OWNER)
- async def plugin_add(self, ctx, *, plugin_name: str):
- """Add a plugin."""
-
- if plugin_name in self.registry:
- details = self.registry[plugin_name]
- plugin_name = (
- details["repository"] + "/" + plugin_name + "@" + details["branch"]
- )
+ async def plugins_add(self, ctx, *, plugin_name: str):
+ """
+ Install a new plugin for the bot.
- required_version = details["bot_version"]
+ `plugin_name` can be the name of the plugin found in `{prefix}plugin registry`, or a direct reference to a GitHub hosted plugin (in the format `user/repo/name[@branch]`).
+ """
- if parse_version(self.bot.version) < parse_version(required_version):
- embed = discord.Embed(
- description=f"Your bot's version is too low. This plugin requires version `{required_version}`.",
- color=self.bot.main_color,
- )
- return await ctx.send(embed=embed)
+ plugin = await self.parse_user_input(ctx, plugin_name, check_version=True)
+ if plugin is None:
+ return
- if plugin_name in self.bot.config["plugins"]:
+ if str(plugin) in self.bot.config["plugins"]:
embed = discord.Embed(
description="This plugin is already installed.",
- color=self.bot.main_color,
+ color=discord.Color.red(),
)
return await ctx.send(embed=embed)
- if plugin_name in self.bot.cogs.keys():
+ if plugin.name in self.bot.cogs:
# another class with the same name
embed = discord.Embed(
- description="There's another cog installed with the same name.",
- color=self.bot.main_color,
+ description="Cannot install this plugin (dupe cog name).",
+ color=discord.Color.red(),
)
return await ctx.send(embed=embed)
embed = discord.Embed(
- description="Downloading this plugin...", color=self.bot.main_color
+ description=f"Starting to download plugin from {plugin.link}...",
+ color=self.bot.main_color
)
- await ctx.send(embed=embed)
+ msg = await ctx.send(embed=embed)
async with ctx.typing():
- if len(plugin_name.split("/")) >= 3:
- username, repo, name, branch = self.parse_plugin(plugin_name)
-
- try:
- await self.download_plugin_repo(username, repo, branch)
- except Exception as exc:
- if not isinstance(exc, DownloadError):
- logger.error(
- "Unknown error when adding a plugin:", exc_info=True
- )
- embed = discord.Embed(
- description=f"Unable to fetch this plugin from Github: `{exc}`.",
- color=self.bot.main_color,
- )
- return await ctx.send(embed=embed)
+ try:
+ await self.download_plugin(plugin, force=True)
+ except Exception:
+ logger.warning(f"Unable to download plugin %s.", plugin, exc_info=True)
- importlib.invalidate_caches()
+ embed = discord.Embed(
+ description=f"Failed to download plugin, check logs for error.",
+ color=discord.Color.red()
+ )
- try:
- await self.load_plugin(username, repo, name, branch)
- except Exception as exc:
- if not isinstance(exc, DownloadError):
- logger.error(
- "Unknown error when adding a plugin:", exc_info=True
- )
- embed = discord.Embed(
- description=f"Unable to load this plugin: `{exc}`.",
- color=self.bot.main_color,
- )
- return await ctx.send(embed=embed)
+ return await msg.edit(embed=embed)
- # if it makes it here, it has passed all checks and should
- # be entered into the config
+ invalidate_caches()
- self.bot.config["plugins"].append(plugin_name)
- await self.bot.config.update()
+ try:
+ await self.load_plugin(plugin)
+ except Exception:
+ logger.warning(f"Unable to load plugin %s.", plugin, exc_info=True)
embed = discord.Embed(
- description="The plugin is installed.\n"
- "*Friendly reminder, plugins have absolute control over your bot. "
- "Please only install plugins from developers you trust.*",
- color=self.bot.main_color,
- )
- await ctx.send(embed=embed)
- else:
- embed = discord.Embed(
- description="Invalid plugin name format: use plugin-name or "
- "username/repo/plugin or username/repo/plugin@branch.",
- color=self.bot.main_color,
+ description=f"Failed to download plugin, check logs for error.",
+ color=discord.Color.red()
)
- await ctx.send(embed=embed)
- @plugin.command(name="remove", aliases=["del", "delete"])
+ return await msg.edit(embed=embed)
+
+ self.bot.config["plugins"].append(str(plugin))
+ await self.bot.config.update()
+
+ embed = discord.Embed(
+ description="Successfully installed plugin.\n"
+ "*Friendly reminder, plugins have absolute control over your bot. "
+ "Please only install plugins from developers you trust.*",
+ color=self.bot.main_color,
+ )
+ return await msg.edit(embed=embed)
+
+ @plugins.command(name="remove", aliases=["del", "delete"])
@checks.has_permissions(PermissionLevel.OWNER)
- async def plugin_remove(self, ctx, *, plugin_name: str):
- """Remove a plugin."""
+ async def plugins_remove(self, ctx, *, plugin_name: str):
+ """
+ Remove an installed plugin of the bot.
- if plugin_name in self.registry:
- details = self.registry[plugin_name]
- plugin_name = (
- details["repository"] + "/" + plugin_name + "@" + details["branch"]
+ `plugin_name` can be the name of the plugin found in `{prefix}plugin registry`, or a direct reference
+ to a GitHub hosted plugin (in the format `user/repo/name[@branch]`).
+ """
+ plugin = await self.parse_user_input(ctx, plugin_name)
+ if plugin is None:
+ return
+
+ if str(plugin) not in self.bot.config["plugins"]:
+ embed = discord.Embed(
+ description="Plugin is not installed.",
+ color=discord.Color.red(),
)
+ return await ctx.send(embed=embed)
- if plugin_name in self.bot.config["plugins"]:
- username, repo, name, branch = self.parse_plugin(plugin_name)
- try:
- self.bot.unload_extension(
- f"plugins.{username}-{repo}-{branch}.{name}.{name}"
- )
- except commands.ExtensionNotLoaded:
- logger.error("Plugin was never loaded.")
+ try:
+ self.bot.unload_extension(plugin.ext_string)
+ self.loaded_plugins.remove(plugin)
+ except (commands.ExtensionNotLoaded, KeyError):
+ logger.warning("Plugin was never loaded.")
- self.bot.config["plugins"].remove(plugin_name)
+ self.bot.config["plugins"].remove(str(plugin))
+ await self.bot.config.update()
- try:
- if not any(
- i.startswith(f"{username}/{repo}")
- for i in self.bot.config["plugins"]
- ):
- # if there are no more of such repos, delete the folder
- def onerror(func, path, _):
- if not os.access(path, os.W_OK):
- # Is the error an access error?
- os.chmod(path, stat.S_IWUSR)
- func(path)
-
- shutil.rmtree(
- f"plugins/{username}-{repo}-{branch}", onerror=onerror
- )
- except Exception:
- logger.error("Failed to remove plugin %s.", plugin_name, exc_info=True)
- self.bot.config["plugins"].append(plugin_name)
- return
+ embed = discord.Embed(
+ description="The plugin is uninstalled and all its data is erased.",
+ color=self.bot.main_color,
+ )
+ await ctx.send(embed=embed)
- await self.bot.config.update()
+ @plugins.command(name="update")
+ @checks.has_permissions(PermissionLevel.OWNER)
+ async def plugins_update(self, ctx, *, plugin_name: str):
+ """
+ Update a plugin for the bot.
+
+ `plugin_name` can be the name of the plugin found in `{prefix}plugin registry`, or a direct reference
+ to a GitHub hosted plugin (in the format `user/repo/name[@branch]`).
+ """
+
+ plugin = await self.parse_user_input(ctx, plugin_name, check_version=True)
+ if plugin is None:
+ return
+ if str(plugin) not in self.bot.config["plugins"]:
embed = discord.Embed(
- description="The plugin is uninstalled and all its data is erased.",
- color=self.bot.main_color,
+ description="Plugin is not installed.",
+ color=discord.Color.red(),
)
- await ctx.send(embed=embed)
- else:
+ return await ctx.send(embed=embed)
+
+ async with ctx.typing():
+ await self.download_plugin(plugin, force=True)
+ try:
+ self.bot.unload_extension(plugin.ext_string)
+ except commands.ExtensionError:
+ logger.warning("Plugin unload fail.", exc_info=True)
+ await self.load_plugin(plugin)
+
embed = discord.Embed(
- description="That plugin is not installed.", color=self.bot.main_color
+ description=f"Successfully updated {plugin.name}.",
+ color=self.bot.main_color,
)
- await ctx.send(embed=embed)
+ return await ctx.send(embed=embed)
- @plugin.command(name="update")
+ @plugins.command(name="loaded", aliases=["enabled", "installed"])
@checks.has_permissions(PermissionLevel.OWNER)
- async def plugin_update(self, ctx, *, plugin_name: str):
- """Update a plugin."""
+ async def plugins_loaded(self, ctx):
+ """
+ Show a list of currently loaded plugins.
+ """
- if plugin_name in self.registry:
- details = self.registry[plugin_name]
- plugin_name = (
- details["repository"] + "/" + plugin_name + "@" + details["branch"]
+ if not self._ready_event.is_set():
+ embed = discord.Embed(
+ description="Plugins are still loading, please try again later.",
+ color=self.bot.main_color
)
+ return await ctx.send(embed=embed)
- if plugin_name not in self.bot.config["plugins"]:
+ if not self.loaded_plugins:
embed = discord.Embed(
- description="That plugin is not installed.", color=self.bot.main_color
+ description="There are no plugins currently loaded.",
+ color=discord.Color.red()
)
return await ctx.send(embed=embed)
- async with ctx.typing():
- username, repo, name, branch = self.parse_plugin(plugin_name)
-
- try:
- cmd = (
- f"cd plugins/{username}-{repo}-{branch} && "
- f"git reset --hard origin/{branch} && git fetch --all && git pull"
- )
- cmd = await self.bot.loop.run_in_executor(
- None, self._asubprocess_run, cmd
- )
- except subprocess.CalledProcessError as exc:
- err = exc.stderr.decode("utf8").strip()
-
- embed = discord.Embed(
- description=f"An error occurred while updating: {err}.",
- color=self.bot.main_color,
- )
- logger.error("An error occurred while updating plugin:", exc_info=True)
- await ctx.send(embed=embed)
-
+ loaded_plugins = map(str, sorted(self.loaded_plugins))
+ pages = ['```\n']
+ for plugin in loaded_plugins:
+ msg = str(plugin) + '\n'
+ if len(msg) + len(pages[-1]) + 3 <= 2048:
+ pages[-1] += msg
else:
- output = cmd.stdout.decode("utf8").strip()
+ pages[-1] += '```'
+ pages.append(f'```\n{msg}')
- embed = discord.Embed(
- description=f"```\n{output}\n```", color=self.bot.main_color
- )
- await ctx.send(embed=embed)
-
- if output != "Already up to date.":
- # repo was updated locally, now perform the cog reload
- ext = f"plugins.{username}-{repo}-{branch}.{name}.{name}"
- self.bot.unload_extension(ext)
-
- try:
- await self.load_plugin(username, repo, name, branch)
- except DownloadError as exc:
- embed = discord.Embed(
- description=f"Unable to start the plugin: `{exc}`.",
- color=self.bot.main_color,
- )
- logger.error(
- "An error occurred while updating plugin:", exc_info=True
- )
- await ctx.send(embed=embed)
-
- @plugin.command(name="enabled", aliases=["installed"])
- @checks.has_permissions(PermissionLevel.OWNER)
- async def plugin_enabled(self, ctx):
- """Shows a list of currently enabled plugins."""
+ if pages[-1][-3:] != '```':
+ pages[-1] += '```'
- if self.bot.config["plugins"]:
- msg = "```\n" + "\n".join(sorted(self.bot.config["plugins"])) + "\n```"
- embed = discord.Embed(description=msg, color=self.bot.main_color)
- await ctx.send(embed=embed)
- else:
+ embeds = []
+ for page in pages:
embed = discord.Embed(
- description="There are no plugins installed.", color=self.bot.main_color
+ title="Loaded plugins:",
+ description=page,
+ color=self.bot.main_color
)
- await ctx.send(embed=embed)
+ embeds.append(embed)
+ paginator = EmbedPaginatorSession(ctx, *embeds)
+ await paginator.run()
- @plugin.group(
+ @plugins.group(
invoke_without_command=True, name="registry", aliases=["list", "info"]
)
@checks.has_permissions(PermissionLevel.OWNER)
- async def plugin_registry(self, ctx, *, plugin_name: typing.Union[int, str] = None):
+ async def plugins_registry(self, ctx, *, plugin_name: typing.Union[int, str] = None):
"""
Shows a list of all approved plugins.
@@ -440,57 +488,95 @@ async def plugin_registry(self, ctx, *, plugin_name: typing.Union[int, str] = No
return await ctx.send(embed=embed)
- for name, details in registry:
- repo = f"https://github.com/{details['repository']}"
- url = f"{repo}/tree/master/{name}"
+ for plugin_name, details in registry:
+ details = self.registry[plugin_name]
+ user, repo = details["repository"].split('/', maxsplit=1)
+ branch = details.get("branch")
+
+ plugin = Plugin(user, repo, plugin_name, branch)
embed = discord.Embed(
color=self.bot.main_color,
description=details["description"],
- url=repo,
+ url=plugin.link,
title=details["repository"],
)
embed.add_field(
- name="Installation", value=f"```{self.bot.prefix}plugins add {name}```"
+ name="Installation", value=f"```{self.bot.prefix}plugins add {plugin_name}```"
)
embed.set_author(
- name=details["title"], icon_url=details.get("icon_url"), url=url
+ name=details["title"],
+ icon_url=details.get("icon_url"),
+ url=plugin.link
)
+
if details.get("thumbnail_url"):
embed.set_thumbnail(url=details.get("thumbnail_url"))
+
if details.get("image_url"):
embed.set_image(url=details.get("image_url"))
+ if plugin in self.loaded_plugins:
+ embed.set_footer(text="This plugin is currently loaded.")
+ else:
+ required_version = details.get("bot_version", False)
+ if required_version and self.bot.version < parse_version(required_version):
+ embed.set_footer(
+ text=f"Your bot is unable to install this plugin, "
+ f"minimum required version is v{required_version}.")
+ else:
+ embed.set_footer(text="Your bot is able to install this plugin.")
+
embeds.append(embed)
paginator = EmbedPaginatorSession(ctx, *embeds)
paginator.current = index
await paginator.run()
- @plugin_registry.command(name="compact")
+ @plugins_registry.command(name="compact", aliases=["slim"])
@checks.has_permissions(PermissionLevel.OWNER)
- async def plugin_registry_compact(self, ctx):
- """Shows a compact view of all plugins within the registry."""
+ async def plugins_registry_compact(self, ctx):
+ """
+ Shows a compact view of all plugins within the registry.
+ """
await self.populate_registry()
registry = sorted(self.registry.items(), key=lambda elem: elem[0])
- pages = [""]
-
- for name, details in registry:
- repo = f"https://github.com/{details['repository']}"
- url = f"{repo}/tree/{details['branch']}/{name}"
- desc = details["description"].replace("\n", "")
- fmt = f"[`{name}`]({url}) - {desc}"
- length = len(fmt) - len(url) - 4
- fmt = fmt[: 75 + len(url)].strip() + "..." if length > 75 else fmt
- if len(fmt) + len(pages[-1]) >= 2048:
- pages.append(fmt + "\n")
+ pages = ['']
+
+ for plugin_name, details in registry:
+ details = self.registry[plugin_name]
+ user, repo = details["repository"].split('/', maxsplit=1)
+ branch = details.get("branch")
+
+ plugin = Plugin(user, repo, plugin_name, branch)
+
+ desc = discord.utils.escape_markdown(details["description"].replace("\n", ""))
+
+ name = f"[`{plugin.name}`]({plugin.link})"
+ fmt = f"{name} - {desc}"
+
+ if plugin_name in self.loaded_plugins:
+ limit = 75 - len(plugin_name) - 4 - 8 + len(name)
+ if limit < 0:
+ fmt = plugin.name
+ limit = 75
+ fmt = truncate(fmt, limit) + '[loaded]\n'
+ else:
+ limit = 75 - len(plugin_name) - 4 + len(name)
+ if limit < 0:
+ fmt = plugin.name
+ limit = 75
+ fmt = truncate(fmt, limit) + '\n'
+
+ if len(fmt) + len(pages[-1]) <= 2048:
+ pages[-1] += fmt
else:
- pages[-1] += fmt + "\n"
+ pages.append(fmt)
embeds = []
diff --git a/cogs/utility.py b/cogs/utility.py
index 8f0986f06b..d54ad362b9 100644
--- a/cogs/utility.py
+++ b/cogs/utility.py
@@ -41,7 +41,7 @@ async def format_cog_help(self, cog, *, no_cog=False):
for cmd in await self.filter_commands(
cog.get_commands() if not no_cog else cog,
sort=True,
- key=lambda c: bot.command_perm(c.qualified_name),
+ key=lambda c: (bot.command_perm(c.qualified_name), c.qualified_name),
):
perm_level = bot.command_perm(cmd.qualified_name)
if perm_level is PermissionLevel.INVALID:
@@ -119,41 +119,34 @@ async def send_cog_help(self, cog):
)
return await session.run()
- async def send_command_help(self, command):
- if not await self.filter_commands([command]):
+ async def _get_help_embed(self, topic):
+ if not await self.filter_commands([topic]):
return
- perm_level = self.context.bot.command_perm(command.qualified_name)
+ perm_level = self.context.bot.command_perm(topic.qualified_name)
if perm_level is not PermissionLevel.INVALID:
perm_level = f"{perm_level.name} [{perm_level}]"
else:
perm_level = "NONE"
embed = Embed(
- title=f"`{self.get_command_signature(command)}`",
+ title=f"`{self.get_command_signature(topic)}`",
color=self.context.bot.main_color,
- description=self.process_help_msg(command.help),
+ description=self.process_help_msg(topic.help),
)
- embed.set_footer(text=f"Permission level: {perm_level}")
- await self.get_destination().send(embed=embed)
+ return embed, perm_level
+
+ async def send_command_help(self, command):
+ topic = await self._get_help_embed(command)
+ if topic is not None:
+ topic[0].set_footer(text=f"Permission level: {topic[1]}")
+ await self.get_destination().send(embed=topic[0])
async def send_group_help(self, group):
- if not await self.filter_commands([group]):
+ topic = await self._get_help_embed(group)
+ if topic is None:
return
-
- perm_level = self.context.bot.command_perm(group.qualified_name)
- if perm_level is not PermissionLevel.INVALID:
- perm_level = f"{perm_level.name} [{perm_level}]"
- else:
- perm_level = "NONE"
-
- embed = Embed(
- title=f"`{self.get_command_signature(group)}`",
- color=self.context.bot.main_color,
- description=self.process_help_msg(group.help),
- )
-
- if perm_level:
- embed.add_field(name="Permission Level", value=perm_level, inline=False)
+ embed = topic[0]
+ embed.add_field(name="Permission Level", value=topic[1], inline=False)
format_ = ""
length = len(group.commands)
@@ -284,7 +277,7 @@ async def changelog(self, ctx, version: str.lower = ""):
pass
logger.warning("Failed to display changelog.", exc_info=True)
await ctx.send(
- f"View the changelog here: {changelog.CHANGELOG_URL}#v{version[::2]}"
+ f"View the changelog here: {changelog.latest_version.changelog_url}#v{version[::2]}"
)
@commands.command(aliases=["bot", "info"])
@@ -308,27 +301,27 @@ async def about(self, ctx):
embed.add_field(name="Uptime", value=self.bot.uptime)
embed.add_field(name="Latency", value=f"{self.bot.latency * 1000:.2f} ms")
embed.add_field(name="Version", value=f"`{self.bot.version}`")
- embed.add_field(name="Author", value="[`kyb3r`](https://github.com/kyb3r)")
+ embed.add_field(name="Author", value="[`kyb3r`, `Taki`, `4jr`](https://github.com/kyb3r)")
changelog = await Changelog.from_url(self.bot)
latest = changelog.latest_version
- if parse_version(self.bot.version) < parse_version(latest.version):
+ if self.bot.version < parse_version(latest.version):
footer = f"A newer version is available v{latest.version}"
else:
footer = "You are up to date with the latest version."
embed.add_field(
- name="GitHub", value="https://github.com/kyb3r/modmail", inline=False
- )
-
- embed.add_field(
- name="Discord Server", value="https://discord.gg/F34cRU8", inline=False
+ name="Want Modmail in Your Server?",
+ value="Installation guide on GitHub (https://github.com/kyb3r/modmail) "
+ "and join our discord server (https://discord.gg/F34cRU8)!", inline=False
)
embed.add_field(
- name="Donate",
- value="Support this bot on [`Patreon`](https://patreon.com/kyber).",
+ name="Support the Developers",
+ value="This bot is completely free for everyone. We rely on kind individuals "
+ "like you to support us on [`Patreon`](https://patreon.com/kyber) (includes perks!) "
+ "to keep this bot free forever!",
)
embed.set_footer(text=footer)
@@ -648,12 +641,11 @@ async def loop_presence(self):
"""Set presence to the configured value every 45 minutes."""
# TODO: Does this even work?
presence = await self.set_presence()
- logger.debug(f'{presence["activity"][1]} {presence["status"][1]}')
+ logger.debug('Loop... %s - %s', presence["activity"][1], presence["status"][1])
@loop_presence.before_loop
async def before_loop_presence(self):
await self.bot.wait_for_connected()
- logger.debug("Starting metadata loop.")
logger.line()
presence = await self.set_presence()
logger.info(presence["activity"][1])
@@ -771,17 +763,15 @@ async def config_set(self, ctx, key: str.lower, *, value: str):
if key in keys:
try:
- value, value_text = await self.bot.config.clean_data(key, value)
- except InvalidConfigError as exc:
- embed = exc.embed
- else:
- self.bot.config[key] = value
+ self.bot.config.set(key, value)
await self.bot.config.update()
embed = Embed(
title="Success",
color=self.bot.main_color,
- description=f"Set `{key}` to `{value_text}`",
+ description=f"Set `{key}` to `{self.bot.config[key]}`.",
)
+ except InvalidConfigError as exc:
+ embed = exc.embed
else:
embed = Embed(
title="Error",
@@ -795,7 +785,7 @@ async def config_set(self, ctx, key: str.lower, *, value: str):
@config.command(name="remove", aliases=["del", "delete"])
@checks.has_permissions(PermissionLevel.OWNER)
- async def config_remove(self, ctx, key: str.lower):
+ async def config_remove(self, ctx, *, key: str.lower):
"""Delete a set configuration variable."""
keys = self.bot.config.public_keys
if key in keys:
@@ -819,7 +809,7 @@ async def config_remove(self, ctx, key: str.lower):
@config.command(name="get")
@checks.has_permissions(PermissionLevel.OWNER)
- async def config_get(self, ctx, key: str.lower = None):
+ async def config_get(self, ctx, *, key: str.lower = None):
"""
Show the configuration variables that are currently set.
diff --git a/core/_color_data.py b/core/_color_data.py
index 997a6a1d25..acfd834505 100644
--- a/core/_color_data.py
+++ b/core/_color_data.py
@@ -5,41 +5,41 @@
BASE_COLORS = {
- "b": "#0000ff",
- "g": "#007f00",
- "r": "#ff0000",
- "c": "#00bfbf",
- "m": "#bf00bf",
- "y": "#bfbf00",
- "k": "#000000",
- "w": "#ffffff",
+ "b": "0000ff",
+ "g": "007f00",
+ "r": "ff0000",
+ "c": "00bfbf",
+ "m": "bf00bf",
+ "y": "bfbf00",
+ "k": "000000",
+ "w": "ffffff",
}
# Discord native colors
DISCORD_COLORS = {
- "default": "#000000",
- "teal": "#1abc9c",
- "dark teal": "#11806a",
- "green": "#2ecc71",
- "dark green": "#1f8b4c",
- "blue": "#3498db",
- "dark blue": "#206694",
- "purple": "#9b59b6",
- "dark purple": "#71368a",
- "magenta": "#e91e63",
- "dark magenta": "#ad1457",
- "gold": "#f1c40f",
- "dark gold": "#c27c0e",
- "orange": "#e67e22",
- "dark orange": "#a84300",
- "red": "#e74c3c",
- "dark red": "#992d22",
- "lighter gray": "#95a5a6",
- "darker gray": "#546e7a",
- "light gray": "#979c9f",
- "dark gray": "#607d8b",
- "blurple": "#7289da",
- "grayple": "#99aab5"
+ "default": "000000",
+ "teal": "1abc9c",
+ "dark teal": "11806a",
+ "green": "2ecc71",
+ "dark green": "1f8b4c",
+ "blue": "3498db",
+ "dark blue": "206694",
+ "purple": "9b59b6",
+ "dark purple": "71368a",
+ "magenta": "e91e63",
+ "dark magenta": "ad1457",
+ "gold": "f1c40f",
+ "dark gold": "c27c0e",
+ "orange": "e67e22",
+ "dark orange": "a84300",
+ "red": "e74c3c",
+ "dark red": "992d22",
+ "lighter gray": "95a5a6",
+ "darker gray": "546e7a",
+ "light gray": "979c9f",
+ "dark gray": "607d8b",
+ "blurple": "7289da",
+ "grayple": "99aab5"
}
# Normalize name to "discord: