Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion lightning/cogs/emoji.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,8 @@
from lightning import (CommandLevel, GuildContext, LightningBot, LightningCog,
LightningContext, command, errors, group)
from lightning.converters import EmojiRE, Whitelisted_URL
from lightning.modlogformats import action_format
from lightning.utils.checks import has_guild_permissions
from lightning.utils.modlogformats import action_format

log = logging.getLogger(__name__)

Expand Down
2 changes: 1 addition & 1 deletion lightning/cogs/infractions/cog.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,9 +43,9 @@
from lightning.errors import LightningCommandError
from lightning.events import InfractionDeleteEvent, InfractionUpdateEvent
from lightning.models import InfractionRecord
from lightning.modlogformats import base_user_format
from lightning.utils.checks import hybrid_guild_permissions, is_server_manager
from lightning.utils.helpers import ticker
from lightning.utils.modlogformats import base_user_format
from lightning.utils.time import add_tzinfo

matplotlib.use('Agg')
Expand Down
2 changes: 1 addition & 1 deletion lightning/cogs/infractions/ui.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
from lightning.enums import ActionType
from lightning.formatters import truncate_text
from lightning.models import InfractionRecord
from lightning.utils.modlogformats import base_user_format
from lightning.modlogformats import base_user_format
from lightning.utils.paginator import Paginator
from lightning.utils.time import add_tzinfo

Expand Down
4 changes: 2 additions & 2 deletions lightning/cogs/mod/cog.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@

from lightning import (CommandLevel, GuildContext, LightningBot, LightningCog,
LightningContext, cache, command, converters, group,
hybrid_command)
hybrid_command, modlogformats)
from lightning.cogs.mod.converters import BannedMember
from lightning.cogs.mod.flags import BanFlags, DefaultModFlags, PurgeFlags
from lightning.constants import COMMON_HOIST_CHARACTERS
Expand All @@ -38,7 +38,7 @@
from lightning.events import InfractionEvent
from lightning.formatters import plural, truncate_text
from lightning.models import GuildModConfig, PartialGuild, Timer
from lightning.utils import helpers, modlogformats
from lightning.utils import helpers
from lightning.utils.checks import (has_channel_permissions,
has_guild_permissions,
hybrid_guild_permissions)
Expand Down
184 changes: 46 additions & 138 deletions lightning/cogs/modlog/cog.py

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion lightning/cogs/reports/ui.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,8 @@
from lightning.enums import ActionType, AutoModPunishmentType
from lightning.errors import LightningError
from lightning.formatters import truncate_text
from lightning.modlogformats import construct_dm_message
from lightning.ui import ExitableMenu, MenuLikeView, UpdateableMenu, _BaseView
from lightning.utils.modlogformats import construct_dm_message
from lightning.utils.time import FutureTime, add_tzinfo

if TYPE_CHECKING:
Expand Down
2 changes: 1 addition & 1 deletion lightning/cogs/stats.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,9 @@
from lightning.constants import LIGHTNING_COLOR
from lightning.converters import InbetweenNumber
from lightning.models import ActionType
from lightning.modlogformats import base_user_format
from lightning.utils.checks import has_guild_permissions
from lightning.utils.emitters import WebhookEmbedEmitter
from lightning.utils.modlogformats import base_user_format
from lightning.utils.time import natural_timedelta

