Skip to content

Commit 37d911f

Browse files
authored
Merge pull request #7 from khakers/feature/upstream-4.1.0-merge
2 parents 88a41f9 + 3ee6041 commit 37d911f

File tree

15 files changed

+468
-191
lines changed

15 files changed

+468
-191
lines changed

CHANGELOG.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ however, insignificant breaking changes do not guarantee a major version bump, s
1717

1818
### Added
1919
- Added `content_type` to attachments stored in the database.
20+
- `?log key <key>` to retrieve the log link and view a preview using a log key. ([PR #3196](https://github.com/modmail-dev/Modmail/pull/3196))
21+
2022

2123
### Changed
2224
- Changing a threads title or NSFW status immediately updates the status in the database.
@@ -33,6 +35,7 @@ however, insignificant breaking changes do not guarantee a major version bump, s
3335
- Persistent notes have been fixed after the previous discord.py update.
3436
- `is_image` now is true only if the image is actually an image.
3537
- Fix contact command reporting user was blocked when they weren't.
38+
- Cleanup imports after removing/unloading a plugin. ([PR #3226](https://github.com/modmail-dev/Modmail/pull/3226))
3639

3740
### Internal
3841
- Add `update_title` and `update_nsfw` methods to `ApiClient` to update thread title and nsfw status in the database.
@@ -52,6 +55,7 @@ however, insignificant breaking changes do not guarantee a major version bump, s
5255
- Support for trailing space in `?prefix` command, example: `?prefix "mm "` for `mm ping`.
5356
- Added logviewer as built-in local plugin `?plugin load @local/logviewer`.
5457
- `?plugin uninstall` is now an alias for `?plugin remove` ([GH #3260](https://github.com/modmail-dev/modmail/issues/3260))
58+
- `DISCORD_LOG_LEVEL` environment variable to set the log level of discord.py. ([PR #3216](https://github.com/modmail-dev/Modmail/pull/3216))
5559

5660
### Changed
5761
- Guild icons in embed footers and author urls now have a fixed size of 128. ([PR #3261](https://github.com/modmail-dev/modmail/pull/3261))
@@ -76,10 +80,15 @@ however, insignificant breaking changes do not guarantee a major version bump, s
7680
- Fixed uncached member issue in large guild for react_to_contact and ticket creation.
7781
- Fixed blocked roles improperly saving in `blocked_users` config.
7882
- Fixed `?block` command improperly parsing reason as timestamp.
83+
- Rate limit issue when fetch the messages due to reaction linking. ([PR #3306](https://github.com/modmail-dev/Modmail/pull/3306))
84+
- Update command fails when the plugin is invalid. ([PR #3295](https://github.com/modmail-dev/Modmail/pull/3295))
7985

8086
### Internal
8187
- `ConfigManager.get` no longer accepts two positional arguments: the `convert` argument is now keyword-only.
8288

89+
### Internal
90+
- Renamed `Bot.log_file_name` to `Bot.log_file_path`. Log files are now created at `temp/logs/modmail.log`. ([PR #3216](https://github.com/modmail-dev/Modmail/pull/3216))
91+
8392
# v4.0.2
8493

8594
### Breaking

app.json

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,6 @@
1111
"description": "The id for the server you are hosting this bot for.",
1212
"required": true
1313
},
14-
"MODMAIL_GUILD_ID": {
15-
"description": "The ID of the discord server where the threads channels should be created (receiving server). Default to GUILD_ID.",
16-
"required": false
17-
},
1814
"OWNERS": {
1915
"description": "Comma separated user IDs of people that are allowed to use owner only commands. (eval).",
2016
"required": true
@@ -68,4 +64,4 @@
6864
"required": false
6965
}
7066
}
71-
}
67+
}

bot.py

Lines changed: 59 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222
from discord.ext.commands import MemberConverter
2323
from discord.ext.commands.view import StringView
2424
from emoji import UNICODE_EMOJI
25-
from pkg_resources import parse_version
25+
from packaging.version import Version
2626

2727
from core.blocklist import Blocklist, BlockReason
2828

@@ -49,11 +49,10 @@
4949
)
5050
from core.thread import ThreadManager
5151
from core.time import human_timedelta
52-
from core.utils import normalize_alias, parse_alias, truncate, tryint
52+
from core.utils import human_join, normalize_alias, parse_alias, truncate, tryint
5353

5454
logger = getLogger(__name__)
5555

56-
5756
temp_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), "temp")
5857
if not os.path.exists(temp_dir):
5958
os.mkdir(temp_dir)
@@ -85,8 +84,11 @@ def __init__(self):
8584

8685
self.threads = ThreadManager(self)
8786

88-
self.log_file_name = os.path.join(temp_dir, f"{self.token.split('.')[0]}.log")
89-
self._configure_logging()
87+
log_dir = os.path.join(temp_dir, "logs")
88+
if not os.path.exists(log_dir):
89+
os.mkdir(log_dir)
90+
self.log_file_path = os.path.join(log_dir, "modmail.log")
91+
configure_logging(self)
9092

9193
self.plugin_db = PluginDatabaseClient(self) # Deprecated
9294

@@ -186,32 +188,9 @@ async def load_extensions(self):
186188
logger.exception("Failed to load %s.", cog)
187189
logger.line("debug")
188190

189-
def _configure_logging(self):
190-
level_text = self.config["log_level"].upper()
191-
logging_levels = {
192-
"CRITICAL": logging.CRITICAL,
193-
"ERROR": logging.ERROR,
194-
"WARNING": logging.WARNING,
195-
"INFO": logging.INFO,
196-
"DEBUG": logging.DEBUG,
197-
}
198-
logger.line()
199-
200-
log_level = logging_levels.get(level_text)
201-
if log_level is None:
202-
log_level = self.config.remove("log_level")
203-
logger.warning("Invalid logging level set: %s.", level_text)
204-
logger.warning("Using default logging level: INFO.")
205-
else:
206-
logger.info("Logging level: %s", level_text)
207-
208-
logger.info("Log file: %s", self.log_file_name)
209-
configure_logging(self.log_file_name, log_level)
210-
logger.debug("Successfully configured logging.")
211-
212191
@property
213192
def version(self):
214-
return parse_version(__version__)
193+
return Version(__version__)
215194

216195
@property
217196
def api(self) -> ApiClient:
@@ -1268,33 +1247,44 @@ async def handle_reaction_events(self, payload):
12681247
return
12691248

12701249
channel = self.get_channel(payload.channel_id)
1271-
if not channel: # dm channel not in internal cache
1272-
_thread = await self.threads.find(recipient=user)
1273-
if not _thread:
1250+
thread = None
1251+
# dm channel not in internal cache
1252+
if not channel:
1253+
thread = await self.threads.find(recipient=user)
1254+
if not thread:
1255+
return
1256+
channel = await thread.recipient.create_dm()
1257+
if channel.id != payload.channel_id:
12741258
return
1275-
channel = await _thread.recipient.create_dm()
12761259

1260+
from_dm = isinstance(channel, discord.DMChannel)
1261+
from_txt = isinstance(channel, discord.TextChannel)
1262+
if not from_dm and not from_txt:
1263+
return
1264+
1265+
if not thread:
1266+
params = {"recipient": user} if from_dm else {"channel": channel}
1267+
thread = await self.threads.find(**params)
1268+
if not thread:
1269+
return
1270+
1271+
# thread must exist before doing this API call
12771272
try:
12781273
message = await channel.fetch_message(payload.message_id)
12791274
except (discord.NotFound, discord.Forbidden):
12801275
return
12811276

12821277
reaction = payload.emoji
1283-
12841278
close_emoji = await self.convert_emoji(self.config["close_emoji"])
1285-
1286-
if isinstance(channel, discord.DMChannel):
1287-
thread = await self.threads.find(recipient=user)
1288-
if not thread:
1289-
return
1279+
if from_dm:
12901280
if (
12911281
payload.event_type == "REACTION_ADD"
12921282
and message.embeds
12931283
and str(reaction) == str(close_emoji)
12941284
and self.config.get("recipient_thread_close")
12951285
):
12961286
ts = message.embeds[0].timestamp
1297-
if thread and ts == thread.channel.created_at:
1287+
if ts == thread.channel.created_at:
12981288
# the reacted message is the corresponding thread creation embed
12991289
# closing thread
13001290
return await thread.close(closer=user)
@@ -1314,11 +1304,10 @@ async def handle_reaction_events(self, payload):
13141304
logger.warning("Failed to find linked message for reactions: %s", e)
13151305
return
13161306
else:
1317-
thread = await self.threads.find(channel=channel)
1318-
if not thread:
1319-
return
13201307
try:
1321-
_, *linked_messages = await thread.find_linked_messages(message.id, either_direction=True)
1308+
_, *linked_messages = await thread.find_linked_messages(
1309+
message1=message, either_direction=True
1310+
)
13221311
except ValueError as e:
13231312
logger.warning("Failed to find linked message for reactions: %s", e)
13241313
return
@@ -1428,28 +1417,44 @@ async def on_guild_channel_delete(self, channel):
14281417
await thread.close(closer=mod, silent=True, delete_channel=False)
14291418

14301419
async def on_member_remove(self, member):
1431-
if member.guild != self.guild:
1432-
return
14331420
thread = await self.threads.find(recipient=member)
14341421
if thread:
1435-
if self.config["close_on_leave"]:
1422+
if member.guild == self.guild and self.config["close_on_leave"]:
14361423
await thread.close(
14371424
closer=member.guild.me,
14381425
message=self.config["close_on_leave_reason"],
14391426
silent=True,
14401427
)
14411428
else:
1442-
embed = discord.Embed(
1443-
description=self.config["close_on_leave_reason"], color=self.error_color
1444-
)
1429+
if len(self.guilds) > 1:
1430+
guild_left = member.guild
1431+
remaining_guilds = member.mutual_guilds
1432+
1433+
if remaining_guilds:
1434+
remaining_guild_names = [guild.name for guild in remaining_guilds]
1435+
leave_message = (
1436+
f"The recipient has left {guild_left}. "
1437+
f"They are still in {human_join(remaining_guild_names, final='and')}."
1438+
)
1439+
else:
1440+
leave_message = (
1441+
f"The recipient has left {guild_left}. We no longer share any mutual servers."
1442+
)
1443+
else:
1444+
leave_message = "The recipient has left the server."
1445+
1446+
embed = discord.Embed(description=leave_message, color=self.error_color)
14451447
await thread.channel.send(embed=embed)
14461448

14471449
async def on_member_join(self, member):
1448-
if member.guild != self.guild:
1449-
return
14501450
thread = await self.threads.find(recipient=member)
14511451
if thread:
1452-
embed = discord.Embed(description="The recipient has joined the server.", color=self.mod_color)
1452+
if len(self.guilds) > 1:
1453+
guild_joined = member.guild
1454+
join_message = f"The recipient has joined {guild_joined}."
1455+
else:
1456+
join_message = "The recipient has joined the server."
1457+
embed = discord.Embed(description=join_message, color=self.mod_color)
14531458
await thread.channel.send(embed=embed)
14541459

14551460
async def on_message_delete(self, message):
@@ -1583,7 +1588,7 @@ async def autoupdate(self):
15831588
changelog = await Changelog.from_url(self)
15841589
latest = changelog.latest_version
15851590

1586-
if self.version < parse_version(latest.version):
1591+
if self.version < Version(latest.version):
15871592
error = None
15881593
data = {}
15891594
try:
@@ -1755,16 +1760,6 @@ def main():
17551760
except ImportError:
17561761
pass
17571762

1758-
# Set up discord.py internal logging
1759-
if os.environ.get("LOG_DISCORD"):
1760-
logger.debug(f"Discord logging enabled: {os.environ['LOG_DISCORD'].upper()}")
1761-
d_logger = logging.getLogger("discord")
1762-
1763-
d_logger.setLevel(os.environ["LOG_DISCORD"].upper())
1764-
handler = logging.FileHandler(filename="discord.log", encoding="utf-8", mode="w")
1765-
handler.setFormatter(logging.Formatter("%(asctime)s:%(levelname)s:%(name)s: %(message)s"))
1766-
d_logger.addHandler(handler)
1767-
17681763
bot = ModmailBot()
17691764
bot.run()
17701765

cogs/modmail.py

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22
import re
33
from datetime import timezone
44
from itertools import zip_longest
5-
from types import SimpleNamespace
65
from typing import List, Literal, Optional, Tuple, Union
76

87
import discord
@@ -1166,7 +1165,7 @@ async def logs(self, ctx, *, user: User = None):
11661165
if not user:
11671166
thread = ctx.thread
11681167
if not thread:
1169-
raise commands.MissingRequiredArgument(SimpleNamespace(name="member"))
1168+
raise commands.MissingRequiredArgument(DummyParam("user"))
11701169
user = thread.recipient or await self.bot.get_or_fetch_user(thread.id)
11711170

11721171
default_avatar = "https://cdn.discordapp.com/embed/avatars/0.png"
@@ -1212,6 +1211,28 @@ async def logs_closed_by(self, ctx, *, user: User = None):
12121211
session = EmbedPaginatorSession(ctx, *embeds)
12131212
await session.run()
12141213

1214+
@logs.command(name="key", aliases=["id"])
1215+
@checks.has_permissions(PermissionLevel.SUPPORTER)
1216+
async def logs_key(self, ctx, key: str):
1217+
"""
1218+
Get the log link for the specified log key.
1219+
"""
1220+
icon_url = ctx.author.avatar.url
1221+
1222+
logs = await self.bot.api.find_log_entry(key)
1223+
1224+
if not logs:
1225+
embed = discord.Embed(
1226+
color=self.bot.error_color,
1227+
description=f"Log entry `{key}` not found.",
1228+
)
1229+
return await ctx.send(embed=embed)
1230+
1231+
embeds = self.format_log_embeds(logs, avatar_url=icon_url)
1232+
1233+
session = EmbedPaginatorSession(ctx, *embeds)
1234+
await session.run()
1235+
12151236
@logs.command(name="delete", aliases=["wipe"])
12161237
@checks.has_permissions(PermissionLevel.OWNER)
12171238
async def logs_delete(self, ctx, key_or_link: str):
@@ -1802,7 +1823,7 @@ async def block(
18021823
user_or_role = ctx.thread.recipient if (ctx.thread and not user_or_role) else user_or_role
18031824

18041825
if not user_or_role:
1805-
raise commands.MissingRequiredArgument(SimpleNamespace(name="user"))
1826+
raise commands.MissingRequiredArgument(DummyParam("user"))
18061827

18071828
mention = getattr(user_or_role, "mention", f"`{user_or_role.id}`")
18081829

@@ -1862,7 +1883,7 @@ async def unblock(self, ctx, *, user_or_role: Union[discord.User, discord.Role,
18621883
user_or_role = ctx.thread.recipient if (ctx.thread and not user_or_role) else user_or_role
18631884

18641885
if not user_or_role:
1865-
raise commands.MissingRequiredArgument(SimpleNamespace(name="user"))
1886+
raise commands.MissingRequiredArgument(DummyParam("user or role"))
18661887

18671888
mention = getattr(user_or_role, "mention", f"`{user_or_role.id}`")
18681889

0 commit comments

Comments
 (0)