1+ from __future__ import annotations
2+
13import typing
24from collections .abc import Callable , Coroutine , Iterable
3- from dataclasses import dataclass , field , replace
5+ from dataclasses import dataclass , field , replace as dataclass_replace
46from enum import Enum , auto
57
68import discord
@@ -25,60 +27,124 @@ class Event(Enum):
2527
2628
2729@dataclass
28- class FilterContext :
29- """A dataclass containing the information that should be filtered, and output information of the filtering."""
30-
31- # Input context
32- event : Event # The type of event
33- author : User | Member | None # Who triggered the event
34- channel : TextChannel | VoiceChannel | StageChannel | Thread | DMChannel | None # The channel involved
35- content : str | Iterable # What actually needs filtering. The Iterable type depends on the filter list.
36- message : Message | None # The message involved
37- embeds : list [Embed ] = field (default_factory = list ) # Any embeds involved
38- attachments : list [discord .Attachment | FileAttachment ] = field (default_factory = list ) # Any attachments sent.
30+ class FilterSource :
31+ """The source/sender metadata for a filtering context."""
32+
33+ event : Event
34+ author : User | Member | None
35+ channel : TextChannel | VoiceChannel | StageChannel | Thread | DMChannel | None
36+ message : Message | None
3937 before_message : Message | None = None
4038 message_cache : MessageCache | None = None
41- # Output context
42- dm_content : str = "" # The content to DM the invoker
43- dm_embed : str = "" # The embed description to DM the invoker
44- send_alert : bool = False # Whether to send an alert for the moderators
45- alert_content : str = "" # The content of the alert
46- alert_embeds : list [Embed ] = field (default_factory = list ) # Any embeds to add to the alert
47- action_descriptions : list [str ] = field (default_factory = list ) # What actions were taken
48- matches : list [str ] = field (default_factory = list ) # What exactly was found
49- notification_domain : str = "" # A domain to send the user for context
50- filter_info : dict [Filter , str ] = field (default_factory = dict ) # Additional info from a filter.
51- messages_deletion : bool = False # Whether the messages were deleted. Can't upload deletion log otherwise.
52- blocked_exts : set [str ] = field (default_factory = set ) # Any extensions blocked (used for snekbox)
53- potential_phish : dict [FilterList , set [str ]] = field (default_factory = dict )
54- # Additional actions to perform
39+
40+
41+ @dataclass
42+ class FilterContent :
43+ """The content being filtered."""
44+
45+ content : str | Iterable
46+ embeds : list [Embed ] = field (default_factory = list )
47+ attachments : list [discord .Attachment | FileAttachment ] = field (default_factory = list )
48+
49+
50+ @dataclass
51+ class FilterNotifications :
52+ """DM and alert content produced by filtering."""
53+
54+ dm_content : str = ""
55+ dm_embed : str = ""
56+ send_alert : bool = False
57+ alert_content : str = ""
58+ alert_embeds : list [Embed ] = field (default_factory = list )
59+ notification_domain : str = ""
60+ action_descriptions : list [str ] = field (default_factory = list )
61+
62+
63+ @dataclass
64+ class FilterActions :
65+ """Side effects and deletion metadata produced by filtering."""
66+
5567 additional_actions : list [Callable [[FilterContext ], Coroutine ]] = field (default_factory = list )
56- related_messages : set [Message ] = field (default_factory = set ) # Deletion will include these.
68+ messages_deletion : bool = False
69+ related_messages : set [Message ] = field (default_factory = set )
5770 related_channels : set [TextChannel | Thread | DMChannel ] = field (default_factory = set )
58- uploaded_attachments : dict [int , list [str ]] = field (default_factory = dict ) # Message ID to attachment URLs.
59- upload_deletion_logs : bool = True # Whether it's allowed to upload deletion logs.
71+ uploaded_attachments : dict [int , list [str ]] = field (default_factory = dict )
72+ upload_deletion_logs : bool = True
73+
74+
75+ @dataclass
76+ class FilterResults :
77+ """Filter match results and tracking data."""
78+
79+ matches : list [str ] = field (default_factory = list )
80+ filter_info : dict [Filter , str ] = field (default_factory = dict )
81+ blocked_exts : set [str ] = field (default_factory = set )
82+ potential_phish : dict [FilterList , set [str ]] = field (default_factory = dict )
83+
6084
61- def __post_init__ (self ):
62- # If it's in the context of a DM channel, self.channel won't be None, but self.channel.guild will.
63- self .in_guild = self .channel is None or self .channel .guild is not None
85+ class FilterContext :
86+ """A context object containing the information that should be filtered, and output information of the filtering.
87+
88+ Attributes are delegated to sub-objects for organization:
89+ - ``source``: event, author, channel, message, before_message, message_cache
90+ - ``content``: content, embeds, attachments
91+ - ``notifications``: dm_content, dm_embed, send_alert, alert_content, alert_embeds, notification_domain, action_descriptions
92+ - ``actions``: additional_actions, messages_deletion, related_messages, related_channels, uploaded_attachments, upload_deletion_logs
93+ - ``results``: matches, filter_info, blocked_exts, potential_phish
94+ """
95+
96+ def __init__ (self , source , content , notifications = None , actions = None , results = None ):
97+ self ._source = source
98+ self ._content = content
99+ self ._notifications = notifications or FilterNotifications ()
100+ self ._actions = actions or FilterActions ()
101+ self ._results = results or FilterResults ()
102+ self .in_guild = source .channel is None or source .channel .guild is not None
103+
104+ def __getattr__ (self , name ):
105+ for obj in (self ._source , self ._content , self ._notifications , self ._actions , self ._results ):
106+ if hasattr (obj , name ):
107+ return getattr (obj , name )
108+ raise AttributeError (f"'{ type (self ).__name__ } ' has no attribute '{ name } '" )
109+
110+ def __setattr__ (self , name , value ):
111+ if name .startswith ('_' ) or name == 'in_guild' :
112+ object .__setattr__ (self , name , value )
113+ return
114+ for obj in (self ._source , self ._content , self ._notifications , self ._actions , self ._results ):
115+ if hasattr (obj , name ):
116+ setattr (obj , name , value )
117+ return
118+ object .__setattr__ (self , name , value )
64119
65120 @classmethod
66121 def from_message (
67122 cls , event : Event , message : Message , before : Message | None = None , cache : MessageCache | None = None
68123 ) -> FilterContext :
69124 """Create a filtering context from the attributes of a message."""
70- return cls (
71- event ,
72- message .author ,
73- message .channel ,
74- message .content ,
75- message ,
76- message .embeds ,
77- message .attachments ,
78- before ,
79- cache
80- )
125+ source = FilterSource (event , message .author , message .channel , message , before , cache )
126+ content = FilterContent (message .content , message .embeds , message .attachments )
127+ return cls (source , content )
81128
82129 def replace (self , ** changes ) -> FilterContext :
83130 """Return a new context object assigning new values to the specified fields."""
84- return replace (self , ** changes )
131+ sub_objects = {
132+ '_source' : self ._source ,
133+ '_content' : self ._content ,
134+ '_notifications' : self ._notifications ,
135+ '_actions' : self ._actions ,
136+ '_results' : self ._results ,
137+ }
138+ sub_changes = {}
139+ for key , value in changes .items ():
140+ for attr_name , obj in sub_objects .items ():
141+ if hasattr (obj , key ):
142+ sub_changes .setdefault (attr_name , {})[key ] = value
143+ break
144+ return FilterContext (
145+ source = dataclass_replace (self ._source , ** sub_changes .get ('_source' , {})),
146+ content = dataclass_replace (self ._content , ** sub_changes .get ('_content' , {})),
147+ notifications = dataclass_replace (self ._notifications , ** sub_changes .get ('_notifications' , {})),
148+ actions = dataclass_replace (self ._actions , ** sub_changes .get ('_actions' , {})),
149+ results = dataclass_replace (self ._results , ** sub_changes .get ('_results' , {})),
150+ )
0 commit comments