if TYPE_CHECKING:
Expand Down
43 changes: 40 additions & 3 deletions lightning/events.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,15 @@
"""
from __future__ import annotations

from datetime import datetime
from typing import List, Optional, Union

import discord

from lightning.enums import ActionType
from lightning.formatters import base_user_format
from lightning.models import Action, InfractionRecord
from lightning.utils.helpers import ticker
from lightning.utils.modlogformats import base_user_format
from lightning.utils.time import add_tzinfo

# These are event models that'll be passed for listeners cog
Expand Down Expand Up @@ -233,8 +234,6 @@ def format_infraction(self) -> discord.Embed:

return embed

# These won't be used yet until I do a refactor of modlogformats for single param events


# lightning_command_ran
class CommandEvent:
Expand All @@ -245,3 +244,41 @@ def __init__(self, ctx):
self.user = ctx.author
self.ran_at = ctx.message.created_at
self.channel = ctx.channel # We can get guild from here...


# These are only ever used by the modlog renderers. Nothing dispatches them, the modlog cog
# builds them from the discord.py events it already gets so each renderer can take a single event.
class MemberEvent:
__slots__ = ("member",)

def __init__(self, member: discord.Member) -> None:
self.member = member

@property
def guild(self) -> discord.Guild:
return self.member.guild


class MemberJoinEvent(MemberEvent):
__slots__ = ()


class MemberLeaveEvent(MemberEvent):
__slots__ = ()


# lightning_member_passed_screening
class MemberScreeningEvent(MemberEvent):
__slots__ = ()


# lightning_timed_moderation_action_done
class TimedActionExpiredEvent:
__slots__ = ("action", "user", "moderator", "created_at", "expiry")

def __init__(self, action: str, user, moderator, created_at: datetime, expiry: Optional[datetime]) -> None:
self.action = action
self.user = user
self.moderator = moderator
self.created_at = created_at
self.expiry = expiry
52 changes: 52 additions & 0 deletions lightning/formatters.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@
along with this program. If not, see <https://www.gnu.org/licenses/>.
"""

from datetime import datetime

import discord


def codeblock(text: str, *, language: str = "py") -> str:
return f"```{language}\n{text}```"
Expand Down Expand Up @@ -54,3 +58,51 @@ def human_join(seq, delim=', ', conj='or') -> str:
return f'{seq[0]} {conj} {seq[1]}'

return delim.join(seq[:-1]) + f' {conj} {seq[-1]}'


# These live here (instead of in modlogformats) so lightning.events can use them without
# importing the renderers, which import the events. That's what would cause a circular import.
def escape_markdown_and_mentions(text) -> str:
"""Helper function to escape mentions and markdown from a string

Parameters
----------
text : str
The string to remove markdown and mentions from

Returns
-------
str
The escaped text
"""
escaped_markdown = discord.utils.escape_markdown(text)
return discord.utils.escape_mentions(escaped_markdown)


def action_format(author, action_text="Action done by", *, reason=None) -> str:
if reason is None:
return f"{action_text} {str(author)} (ID: {author.id})"
else:
return f"{str(author)} (ID: {author.id}): {reason}"


def base_user_format(user, unknown_text="Unknown user with ID: ") -> str:
if hasattr(user, 'name'):
return f"{escape_markdown_and_mentions(str(user))} ({user.id})"
else:
return f"{unknown_text}{user.id if hasattr(user, 'id') else user}"


def format_timestamp(dt: datetime) -> str:
return discord.utils.format_dt(dt, style="T")


def construct_dm_message(member, action_verb, location, *, middle=None, reason=None, ending=None):
msg = [f"You were {action_verb} {location} {member.guild.name}"]
if middle:
msg.append(middle)
if reason:
msg.append(f"\n\n**Reason**: {reason}")
if ending:
msg.append(f"\n{ending}")
return ''.join(msg)
58 changes: 58 additions & 0 deletions lightning/modlogformats/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
"""
Lightning.py - A Discord bot
Copyright (C) 2019-present Célveren

This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation at version 3 of the License.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.

You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
"""
from lightning.formatters import (action_format, base_user_format,
construct_dm_message,
escape_markdown_and_mentions,
format_timestamp)
from lightning.modlogformats.base import (FormatContext, FormatName, Renderer,
get_renderer, renders)
# Importing these is what registers the renderers with @renders
from lightning.modlogformats.commands import CommandRan
from lightning.modlogformats.member import (MemberJoin, MemberLeave,
MemberScreening, NickChange,
RoleChange)
from lightning.modlogformats.moderation import (CompactModAction,
InfractionDelete,
InfractionUpdate, ModAction,
TimedActionExpired,
TimeoutExpired, log_actions)

