-
+
From fc2d0a2d6d77bb1453eeb6862b966b57b99cdef1 Mon Sep 17 00:00:00 2001
From: Taven Argus
-
+
@@ -175,9 +189,9 @@ Become a sponsor on [Patreon](https://patreon.com/kyber).
## Plugins
Modmail supports the use of third-party plugins to extend or add functionalities to the bot.
-Plugins allow niche features as well as anything else outside of the scope of the core functionality of Modmail.
+This allows niche features as well as anything else outside of the scope of the core functionality of Modmail.
-You can find a list of third-party plugins using the `?plugins registry` command or visit the [Unofficial List of Plugins](https://github.com/kyb3r/modmail/wiki/Unofficial-List-of-Plugins) for a list of plugins contributed by the community.
+A list of third-party plugins can be found using the `?plugins registry` command or visit the [Unofficial List of Plugins](https://github.com/kyb3r/modmail/wiki/Unofficial-List-of-Plugins) for a list of plugins contributed by the community.
To develop your own, check out the [plugins documentation](https://github.com/kyb3r/modmail/wiki/Plugins).
@@ -185,6 +199,6 @@ Plugins requests and support is available in our [Modmail Plugins Server](https:
## Contributing
-Contributions to Modmail are always welcome, whether it be improvements to the documentation or new functionality, please feel free to make the change. Check out our [contributing 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.
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 24f1218585..ddbeec2d31 100644
--- a/bot.py
+++ b/bot.py
@@ -1,4 +1,4 @@
-__version__ = "3.4.1"
+__version__ = "3.5.0-dev0"
import asyncio
@@ -31,7 +31,7 @@
except ImportError:
pass
-from core import checks
+from core import checks, translations
from core.clients import ApiClient, PluginDatabaseClient
from core.config import ConfigManager
from core.utils import human_join, normalize_alias
@@ -48,7 +48,7 @@
class ModmailBot(commands.Bot):
- def __init__(self, args):
+ def __init__(self):
super().__init__(command_prefix=None) # implemented in `get_prefix`
self._session = None
self._api = None
@@ -58,10 +58,7 @@ def __init__(self, args):
self._connected = asyncio.Event()
self.start_time = datetime.utcnow()
- if len(args):
- self.config = ConfigManager(self, args[0])
- else:
- self.config = ConfigManager(self)
+ self.config = ConfigManager(self)
self.config.populate_cache()
self.threads = ThreadManager(self)
@@ -472,7 +469,7 @@ async def on_ready(self):
{
"open": False,
"closed_at": str(datetime.utcnow()),
- "close_message": "Channel has been deleted, no closer found.",
+ "close_message": _("Channel has been deleted, no closer found."),
"closer": {
"id": str(self.user.id),
"name": self.user.name,
@@ -552,7 +549,7 @@ def check_account_age(self, author: discord.Member) -> bool:
logger.debug("Blocked due to account age, user %s.", author.name)
if str(author.id) not in self.blocked_users:
- new_reason = f"System Message: New Account. Required to wait for {delta}."
+ new_reason = _("System Message: New Account. Required to wait for {time}.").format(time=delta)
self.blocked_users[str(author.id)] = new_reason
return False
@@ -618,7 +615,7 @@ def check_manual_blocked(self, author: discord.Member) -> bool:
return False
async def _process_blocked(self, message):
- _, blocked_emoji = await self.retrieve_emoji()
+ x, blocked_emoji = await self.retrieve_emoji()
if await self.is_blocked(message.author, channel=message.channel, send_message=True):
await self.add_reaction(message, blocked_emoji)
return True
@@ -988,7 +985,7 @@ async def handle_reaction_events(self, payload, *, add):
if not thread:
return
try:
- _, linked_message = await thread.find_linked_messages(
+ x, linked_message = await thread.find_linked_messages(
message.id, either_direction=True
)
except ValueError as e:
@@ -1072,32 +1069,19 @@ async def on_member_join(self, member):
async def on_message_delete(self, message):
"""Support for deleting linked messages"""
# TODO: use audit log to check if modmail deleted the message
- if isinstance(message.channel, discord.DMChannel):
- thread = await self.threads.find(recipient=message.author)
- if not thread:
- return
+ if message.embeds and not isinstance(message.channel, discord.DMChannel):
+ thread = await self.threads.find(channel=message.channel)
try:
- message = await thread.find_linked_message_from_dm(message)
+ await thread.delete_message(message)
except ValueError as e:
- if str(e) != "Thread channel message not found.":
+ if str(e) not in {"DM message not found.", " Malformed thread message."}:
logger.warning("Failed to find linked message to delete: %s", e)
- return
+ else:
+ thread = await self.threads.find(recipient=message.author)
+ message = await thread.find_linked_message_from_dm(message)
embed = message.embeds[0]
embed.set_footer(text=f"{embed.footer.text} (deleted)", icon_url=embed.footer.icon_url)
await message.edit(embed=embed)
- return
-
- thread = await self.threads.find(channel=message.channel)
- if not thread:
- return
- try:
- await thread.delete_message(message, note=False)
- except ValueError as e:
- if str(e) not in {"DM message not found.", "Malformed thread message."}:
- logger.warning("Failed to find linked message to delete: %s", e)
- return
- except discord.NotFound:
- return
async def on_bulk_message_delete(self, messages):
await discord.utils.async_all(self.on_message_delete(msg) for msg in messages)
@@ -1110,13 +1094,10 @@ async def on_message_edit(self, before, after):
if isinstance(after.channel, discord.DMChannel):
thread = await self.threads.find(recipient=before.author)
- if not thread:
- return
-
try:
await thread.edit_dm_message(after, after.content)
except ValueError:
- _, blocked_emoji = await self.retrieve_emoji()
+ x, blocked_emoji = await self.retrieve_emoji()
await self.add_reaction(after, blocked_emoji)
else:
embed = discord.Embed(
@@ -1227,7 +1208,7 @@ async def before_post_metadata(self):
self.metadata_loop.cancel()
-def main(args):
+def main():
try:
# noinspection PyUnresolvedReferences
import uvloop
@@ -1237,9 +1218,10 @@ def main(args):
except ImportError:
pass
- bot = ModmailBot(args)
+ translations.init()
+ bot = ModmailBot()
bot.run()
if __name__ == "__main__":
- main(sys.argv[1:])
+ main()
diff --git a/cogs/modmail.py b/cogs/modmail.py
index 3b9c591294..7f5b2fe15b 100644
--- a/cogs/modmail.py
+++ b/cogs/modmail.py
@@ -41,17 +41,17 @@ async def setup(self, ctx):
if ctx.guild != self.bot.modmail_guild:
return await ctx.send(
- f"You can only setup in the Modmail guild: {self.bot.modmail_guild}."
+ _("You can only setup in the Modmail guild: {guild_name}.".format(guild_name=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.")
+ return await ctx.send(_("{guild_name} is already set up.").format(guild_name=self.bot.modmail_guild))
if self.bot.modmail_guild is None:
embed = discord.Embed(
- title="Error",
- description="Modmail functioning guild not found.",
+ title=_("Error"),
+ description=_("Modmail functioning guild not found."),
color=self.bot.error_color,
)
return await ctx.send(embed=embed)
@@ -88,21 +88,23 @@ 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 "
- "
-
-
-
-
Become a sponsor on [Patreon](https://patreon.com/kyber).
## Plugins
From 50c440f104eb5b89373822ed2fbed35bbac7ac40 Mon Sep 17 00:00:00 2001
From: Taku 3 Animals <45324516+Taaku18@users.noreply.github.com>
Date: Sun, 11 Oct 2020 13:55:48 +0800
Subject: [PATCH 072/705] Update bot.py
---
bot.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/bot.py b/bot.py
index 9e48c0f342..daa62d0516 100644
--- a/bot.py
+++ b/bot.py
@@ -1214,7 +1214,7 @@ async def post_metadata(self):
else:
data.update({"owner_name": info.owner.name, "owner_id": info.owner.id, "team": False})
- async with self.session.post("https://api.logviewer.tech/metadata", json=data):
+ async with self.session.post("https://api.modmail.dev/metadata", json=data):
logger.debug("Uploading metadata to Modmail server.")
async def before_post_metadata(self):
From eb80fed05e7a7e6fc6c6e59693bbb1a8e416a8a2 Mon Sep 17 00:00:00 2001
From: fourjr <28086837+fourjr@users.noreply.github.com>
Date: Wed, 28 Oct 2020 15:06:52 +0800
Subject: [PATCH 073/705] Fix errors on windows selfhost
---
bot.py | 10 +++++++---
cogs/plugins.py | 4 ++--
2 files changed, 9 insertions(+), 5 deletions(-)
diff --git a/bot.py b/bot.py
index 9e48c0f342..976c01744f 100644
--- a/bot.py
+++ b/bot.py
@@ -89,9 +89,13 @@ def uptime(self) -> str:
def startup(self):
logger.line()
- logger.info("┌┬┐┌─┐┌┬┐┌┬┐┌─┐┬┬")
- logger.info("││││ │ │││││├─┤││")
- logger.info("┴ ┴└─┘─┴┘┴ ┴┴ ┴┴┴─┘")
+ if os.name != 'nt':
+ logger.info("┌┬┐┌─┐┌┬┐┌┬┐┌─┐┬┬")
+ logger.info("││││ │ │││││├─┤││")
+ logger.info("┴ ┴└─┘─┴┘┴ ┴┴ ┴┴┴─┘")
+ else:
+ logger.info("MODMAIL")
+ logger.info("MODMAIL")
logger.info("v%s", __version__)
logger.info("Authors: kyb3r, fourjr, Taaku18")
logger.line()
diff --git a/cogs/plugins.py b/cogs/plugins.py
index df8259736c..6203edab67 100644
--- a/cogs/plugins.py
+++ b/cogs/plugins.py
@@ -199,7 +199,7 @@ async def load_plugin(self, plugin):
venv = hasattr(sys, "real_prefix") or hasattr(sys, "base_prefix") # in a virtual env
user_install = " --user" if not venv else ""
proc = await asyncio.create_subprocess_shell(
- f"{sys.executable} -m pip install --upgrade{user_install} -r {req_txt} -q -q",
+ f"\"{sys.executable}\" -m pip install --upgrade{user_install} -r {req_txt} -q -q --user",
stderr=PIPE,
stdout=PIPE,
)
@@ -497,7 +497,7 @@ async def plugins_reset(self, ctx):
logger.warning("Removing %s.", entry.name)
embed = discord.Embed(
- description=f"Successfully purged all plugins from the bot.", color=self.bot.main_color
+ description="Successfully purged all plugins from the bot.", color=self.bot.main_color
)
return await ctx.send(embed=embed)
From 6f05aa8129d634d1c4616230d5f21d24ddcd83eb Mon Sep 17 00:00:00 2001
From: fourjr <28086837+fourjr@users.noreply.github.com>
Date: Wed, 28 Oct 2020 15:23:12 +0800
Subject: [PATCH 074/705] Add avatar to closed thread messages, resolve #2828
---
core/thread.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/core/thread.py b/core/thread.py
index 960dafe0f7..ffe9c212df 100644
--- a/core/thread.py
+++ b/core/thread.py
@@ -361,7 +361,7 @@ async def _close(
event = "Thread Closed as Scheduled" if scheduled else "Thread Closed"
# embed.set_author(name=f"Event: {event}", url=log_url)
- embed.set_footer(text=f"{event} by {_closer}")
+ embed.set_footer(text=f"{event} by {_closer}", icon_url=closer.avatar_url)
embed.timestamp = datetime.utcnow()
tasks = [self.bot.config.update()]
From 17e287c8579322531a6e5f6d0564a1970890fcf6 Mon Sep 17 00:00:00 2001
From: fourjr <28086837+fourjr@users.noreply.github.com>
Date: Wed, 28 Oct 2020 15:27:15 +0800
Subject: [PATCH 075/705] Add nsfw indicator to log messages, resolve #2792
---
core/thread.py | 8 +++++++-
1 file changed, 7 insertions(+), 1 deletion(-)
diff --git a/core/thread.py b/core/thread.py
index ffe9c212df..b300e9fbbc 100644
--- a/core/thread.py
+++ b/core/thread.py
@@ -316,6 +316,7 @@ async def _close(
{
"open": False,
"closed_at": str(datetime.utcnow()),
+ "nsfw": self.channel.nsfw,
"close_message": message if not silent else None,
"closer": {
"id": str(closer.id),
@@ -338,8 +339,13 @@ async def _close(
sneak_peak = content.replace("\n", "")
else:
sneak_peak = "No content"
+
+ if self.channel.nsfw:
+ _nsfw = 'NSFW-'
+ else:
+ _nsfw = ''
- desc = f"[`{log_data['key']}`]({log_url}): "
+ desc = f"[`{_nsfw}{log_data['key']}`]({log_url}): "
desc += truncate(sneak_peak, max=75 - 13)
else:
desc = "Could not resolve log url."
From d70fd49a3fc668eb278764296b6aa53cfd6c8d07 Mon Sep 17 00:00:00 2001
From: fourjr <28086837+fourjr@users.noreply.github.com>
Date: Wed, 28 Oct 2020 15:34:04 +0800
Subject: [PATCH 076/705] Add thread_move_title config
---
core/config.py | 1 +
core/config_help.json | 14 ++++++++++++--
core/thread.py | 4 ++--
3 files changed, 15 insertions(+), 4 deletions(-)
diff --git a/core/config.py b/core/config.py
index 4f54c2b29c..f2bb54a5c8 100644
--- a/core/config.py
+++ b/core/config.py
@@ -57,6 +57,7 @@ class ConfigManager:
"thread_close_title": "Thread Closed",
"thread_close_response": "{closer.mention} has closed this Modmail thread.",
"thread_self_close_response": "You have closed this Modmail thread.",
+ "thread_move_title": "Thread Moved",
"thread_move_notify": False,
"thread_move_response": "This thread has been moved.",
"disabled_new_thread_title": "Not Delivered",
diff --git a/core/config_help.json b/core/config_help.json
index db9218d2b3..9778f49f8c 100644
--- a/core/config_help.json
+++ b/core/config_help.json
@@ -350,6 +350,16 @@
"See also: `thread_close_title`, `thread_close_footer`, `thread_close_response`."
]
},
+ "thread_move_title": {
+ "default": "Thread Moved",
+ "description": "The title of the message embed when a thread is moved.",
+ "examples": [
+ "`{prefix}config set thread_move_title Thread transferred to another channel!"
+ ],
+ "notes": [
+ "See also: `thread_move_notify`, `thread_move_response`."
+ ]
+ },
"thread_move_notify": {
"default": "No",
"description": "Notify the recipient if the thread was moved.",
@@ -358,7 +368,7 @@
"`{prefix}config set thread_move_notify no`"
],
"notes": [
- "See also: `thread_move_response`."
+ "See also: `thread_move_title`, `thread_move_response`."
]
},
"thread_move_response": {
@@ -369,7 +379,7 @@
],
"notes": [
"Only has an effect when `thread_move_notify` is on.",
- "See also: `thread_move_notify`."
+ "See also: `thread_move_title`, `thread_move_notify`."
]
},
"disabled_new_thread_title": {
diff --git a/core/thread.py b/core/thread.py
index b300e9fbbc..23ab81ad0d 100644
--- a/core/thread.py
+++ b/core/thread.py
@@ -341,9 +341,9 @@ async def _close(
sneak_peak = "No content"
if self.channel.nsfw:
- _nsfw = 'NSFW-'
+ _nsfw = "NSFW-"
else:
- _nsfw = ''
+ _nsfw = ""
desc = f"[`{_nsfw}{log_data['key']}`]({log_url}): "
desc += truncate(sneak_peak, max=75 - 13)
From 1aa7dfc1d42c6b16565186e96b1b372f40295fa9 Mon Sep 17 00:00:00 2001
From: fourjr <28086837+fourjr@users.noreply.github.com>
Date: Wed, 28 Oct 2020 15:39:26 +0800
Subject: [PATCH 077/705] Add thread_move_title config
---
cogs/modmail.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/cogs/modmail.py b/cogs/modmail.py
index 9ec5d5604f..a8551410e4 100644
--- a/cogs/modmail.py
+++ b/cogs/modmail.py
@@ -305,7 +305,7 @@ async def move(self, ctx, category: discord.CategoryChannel, *, specifics: str =
if self.bot.config["thread_move_notify"] and not silent:
embed = discord.Embed(
- title="Thread Moved",
+ title=self.bot.config["thread_move_title"],
description=self.bot.config["thread_move_response"],
color=self.bot.main_color,
)
From a3edc421f083c15878547310fcb910789c1db827 Mon Sep 17 00:00:00 2001
From: fourjr <28086837+fourjr@users.noreply.github.com>
Date: Wed, 28 Oct 2020 15:51:10 +0800
Subject: [PATCH 078/705] Allow mention command to accept ids/names, resolve
#2796
---
cogs/utility.py | 5 +++--
1 file changed, 3 insertions(+), 2 deletions(-)
diff --git a/cogs/utility.py b/cogs/utility.py
index bd04bd6a09..deca773af1 100644
--- a/cogs/utility.py
+++ b/cogs/utility.py
@@ -638,7 +638,7 @@ async def ping(self, ctx):
@commands.command()
@checks.has_permissions(PermissionLevel.ADMINISTRATOR)
- async def mention(self, ctx, *, mention: str = None):
+ async def mention(self, ctx, *mention: Union[discord.Role, discord.Member]):
"""
Change what the bot mentions at the start of each thread.
@@ -647,11 +647,12 @@ async def mention(self, ctx, *, mention: str = None):
# TODO: ability to disable mention.
current = self.bot.config["mention"]
- if mention is None:
+ if not mention:
embed = discord.Embed(
title="Current mention:", color=self.bot.main_color, description=str(current)
)
else:
+ mention = " ".join(i.mention for i in mention)
embed = discord.Embed(
title="Changed mention!",
description=f'On thread creation the bot now says "{mention}".',
From 1f846fa4cb16082985c15240b5767fe47399bee7 Mon Sep 17 00:00:00 2001
From: fourjr <28086837+fourjr@users.noreply.github.com>
Date: Wed, 28 Oct 2020 15:51:19 +0800
Subject: [PATCH 079/705] Formatting
---
cogs/plugins.py | 2 +-
core/thread.py | 2 +-
2 files changed, 2 insertions(+), 2 deletions(-)
diff --git a/cogs/plugins.py b/cogs/plugins.py
index 6203edab67..835aad49a9 100644
--- a/cogs/plugins.py
+++ b/cogs/plugins.py
@@ -199,7 +199,7 @@ async def load_plugin(self, plugin):
venv = hasattr(sys, "real_prefix") or hasattr(sys, "base_prefix") # in a virtual env
user_install = " --user" if not venv else ""
proc = await asyncio.create_subprocess_shell(
- f"\"{sys.executable}\" -m pip install --upgrade{user_install} -r {req_txt} -q -q --user",
+ f'"{sys.executable}" -m pip install --upgrade{user_install} -r {req_txt} -q -q --user',
stderr=PIPE,
stdout=PIPE,
)
diff --git a/core/thread.py b/core/thread.py
index 23ab81ad0d..9c388389e0 100644
--- a/core/thread.py
+++ b/core/thread.py
@@ -339,7 +339,7 @@ async def _close(
sneak_peak = content.replace("\n", "")
else:
sneak_peak = "No content"
-
+
if self.channel.nsfw:
_nsfw = "NSFW-"
else:
From 732de02a3c321842eeb17fdc7c54fdcbc0d0fef0 Mon Sep 17 00:00:00 2001
From: fourjr <28086837+fourjr@users.noreply.github.com>
Date: Wed, 28 Oct 2020 16:07:20 +0800
Subject: [PATCH 080/705] Change move command to consume for category
---
cogs/modmail.py | 31 ++++++++++++++++++++++++++-----
1 file changed, 26 insertions(+), 5 deletions(-)
diff --git a/cogs/modmail.py b/cogs/modmail.py
index a8551410e4..b7489688cc 100644
--- a/cogs/modmail.py
+++ b/cogs/modmail.py
@@ -284,22 +284,43 @@ async def snippet_edit(self, ctx, name: str.lower, *, value):
embed = create_not_found_embed(name, self.bot.snippets.keys(), "Snippet")
await ctx.send(embed=embed)
- @commands.command()
+ @commands.command(usage='
-
-
-
-
Become a sponsor on [Patreon](https://patreon.com/kyber).
## Plugins
Modmail supports the use of third-party plugins to extend or add functionalities to the bot.
-This allows niche features as well as anything else outside of the scope of the core functionality of Modmail.
+Plugins allow niche features as well as anything else outside of the scope of the core functionality of Modmail.
-A list of third-party plugins can be found using the `?plugins registry` command or visit the [Unofficial List of Plugins](https://github.com/kyb3r/modmail/wiki/Unofficial-List-of-Plugins) for a list of plugins contributed by the community.
+You can find a list of third-party plugins using the `?plugins registry` command or visit the [Unofficial List of Plugins](https://github.com/kyb3r/modmail/wiki/Unofficial-List-of-Plugins) for a list of plugins contributed by the community.
To develop your own, check out the [plugins documentation](https://github.com/kyb3r/modmail/wiki/Plugins).
@@ -199,6 +181,6 @@ Plugins requests and support is available in our [Modmail Plugins Server](https:
## Contributing
-Contributions to Modmail are always welcome, whether it be improvements to the documentation or new functionality, please feel free to make the change. Check out our contribution [guidelines](https://github.com/kyb3r/modmail/blob/master/CONTRIBUTING.md) before you get started.
+Contributions to Modmail are always welcome, whether it be improvements to the documentation or new functionality, please feel free to make the change. Check out our [contributing guidelines](https://github.com/kyb3r/modmail/blob/master/CONTRIBUTING.md) before you get started.
If you like this project and would like to show your appreciation, support us on **[Patreon](https://www.patreon.com/kyber)**!
diff --git a/SPONSORS.json b/SPONSORS.json
index d30134e015..ceed37be23 100644
--- a/SPONSORS.json
+++ b/SPONSORS.json
@@ -26,42 +26,5 @@
}
]
}
- },
- {
- "embed": {
- "title": "Hey there!",
- "description": "Nice to see you here! You can support us by subscribing on youtube -> [Youtube](https://www.youtube.com/user/RoomieOfficial) <- and also join our [Discord](https://discord.gg/zaeVCaV)!",
- "url": "https://discord.gg/zaeVCaV",
- "color": 13003681,
- "footer": {
- "icon_url": "https://imgur.com/Mrc9pLd.gif",
- "text": "everyone is a clown"
- },
- "thumbnail": {
- "url": "https://imgur.com/Mrc9pLd.gif"
- },
- "image": {
- "url": "https://imgur.com/ZUFiL6b.gif"
- },
- "author": {
- "name": "Roomieofficial",
- "url": "https://discord.gg/zaeVCaV",
- "icon_url": "https://imgur.com/6hBkt7Z.png"
- },
- "fields": [
- {
- "name": "What is all this about 🤔",
- "value": "We are mainly focused on everything that has anything to do with music or singing! "
- },
- {
- "name": "Youtube 🙄",
- "value": "U will get great content if you follow our [Youtube](https://www.youtube.com/user/RoomieOfficial) with weekly uploads."
- },
- {
- "name": "Discord 😁",
- "value": "Make sure to join our [Discord](https://discord.gg/zaeVCaV) We have weekly events, 24/7 chats and more!"
- }
- ]
- }
}
]
diff --git a/app.json b/app.json
index 76fe320a30..41697cf064 100644
--- a/app.json
+++ b/app.json
@@ -1,27 +1,35 @@
{
- "name": "Modmail",
- "description": "An easy to install Modmail bot for Discord - DM to contact mods!",
- "repository": "https://github.com/kyb3r/modmail",
- "env": {
- "TOKEN": {
- "description": "Your discord bot's token.",
- "required": true
- },
- "GUILD_ID": {
- "description": "The id for the server you are hosting this bot for.",
- "required": true
- },
- "OWNERS": {
- "description": "Comma separated user IDs of people that are allowed to use owner only commands. (eval).",
- "required": true
- },
- "MONGO_URI": {
- "description": "Mongo DB connection URI for self-hosting your data.",
- "required": true
- },
- "LOG_URL": {
- "description": "The url of the log viewer app for viewing self-hosted logs.",
- "required": true
+ "name": "Modmail",
+ "description": "An easy to install Modmail bot for Discord - DM to contact mods!",
+ "repository": "https://github.com/kyb3r/modmail",
+ "env": {
+ "TOKEN": {
+ "description": "Your discord bot's token.",
+ "required": true
+ },
+ "GUILD_ID": {
+ "description": "The id for the server you are hosting this bot for.",
+ "required": true
+ },
+ "MODMAIL_GUILD_ID": {
+ "description": "The ID of the discord server where the threads channels should be created (receiving server). Default to GUILD_ID.",
+ "required": false
+ },
+ "OWNERS": {
+ "description": "Comma separated user IDs of people that are allowed to use owner only commands. (eval).",
+ "required": true
+ },
+ "CONNECTION_URI": {
+ "description": "The connection URI for your database.",
+ "required": true
+ },
+ "DATABASE_TYPE": {
+ "description": "The type of your database. There is only one supported database at the moment - MongoDB (default).",
+ "required": false
+ },
+ "LOG_URL": {
+ "description": "The url of the log viewer app for viewing self-hosted logs.",
+ "required": true
+ }
}
- }
-}
+}
\ No newline at end of file
diff --git a/bot.py b/bot.py
index a379567b4d..ab5ded6c53 100644
--- a/bot.py
+++ b/bot.py
@@ -1,4 +1,4 @@
-__version__ = "3.5.0-dev0"
+__version__ = "3.6.3-dev0"
import asyncio
@@ -18,8 +18,6 @@
from aiohttp import ClientSession
from emoji import UNICODE_EMOJI
-from motor.motor_asyncio import AsyncIOMotorClient
-from pymongo.errors import ConfigurationError
from pkg_resources import parse_version
@@ -31,8 +29,8 @@
except ImportError:
pass
-from core import checks, translations
-from core.clients import ApiClient, PluginDatabaseClient
+from core import checks
+from core.clients import ApiClient, PluginDatabaseClient, MongoDBClient
from core.config import ConfigManager
from core.utils import human_join, normalize_alias
from core.models import PermissionLevel, SafeFormatter, getLogger, configure_logging
@@ -46,10 +44,17 @@
if not os.path.exists(temp_dir):
os.mkdir(temp_dir)
+if sys.platform == "win32":
+ try:
+ asyncio.set_event_loop_policy(asyncio.WindowsProactorEventLoopPolicy())
+ except AttributeError:
+ logger.error("Failed to use WindowsProactorEventLoopPolicy.", exc_info=True)
+
class ModmailBot(commands.Bot):
def __init__(self):
- super().__init__(command_prefix=None) # implemented in `get_prefix`
+ intents = discord.Intents.all()
+ super().__init__(command_prefix=None, intents=intents) # implemented in `get_prefix`
self._session = None
self._api = None
self.metadata_loop = None
@@ -66,22 +71,7 @@ def __init__(self):
self.log_file_name = os.path.join(temp_dir, f"{self.token.split('.')[0]}.log")
self._configure_logging()
- mongo_uri = self.config["mongo_uri"]
- if mongo_uri is None:
- logger.critical("A Mongo URI is necessary for the bot to function.")
- raise RuntimeError
-
- try:
- self.db = AsyncIOMotorClient(mongo_uri).modmail_bot
- except ConfigurationError as e:
- logger.critical(
- "Your MONGO_URI might be copied wrong, try re-copying from the source again. "
- "Otherwise noted in the following message:"
- )
- logger.critical(e)
- sys.exit(0)
-
- self.plugin_db = PluginDatabaseClient(self)
+ self.plugin_db = PluginDatabaseClient(self) # Deprecated
self.startup()
@property
@@ -100,9 +90,12 @@ def uptime(self) -> str:
def startup(self):
logger.line()
- logger.info("┌┬┐┌─┐┌┬┐┌┬┐┌─┐┬┬")
- logger.info("││││ │ │││││├─┤││")
- logger.info("┴ ┴└─┘─┴┘┴ ┴┴ ┴┴┴─┘")
+ if os.name != "nt":
+ logger.info("┌┬┐┌─┐┌┬┐┌┬┐┌─┐┬┬")
+ logger.info("││││ │ │││││├─┤││")
+ logger.info("┴ ┴└─┘─┴┘┴ ┴┴ ┴┴┴─┘")
+ else:
+ logger.info("MODMAIL")
logger.info("v%s", __version__)
logger.info("Authors: kyb3r, fourjr, Taaku18")
logger.line()
@@ -152,9 +145,18 @@ def session(self) -> ClientSession:
@property
def api(self) -> ApiClient:
if self._api is None:
- self._api = ApiClient(self)
+ if self.config["database_type"].lower() == "mongodb":
+ self._api = MongoDBClient(self)
+ else:
+ logger.critical("Invalid database type.")
+ raise RuntimeError
return self._api
+ @property
+ def db(self):
+ # deprecated
+ return self.api.db
+
async def get_prefix(self, message=None):
return [self.prefix, f"<@{self.user.id}> ", f"<@!{self.user.id}> "]
@@ -165,6 +167,10 @@ def run(self, *args, **kwargs):
pass
except discord.LoginFailure:
logger.critical("Invalid token")
+ except discord.PrivilegedIntentsRequired:
+ logger.critical(
+ "Privileged intents are not explicitly granted in the discord developers dashboard."
+ )
except Exception:
logger.critical("Fatal exception", exc_info=True)
finally:
@@ -180,7 +186,7 @@ def run(self, *args, **kwargs):
logger.error(" - Shutting down bot - ")
@property
- def owner_ids(self):
+ def bot_owner_ids(self):
owner_ids = self.config["owners"]
if owner_ids is not None:
owner_ids = set(map(int, str(owner_ids).split(",")))
@@ -192,7 +198,7 @@ def owner_ids(self):
return owner_ids
async def is_owner(self, user: discord.User) -> bool:
- if user.id in self.owner_ids:
+ if user.id in self.bot_owner_ids:
return True
return await super().is_owner(user)
@@ -370,37 +376,16 @@ def command_perm(self, command_name: str) -> PermissionLevel:
async def on_connect(self):
try:
- await self.validate_database_connection()
+ await self.api.validate_database_connection()
except Exception:
logger.debug("Logging out due to failed database connection.")
return await self.logout()
logger.debug("Connected to gateway.")
await self.config.refresh()
- await self.setup_indexes()
+ await self.api.setup_indexes()
self._connected.set()
- async def setup_indexes(self):
- """Setup text indexes so we can use the $search operator"""
- coll = self.db.logs
- index_name = "messages.content_text_messages.author.name_text_key_text"
-
- index_info = await coll.index_information()
-
- # Backwards compatibility
- old_index = "messages.content_text_messages.author.name_text"
- if old_index in index_info:
- logger.info("Dropping old index: %s", old_index)
- await coll.drop_index(old_index)
-
- if index_name not in index_info:
- logger.info('Creating "text" index for logs collection.')
- logger.info("Name: %s", index_name)
- await coll.create_index(
- [("messages.content", "text"), ("messages.author.name", "text"), ("key", "text")]
- )
- logger.debug("Successfully configured and verified database indexes.")
-
async def on_ready(self):
"""Bot startup, sets uptime."""
@@ -416,7 +401,8 @@ async def on_ready(self):
logger.info("Logged in as: %s", self.user)
logger.info("Bot ID: %s", self.user.id)
owners = ", ".join(
- getattr(self.get_user(owner_id), "name", str(owner_id)) for owner_id in self.owner_ids
+ getattr(self.get_user(owner_id), "name", str(owner_id))
+ for owner_id in self.bot_owner_ids
)
logger.info("Owners: %s", owners)
logger.info("Prefix: %s", self.prefix)
@@ -469,7 +455,7 @@ async def on_ready(self):
{
"open": False,
"closed_at": str(datetime.utcnow()),
- "close_message": _("Channel has been deleted, no closer found."),
+ "close_message": "Channel has been deleted, no closer found.",
"closer": {
"id": str(self.user.id),
"name": self.user.name,
@@ -498,6 +484,17 @@ async def on_ready(self):
self.metadata_loop.before_loop(self.before_post_metadata)
self.metadata_loop.start()
+ other_guilds = [
+ guild for guild in self.guilds if guild not in {self.guild, self.modmail_guild}
+ ]
+ if any(other_guilds):
+ logger.warning(
+ "The bot is in more servers other than the main and staff server."
+ "This may cause data compromise (%s).",
+ ", ".join(guild.name for guild in other_guilds),
+ )
+ logger.warning("If the external servers are valid, you may ignore this message.")
+
async def convert_emoji(self, name: str) -> str:
ctx = SimpleNamespace(bot=self, guild=self.modmail_guild)
converter = commands.EmojiConverter()
@@ -549,9 +546,7 @@ def check_account_age(self, author: discord.Member) -> bool:
logger.debug("Blocked due to account age, user %s.", author.name)
if str(author.id) not in self.blocked_users:
- new_reason = _("System Message: New Account. Required to wait for {time}.").format(
- time=delta
- )
+ new_reason = f"System Message: New Account. Required to wait for {delta}."
self.blocked_users[str(author.id)] = new_reason
return False
@@ -617,7 +612,7 @@ def check_manual_blocked(self, author: discord.Member) -> bool:
return False
async def _process_blocked(self, message):
- x, blocked_emoji = await self.retrieve_emoji()
+ _, blocked_emoji = await self.retrieve_emoji()
if await self.is_blocked(message.author, channel=message.channel, send_message=True):
await self.add_reaction(message, blocked_emoji)
return True
@@ -937,7 +932,7 @@ async def on_typing(self, channel, user, _):
return
await thread.recipient.trigger_typing()
- async def handle_reaction_events(self, payload, *, add):
+ async def handle_reaction_events(self, payload):
user = self.get_user(payload.user_id)
if user.bot:
return
@@ -963,7 +958,7 @@ async def handle_reaction_events(self, payload, *, add):
if not thread:
return
if (
- add
+ payload.event_type == "REACTION_ADD"
and message.embeds
and str(reaction) == str(close_emoji)
and self.config.get("recipient_thread_close")
@@ -987,14 +982,14 @@ async def handle_reaction_events(self, payload, *, add):
if not thread:
return
try:
- x, linked_message = await thread.find_linked_messages(
+ _, linked_message = await thread.find_linked_messages(
message.id, either_direction=True
)
except ValueError as e:
logger.warning("Failed to find linked message for reactions: %s", e)
return
- if add:
+ if payload.event_type == "REACTION_ADD":
if await self.add_reaction(linked_message, reaction):
await self.add_reaction(message, reaction)
else:
@@ -1005,28 +1000,15 @@ async def handle_reaction_events(self, payload, *, add):
logger.warning("Failed to remove reaction: %s", e)
async def on_raw_reaction_add(self, payload):
- await self.handle_reaction_events(payload, add=True)
+ await self.handle_reaction_events(payload)
async def on_raw_reaction_remove(self, payload):
- await self.handle_reaction_events(payload, add=False)
+ await self.handle_reaction_events(payload)
async def on_guild_channel_delete(self, channel):
if channel.guild != self.modmail_guild:
return
- try:
- audit_logs = self.modmail_guild.audit_logs()
- entry = await audit_logs.find(lambda a: a.target == channel)
- mod = entry.user
- except AttributeError as e:
- # discord.py broken implementation with discord API
- # TODO: waiting for dpy
- logger.warning("Failed to retrieve audit log: %s.", e)
- return
-
- if mod == self.user:
- return
-
if isinstance(channel, discord.CategoryChannel):
if self.main_category == channel:
logger.debug("Main category was deleted.")
@@ -1043,6 +1025,19 @@ async def on_guild_channel_delete(self, channel):
await self.config.update()
return
+ audit_logs = self.modmail_guild.audit_logs(
+ limit=10, action=discord.AuditLogAction.channel_delete
+ )
+ entry = await audit_logs.find(lambda a: int(a.target.id) == channel.id)
+
+ if entry is None:
+ logger.debug("Cannot find the audit log entry for channel delete of %d.", channel.id)
+ return
+
+ mod = entry.user
+ if mod == self.user:
+ return
+
thread = await self.threads.find(channel=channel)
if thread and thread.channel == channel:
logger.debug("Manually closed channel %s.", channel.name)
@@ -1070,20 +1065,60 @@ async def on_member_join(self, member):
async def on_message_delete(self, message):
"""Support for deleting linked messages"""
- # TODO: use audit log to check if modmail deleted the message
- if message.embeds and not isinstance(message.channel, discord.DMChannel):
- thread = await self.threads.find(channel=message.channel)
+
+ if message.is_system():
+ return
+
+ if isinstance(message.channel, discord.DMChannel):
+ if message.author == self.user:
+ return
+ thread = await self.threads.find(recipient=message.author)
+ if not thread:
+ return
try:
- await thread.delete_message(message)
+ message = await thread.find_linked_message_from_dm(message)
except ValueError as e:
- if str(e) not in {"DM message not found.", " Malformed thread message."}:
- logger.warning("Failed to find linked message to delete: %s", e)
- else:
- thread = await self.threads.find(recipient=message.author)
- message = await thread.find_linked_message_from_dm(message)
+ if str(e) != "Thread channel message not found.":
+ logger.debug("Failed to find linked message to delete: %s", e)
+ return
embed = message.embeds[0]
embed.set_footer(text=f"{embed.footer.text} (deleted)", icon_url=embed.footer.icon_url)
await message.edit(embed=embed)
+ return
+
+ if message.author != self.user:
+ return
+
+ thread = await self.threads.find(channel=message.channel)
+ if not thread:
+ return
+
+ audit_logs = self.modmail_guild.audit_logs(
+ limit=10, action=discord.AuditLogAction.message_delete
+ )
+
+ entry = await audit_logs.find(lambda a: a.target == self.user)
+
+ if entry is None:
+ return
+
+ try:
+ await thread.delete_message(message, note=False)
+ embed = discord.Embed(
+ description="Successfully deleted message.", color=self.main_color
+ )
+ except ValueError as e:
+ if str(e) not in {"DM message not found.", "Malformed thread message."}:
+ logger.debug("Failed to find linked message to delete: %s", e)
+ embed = discord.Embed(
+ description="Failed to delete message.", color=self.error_color
+ )
+ else:
+ return
+ except discord.NotFound:
+ return
+ embed.set_footer(text=f"Message ID: {message.id} from {message.author}.")
+ return await message.channel.send(embed=embed)
async def on_bulk_message_delete(self, messages):
await discord.utils.async_all(self.on_message_delete(msg) for msg in messages)
@@ -1096,10 +1131,13 @@ async def on_message_edit(self, before, after):
if isinstance(after.channel, discord.DMChannel):
thread = await self.threads.find(recipient=before.author)
+ if not thread:
+ return
+
try:
await thread.edit_dm_message(after, after.content)
except ValueError:
- x, blocked_emoji = await self.retrieve_emoji()
+ _, blocked_emoji = await self.retrieve_emoji()
await self.add_reaction(after, blocked_emoji)
else:
embed = discord.Embed(
@@ -1147,45 +1185,17 @@ async def on_command_error(self, context, exception):
corrected_permission_level.name,
)
logger.warning("CheckFailure: %s", exception)
+ elif isinstance(exception, commands.DisabledCommand):
+ logger.info(
+ "DisabledCommand: %s is trying to run eval but it's disabled", context.author.name
+ )
else:
logger.error("Unexpected exception:", exc_info=exception)
- async def validate_database_connection(self):
- try:
- await self.db.command("buildinfo")
- except Exception as exc:
- logger.critical("Something went wrong while connecting to the database.")
- message = f"{type(exc).__name__}: {str(exc)}"
- logger.critical(message)
-
- if "ServerSelectionTimeoutError" in message:
- logger.critical(
- "This may have been caused by not whitelisting "
- "IPs correctly. Make sure to whitelist all "
- "IPs (0.0.0.0/0) https://i.imgur.com/mILuQ5U.png"
- )
-
- if "OperationFailure" in message:
- logger.critical(
- "This is due to having invalid credentials in your MONGO_URI. "
- "Remember you need to substitute `j3_>C%C@4K9J&g^@vB N&Lnk1
z?L%MCI<{-iO3X%KV`0^;;3Y8~>W(I)`aXkhLjx0_Yv3X{EpE*+AHBuG^_g%pu7t1UsqUeJ`4xow4Intf*IXH24&Q?&s(z
zeOtMo{R&_k9#T~);n7Ksq3(WA^WXlHylDMeJeU74$7$QntMEScFyT?270J(2)!WyM
zSk9k-Zr}IYac8;xB=qFbJJY8s?qX|XG@}vl4{>Qr(iW%Sx-y+dh!HB)Y!{gq8;;}-
zsJ*>U0~-cWJU+L%f2c|5u9x*KC`6~v6=-aq)T63z(>3t};m^y;zAWX{0j8k+Y7Q!4
z*UcdXv8r9wI=FQ*8T@DeA368#0TiQn)xV8|gqDb30!V&X+8|$f$$Dg!f>Jg<`UaeJ
zv>y~{3%${bg*ADLv%5Lemb6C4AY9y2-h|Tuib4$Er*9d{Tc)ky#zVhOqd2LmBsL#
zmms`ItCJtF7M6`r^_lxwBg