@@ -25,60 +25,130 @@ class Event(Enum):
2525
2626
2727@dataclass
28- class FilterContext :
29- """A dataclass containing the information that should be filtered, and output information of the filtering ."""
28+ class FilterInput : # pylint: disable=too-many-instance-attributes
29+ """Input data for filtering: event details and message content ."""
3030
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.
31+ event : Event
32+ author : User | Member | None
33+ channel : TextChannel | VoiceChannel | StageChannel | Thread | DMChannel | None
34+ content : str | Iterable
35+ message : Message | None
36+ embeds : list [Embed ] = field (default_factory = list )
37+ attachments : list [discord .Attachment | FileAttachment ] = field (default_factory = list )
3938 before_message : Message | None = None
4039 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)
40+
41+
42+ @dataclass
43+ class FilterOutput : # pylint: disable=too-many-instance-attributes
44+ """Output data produced by filtering: alerts, actions, and results."""
45+
46+ dm_content : str = ""
47+ dm_embed : str = ""
48+ send_alert : bool = False
49+ alert_content : str = ""
50+ alert_embeds : list [Embed ] = field (default_factory = list )
51+ action_descriptions : list [str ] = field (default_factory = list )
52+ matches : list [str ] = field (default_factory = list )
53+ notification_domain : str = ""
54+ filter_info : dict [Filter , str ] = field (default_factory = dict )
55+ messages_deletion : bool = False
56+ blocked_exts : set [str ] = field (default_factory = set )
5357 potential_phish : dict [FilterList , set [str ]] = field (default_factory = dict )
54- # Additional actions to perform
58+
59+
60+ _FILTER_CONTEXT_DIRECT_FIELDS = frozenset ({
61+ 'input' , 'output' , 'additional_actions' , 'related_messages' ,
62+ 'related_channels' , 'uploaded_attachments' , 'upload_deletion_logs' ,
63+ })
64+
65+
66+ @dataclass
67+ class FilterContext :
68+ """A dataclass containing the information that should be filtered, and output information of the filtering."""
69+
70+ input : FilterInput
71+ output : FilterOutput
5572 additional_actions : list [Callable [[FilterContext ], Coroutine ]] = field (default_factory = list )
56- related_messages : set [Message ] = field (default_factory = set ) # Deletion will include these.
73+ related_messages : set [Message ] = field (default_factory = set )
5774 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.
75+ uploaded_attachments : dict [int , list [str ]] = field (default_factory = dict )
76+ upload_deletion_logs : bool = True
77+
78+ @property
79+ def in_guild (self ) -> bool :
80+ """Whether the context is from a guild channel (not a DM)."""
81+ return self .input .channel is None or self .input .channel .guild is not None
82+
83+ def __getattr__ (self , name ):
84+ try :
85+ input_obj = object .__getattribute__ (self , 'input' )
86+ if hasattr (input_obj , name ):
87+ return getattr (input_obj , name )
88+ except AttributeError :
89+ pass
90+ try :
91+ output_obj = object .__getattribute__ (self , 'output' )
92+ if hasattr (output_obj , name ):
93+ return getattr (output_obj , name )
94+ except AttributeError :
95+ pass
96+ raise AttributeError (f"'FilterContext' has no attribute '{ name } '" )
6097
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
98+ def __setattr__ (self , name , value ):
99+ if name in _FILTER_CONTEXT_DIRECT_FIELDS :
100+ object .__setattr__ (self , name , value )
101+ return
102+ try :
103+ input_obj = object .__getattribute__ (self , 'input' )
104+ if hasattr (input_obj , name ):
105+ setattr (input_obj , name , value )
106+ return
107+ except AttributeError :
108+ pass
109+ try :
110+ output_obj = object .__getattribute__ (self , 'output' )
111+ if hasattr (output_obj , name ):
112+ setattr (output_obj , name , value )
113+ return
114+ except AttributeError :
115+ pass
116+ object .__setattr__ (self , name , value )
64117
65118 @classmethod
66119 def from_message (
67120 cls , event : Event , message : Message , before : Message | None = None , cache : MessageCache | None = None
68121 ) -> FilterContext :
69122 """Create a filtering context from the attributes of a message."""
70123 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
124+ FilterInput (
125+ event ,
126+ message .author ,
127+ message .channel ,
128+ message .content ,
129+ message ,
130+ message .embeds ,
131+ message .attachments ,
132+ before ,
133+ cache
134+ ),
135+ FilterOutput ()
80136 )
81137
82138 def replace (self , ** changes ) -> FilterContext :
83139 """Return a new context object assigning new values to the specified fields."""
84- return replace (self , ** changes )
140+ input_fields = FilterInput .__dataclass_fields__
141+ output_fields = FilterOutput .__dataclass_fields__
142+ input_changes = {}
143+ output_changes = {}
144+ context_changes = {}
145+ for k , v in changes .items ():
146+ if k in input_fields :
147+ input_changes [k ] = v
148+ elif k in output_fields :
149+ output_changes [k ] = v
150+ else :
151+ context_changes [k ] = v
152+ new_input = replace (self .input , ** input_changes ) if input_changes else self .input
153+ new_output = replace (self .output , ** output_changes ) if output_changes else self .output
154+ return FilterContext (new_input , new_output , ** context_changes )
0 commit comments