__all__ = (
"CommandRan",
"CompactModAction",
"FormatContext",
"FormatName",
"InfractionDelete",
"InfractionUpdate",
"MemberJoin",
"MemberLeave",
"MemberScreening",
"ModAction",
"NickChange",
"Renderer",
"RoleChange",
"TimedActionExpired",
"TimeoutExpired",
"action_format",
"base_user_format",
"construct_dm_message",
"escape_markdown_and_mentions",
"format_timestamp",
"get_renderer",
"log_actions",
"renders",
)
111 changes: 111 additions & 0 deletions lightning/modlogformats/base.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
"""
Lightning.py - A Discord bot
Copyright (C) 2019-present Célveren

This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation at version 3 of the License.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.

You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
"""
from __future__ import annotations

from dataclasses import dataclass
from typing import Callable, Generic, Literal, Type, TypeVar, Union

import discord

from lightning.formatters import format_timestamp

FormatName = Literal["emoji", "minimal", "embed"]


@dataclass(frozen=True)
class FormatContext:
"""How a guild wants its modlog entries to look.

with_timestamp only means something to the minimal format, so it lives here
instead of being a kwarg that the other two formats would have to ignore.
"""
fmt: FormatName
with_timestamp: bool = True

@classmethod
def from_setting(cls, setting: str) -> FormatContext:
"""Builds a context from the format string we store in the database"""
if setting in ("minimal with timestamp", "minimal without timestamp"):
return cls("minimal", setting != "minimal without timestamp")
if setting not in ("emoji", "embed"):
raise ValueError(f"Unknown modlog format: {setting}")
return cls(setting) # type: ignore


E = TypeVar("E")


class Renderer(Generic[E]):
"""Base class for a modlog renderer. Subclasses are registered with :func:`renders`.

One renderer handles one kind of event, and knows how to show it in every format.
"""
def __init__(self, event: E) -> None:
self.event = event

def mentions(self) -> list:
"""Who the emoji format is allowed to ping."""
return []

def emoji(self) -> str:
raise NotImplementedError

def minimal(self, ctx: FormatContext) -> str:
raise NotImplementedError

def embed(self) -> discord.Embed:
raise NotImplementedError

def stamp(self, ctx: FormatContext, when=None) -> str:
"""The "[time] " prefix that the minimal format puts at the front, if the guild wants it."""
if not ctx.with_timestamp:
return ""
return f"[{format_timestamp(when or discord.utils.utcnow())}] "

def render(self, ctx: FormatContext) -> Union[str, discord.Embed]:
# This is the only spot that knows which format maps to which method
if ctx.fmt == "emoji":
return self.emoji()
if ctx.fmt == "minimal":
return self.minimal(ctx)
return self.embed()


# Each renderer tags itself with @renders(SomeEvent) so there's no lookup table to keep up by hand,
# and the cog never has to know which renderers exist.
_renderers: dict[type, Type[Renderer]] = {}
_R = TypeVar("_R", bound=Type[Renderer])


def renders(event_type: type) -> Callable[[_R], _R]:
def decorator(cls: _R) -> _R:
_renderers[event_type] = cls
return cls
return decorator


def get_renderer(event) -> Renderer:
"""Finds the renderer for an event.

We walk the MRO so a subclass of an event with a renderer (ex. LightningAutoModInfractionEvent)
still works, but a renderer for the subclass itself (ex. MemberRolesUpdateEvent) wins.
"""
for cls in type(event).__mro__:
if cls in _renderers:
return _renderers[cls](event)

raise LookupError(f"No modlog renderer registered for {type(event).__name__}")
Loading
Loading