Skip to content

Commit

Permalink
Merge pull request #7 from khakers/feature/upstream-4.1.0-merge
Browse files Browse the repository at this point in the history
  • Loading branch information
khakers committed Jun 18, 2024
2 parents 88a41f9 + 3ee6041 commit 37d911f
Show file tree
Hide file tree
Showing 15 changed files with 468 additions and 191 deletions.
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ however, insignificant breaking changes do not guarantee a major version bump, s

### Added
- Added `content_type` to attachments stored in the database.
- `?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))


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

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

### Changed
- 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))
Expand All @@ -76,10 +80,15 @@ however, insignificant breaking changes do not guarantee a major version bump, s
- Fixed uncached member issue in large guild for react_to_contact and ticket creation.
- Fixed blocked roles improperly saving in `blocked_users` config.
- Fixed `?block` command improperly parsing reason as timestamp.
- Rate limit issue when fetch the messages due to reaction linking. ([PR #3306](https://github.com/modmail-dev/Modmail/pull/3306))
- Update command fails when the plugin is invalid. ([PR #3295](https://github.com/modmail-dev/Modmail/pull/3295))

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

### Internal
- 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))

# v4.0.2

### Breaking
Expand Down
6 changes: 1 addition & 5 deletions app.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,6 @@
"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
Expand Down Expand Up @@ -68,4 +64,4 @@
"required": false
}
}
}
}
123 changes: 59 additions & 64 deletions bot.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
from discord.ext.commands import MemberConverter
from discord.ext.commands.view import StringView
from emoji import UNICODE_EMOJI
from pkg_resources import parse_version
from packaging.version import Version

from core.blocklist import Blocklist, BlockReason

Expand All @@ -49,11 +49,10 @@
)
from core.thread import ThreadManager
from core.time import human_timedelta
from core.utils import normalize_alias, parse_alias, truncate, tryint
from core.utils import human_join, normalize_alias, parse_alias, truncate, tryint

logger = getLogger(__name__)


temp_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), "temp")
if not os.path.exists(temp_dir):
os.mkdir(temp_dir)
Expand Down Expand Up @@ -85,8 +84,11 @@ def __init__(self):

self.threads = ThreadManager(self)

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

self.plugin_db = PluginDatabaseClient(self) # Deprecated

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

def _configure_logging(self):
level_text = self.config["log_level"].upper()
logging_levels = {
"CRITICAL": logging.CRITICAL,
"ERROR": logging.ERROR,
"WARNING": logging.WARNING,
"INFO": logging.INFO,
"DEBUG": logging.DEBUG,
}
logger.line()

log_level = logging_levels.get(level_text)
if log_level is None:
log_level = self.config.remove("log_level")
logger.warning("Invalid logging level set: %s.", level_text)
logger.warning("Using default logging level: INFO.")
else:
logger.info("Logging level: %s", level_text)

logger.info("Log file: %s", self.log_file_name)
configure_logging(self.log_file_name, log_level)
logger.debug("Successfully configured logging.")

@property
def version(self):
return parse_version(__version__)
return Version(__version__)

@property
def api(self) -> ApiClient:
Expand Down Expand Up @@ -1268,33 +1247,44 @@ async def handle_reaction_events(self, payload):
return

channel = self.get_channel(payload.channel_id)
if not channel: # dm channel not in internal cache
_thread = await self.threads.find(recipient=user)
if not _thread:
thread = None
# dm channel not in internal cache
if not channel:
thread = await self.threads.find(recipient=user)
if not thread:
return
channel = await thread.recipient.create_dm()
if channel.id != payload.channel_id:
return
channel = await _thread.recipient.create_dm()

from_dm = isinstance(channel, discord.DMChannel)
from_txt = isinstance(channel, discord.TextChannel)
if not from_dm and not from_txt:
return

if not thread:
params = {"recipient": user} if from_dm else {"channel": channel}
thread = await self.threads.find(**params)
if not thread:
return

# thread must exist before doing this API call
try:
message = await channel.fetch_message(payload.message_id)
except (discord.NotFound, discord.Forbidden):
return

reaction = payload.emoji

close_emoji = await self.convert_emoji(self.config["close_emoji"])

if isinstance(channel, discord.DMChannel):
thread = await self.threads.find(recipient=user)
if not thread:
return
if from_dm:
if (
payload.event_type == "REACTION_ADD"
and message.embeds
and str(reaction) == str(close_emoji)
and self.config.get("recipient_thread_close")
):
ts = message.embeds[0].timestamp
if thread and ts == thread.channel.created_at:
if ts == thread.channel.created_at:
# the reacted message is the corresponding thread creation embed
# closing thread
return await thread.close(closer=user)
Expand All @@ -1314,11 +1304,10 @@ async def handle_reaction_events(self, payload):
logger.warning("Failed to find linked message for reactions: %s", e)
return
else:
thread = await self.threads.find(channel=channel)
if not thread:
return
try:
_, *linked_messages = await thread.find_linked_messages(message.id, either_direction=True)
_, *linked_messages = await thread.find_linked_messages(
message1=message, either_direction=True
)
except ValueError as e:
logger.warning("Failed to find linked message for reactions: %s", e)
return
Expand Down Expand Up @@ -1428,28 +1417,44 @@ async def on_guild_channel_delete(self, channel):
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:
if self.config["close_on_leave"]:
if member.guild == self.guild and self.config["close_on_leave"]:
await thread.close(
closer=member.guild.me,
message=self.config["close_on_leave_reason"],
silent=True,
)
else:
embed = discord.Embed(
description=self.config["close_on_leave_reason"], color=self.error_color
)
if len(self.guilds) > 1:
guild_left = member.guild
remaining_guilds = member.mutual_guilds

if remaining_guilds:
remaining_guild_names = [guild.name for guild in remaining_guilds]
leave_message = (
f"The recipient has left {guild_left}. "
f"They are still in {human_join(remaining_guild_names, final='and')}."
)
else:
leave_message = (
f"The recipient has left {guild_left}. We no longer share any mutual servers."
)
else:
leave_message = "The recipient has left the server."

embed = discord.Embed(description=leave_message, color=self.error_color)
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(description="The recipient has joined the server.", color=self.mod_color)
if len(self.guilds) > 1:
guild_joined = member.guild
join_message = f"The recipient has joined {guild_joined}."
else:
join_message = "The recipient has joined the server."
embed = discord.Embed(description=join_message, color=self.mod_color)
await thread.channel.send(embed=embed)

async def on_message_delete(self, message):
Expand Down Expand Up @@ -1583,7 +1588,7 @@ async def autoupdate(self):
changelog = await Changelog.from_url(self)
latest = changelog.latest_version

if self.version < parse_version(latest.version):
if self.version < Version(latest.version):
error = None
data = {}
try:
Expand Down Expand Up @@ -1755,16 +1760,6 @@ def main():
except ImportError:
pass

# Set up discord.py internal logging
if os.environ.get("LOG_DISCORD"):
logger.debug(f"Discord logging enabled: {os.environ['LOG_DISCORD'].upper()}")
d_logger = logging.getLogger("discord")

d_logger.setLevel(os.environ["LOG_DISCORD"].upper())
handler = logging.FileHandler(filename="discord.log", encoding="utf-8", mode="w")
handler.setFormatter(logging.Formatter("%(asctime)s:%(levelname)s:%(name)s: %(message)s"))
d_logger.addHandler(handler)

bot = ModmailBot()
bot.run()

Expand Down
29 changes: 25 additions & 4 deletions cogs/modmail.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
import re
from datetime import timezone
from itertools import zip_longest
from types import SimpleNamespace
from typing import List, Literal, Optional, Tuple, Union

import discord
Expand Down Expand Up @@ -1166,7 +1165,7 @@ async def logs(self, ctx, *, user: User = None):
if not user:
thread = ctx.thread
if not thread:
raise commands.MissingRequiredArgument(SimpleNamespace(name="member"))
raise commands.MissingRequiredArgument(DummyParam("user"))
user = thread.recipient or await self.bot.get_or_fetch_user(thread.id)

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

@logs.command(name="key", aliases=["id"])
@checks.has_permissions(PermissionLevel.SUPPORTER)
async def logs_key(self, ctx, key: str):
"""
Get the log link for the specified log key.
"""
icon_url = ctx.author.avatar.url

logs = await self.bot.api.find_log_entry(key)

if not logs:
embed = discord.Embed(
color=self.bot.error_color,
description=f"Log entry `{key}` not found.",
)
return await ctx.send(embed=embed)

embeds = self.format_log_embeds(logs, avatar_url=icon_url)

session = EmbedPaginatorSession(ctx, *embeds)
await session.run()

@logs.command(name="delete", aliases=["wipe"])
@checks.has_permissions(PermissionLevel.OWNER)
async def logs_delete(self, ctx, key_or_link: str):
Expand Down Expand Up @@ -1802,7 +1823,7 @@ async def block(
user_or_role = ctx.thread.recipient if (ctx.thread and not user_or_role) else user_or_role

if not user_or_role:
raise commands.MissingRequiredArgument(SimpleNamespace(name="user"))
raise commands.MissingRequiredArgument(DummyParam("user"))

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

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

if not user_or_role:
raise commands.MissingRequiredArgument(SimpleNamespace(name="user"))
raise commands.MissingRequiredArgument(DummyParam("user or role"))

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

Expand Down
Loading

0 comments on commit 37d911f

Please sign in to comment.