]> git.mxchange.org Git - fba.git/blob - fba/helpers/processing.py
Continued:
[fba.git] / fba / helpers / processing.py
1 # Fedi API Block - An aggregator for fetching blocking data from fediverse nodes
2 # Copyright (C) 2023 Free Software Foundation
3 #
4 # This program is free software: you can redistribute it and/or modify
5 # it under the terms of the GNU Affero General Public License as published
6 # by the Free Software Foundation, either version 3 of the License, or
7 # (at your option) any later version.
8 #
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12 # GNU Affero General Public License for more details.
13 #
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program.  If not, see <https://www.gnu.org/licenses/>.
16
17 import logging
18
19 import validators
20
21 from fba import database
22 from fba import utils
23
24 from fba.helpers import blacklist
25 from fba.helpers import blocklists
26 from fba.helpers import blocks as blocks_helper
27 from fba.helpers import config
28 from fba.helpers import domain as domain_helper
29 from fba.helpers import tidyup
30
31 from fba.http import federation
32 from fba.http import network
33
34 from fba.models import blocks
35 from fba.models import instances
36
37 logging.basicConfig(level=logging.INFO)
38 logger = logging.getLogger(__name__)
39 #logger.setLevel(logging.DEBUG)
40
41 # "Cache" configuration get() invocations
42 _bot_enabled = config.get("bot_enabled")
43
44 def process_instance(blocked: str, blocker: str, command: str, force: bool = False) -> bool:
45     logger.debug("blocked='%s',blocker='%s',command='%s',force='%s' - CALLED!", blocked, blocker, command, force)
46     domain_helper.raise_on(blocked)
47     domain_helper.raise_on(blocker)
48
49     if not isinstance(command, str):
50         raise TypeError(f"Parameter command[]='{type(command)}' has not expected type 'str'")
51     elif command == "":
52         raise ValueError("Parameter 'command' is an empty string")
53     elif blacklist.is_blacklisted(blocked):
54         raise RuntimeError(f"blocked='{blocked}' is blacklisted but function was invoked")
55     elif blacklist.is_blacklisted(blocker):
56         raise RuntimeError(f"blocker='{blocker}' is blacklisted but function was invoked")
57
58     logger.debug("blocked='%s' - BEFORE!", blocked)
59     blocked = utils.deobfuscate(blocked, blocker)
60     logger.debug("blocked='%s' - AFTER!", blocked)
61
62     logger.debug("Checking if blocker='%s' has pending data ...", blocker)
63     if instances.is_registered(blocker) and instances.has_pending(blocker):
64         logger.debug("Flushing updates for blocker='%s' ...", blocker)
65         instances.update(blocker)
66
67     logger.debug("Checking blocked='%s' if wanted and recent ...", blocked)
68     if not domain_helper.is_wanted(blocked):
69         logger.debug("blocked='%s' is not wanted - EXIT!", blocked)
70         return False
71     elif not force and instances.is_recent(blocked):
72         logger.debug("blocked='%s' has been recently checked - EXIT!", blocked)
73         return False
74
75     processed = False
76     try:
77         logger.info("Fetching instances for blocked='%s',blocker='%s',command='%s' ...", blocked, blocker, command)
78         federation.fetch_instances(blocked, blocker, None, command)
79
80         logger.debug("Setting processed=True for blocked='%s',blocker='%s' ...", blocked, blocker)
81         processed = True
82     except network.exceptions as exception:
83         logger.warning("Exception '%s' during fetching instances (%s) from blocked='%s'", type(exception), command, blocked)
84         instances.set_last_error(blocked, exception)
85
86     logger.debug("Checking if blocked='%s' has pending updates ...", blocked)
87     if instances.has_pending(blocked):
88         logger.debug("Flushing updates for blocked='%s' ...", blocked)
89         instances.update(blocked)
90
91     logger.debug("processed='%s' - EXIT!", processed)
92     return processed
93
94 def process_block(blocker: str, blocked: str, reason: str, block_level: str) -> bool:
95     logger.debug("blocker='%s',blocked='%s',reason='%s',block_level='%s' - CALLED!", blocker, blocked, reason, block_level)
96     domain_helper.raise_on(blocker)
97     domain_helper.raise_on(blocked)
98
99     if not isinstance(reason, str) and reason is not None:
100         raise TypeError(f"Parameter reason[]='{type(reason)}' has not expected type 'str'")
101     elif not isinstance(block_level, str):
102         raise TypeError(f"Parameter block_level[]='{type(block_level)}' has not expected type 'str'")
103     elif block_level == "":
104         raise ValueError("Parameter block_level is empty")
105     elif block_level in ["reject", "suspend", "accept", "silence", "nsfw", "quarantined_instances"]:
106         raise ValueError(f"Parameter block_level='{block_level}' is not supported")
107     elif blacklist.is_blacklisted(blocker):
108         raise RuntimeError(f"blocker='{blocker}' is blacklisted but function was invoked")
109     elif blacklist.is_blacklisted(blocked):
110         raise RuntimeError(f"blocked='{blocked}' is blacklisted but function was invoked")
111
112     added = False
113     if not blocks.is_instance_blocked(blocker, blocked, block_level):
114         logger.debug("Invoking blocks.add(%s, %s, %s, %s) ...", blocker, blocked, reason, block_level)
115         blocks.add(blocker, blocked, reason, block_level)
116         added = True
117     else:
118         if reason not in [None, ""] and blocks.get_reason(blocker, blocked, block_level) is None:
119             logger.debug("Updating reason='%s' for blocker='%s',blocked='%s',block_level='%s' ...", reason, blocker, blocked, block_level)
120             blocks.update_reason(reason, blocker, blocked, block_level)
121
122         logger.debug("Updating last_seen for blocker='%s',blocked='%s',block_level='%s' ...", blocker, blocked, block_level)
123         blocks.update_last_seen(blocker, blocked, block_level)
124
125     logger.debug("added='%s' - EXIT!", added)
126     return added
127
128 def csv_block(blocker: str, url: str, command: str) -> None:
129     logger.debug("blocker='%s',url='%s',command='%s' - CALLED!", blocker, url, command)
130     domain_helper.raise_on(blocker)
131
132     if not isinstance(url, str):
133         raise TypeError(f"url[]='{url}' has not expected type 'str'")
134     elif url in [None, ""]:
135         raise ValueError("Parameter 'url' is an empty string")
136     elif not validators.url(url):
137         raise ValueError(f"Parameter url='{url}' is not a valid URL")
138     elif not isinstance(command, str):
139         raise TypeError(f"command[]='{command}' has not expected type 'str'")
140     elif command == "":
141         raise ValueError("Parameter 'command' is an empty string")
142     elif blacklist.is_blacklisted(blocker):
143         raise RuntimeError(f"blocker='{blocker}' is blacklisted but function was invoked")
144
145     # Init local variables
146     domains = blockdict = []
147
148     logger.debug("Setting last_blocked for blocker='%s' ...", blocker)
149     instances.set_last_blocked(blocker)
150
151     # Fetch this URL
152     logger.info("Fetching url='%s' for blocker='%s' ...", url, blocker)
153     rows = network.fetch_csv_rows(url)
154
155     logger.info("Checking %d CSV lines ...", len(rows))
156     for row in rows:
157         logger.debug("row[%s]='%s'", type(row), row)
158
159         # Init variables
160         domain = severity = reason = None
161         reject_media = reject_reports = False
162
163         if "#domain" in row and row["#domain"] not in [None, ""]:
164             domain = tidyup.domain(row["#domain"])
165         elif "domain" in row and row["domain"] not in [None, ""]:
166             domain = tidyup.domain(row["domain"])
167         elif "Domain" in row and row["Domain"] not in [None, ""]:
168             domain = tidyup.domain(row["Domain"])
169         else:
170             logger.warning("row='%s' does not contain domain column - SKIPPED!", row)
171             continue
172
173         if "#severity" in row:
174             severity = blocks_helper.alias_block_level(row["#severity"])
175         elif "severity" in row:
176             severity = blocks_helper.alias_block_level(row["severity"])
177         else:
178             logger.debug("row='%s' does not contain severity column, setting 'reject'", row)
179             severity = "rejected"
180
181         if "reason" in row and row["reason"] not in [None, ""]:
182             reason = tidyup.reason(row["reason"])
183         elif "comment" in row and row["comment"] not in [None, ""]:
184             reason = tidyup.reason(row["comment"])
185         elif "Comment" in row and row["Comment"] not in [None, ""]:
186             reason = tidyup.reason(row["Comment"])
187         else:
188             logger.debug("row='%s' has no reason/comment key provided", row)
189
190         if "#reject_media" in row and row["#reject_media"].lower() == "true":
191             reject_media = True
192         elif "reject_media" in row and row["reject_media"].lower() == "true":
193             reject_media = True
194         else:
195             logger.debug("row='%s' for domain='%s' does not contain key '[#]reject_media'", row, domain)
196
197         if "#reject_reports" in row and row["#reject_reports"].lower() == "true":
198             reject_reports = True
199         elif "reject_reports" in row and row["reject_reports"].lower() == "true":
200             reject_reports = True
201         else:
202             logger.debug("row='%s' for domain='%s' does not contain key '[#]reject_reports'", row, domain)
203
204         logger.debug("domain='%s',severity='%s',reject_media='%s',reject_reports='%s'", domain, severity, reject_media, reject_reports)
205         if domain in [None, ""]:
206             logger.debug("domain='%s' is empty - SKIPPED!", domain)
207             continue
208         elif not domain_helper.is_tld_wanted(domain):
209             logger.debug("domain='%s' has an unwanted TLD - SKIPPED!", domain)
210             continue
211         elif domain.find("*") >= 0 or domain.find("?") >= 0:
212             logger.debug("domain='%s' is obfuscated - Invoking utils.deobfuscate(%s, %s) ...", domain, domain, blocker)
213             domain = utils.deobfuscate(domain, blocker)
214             logger.debug("domain='%s' - AFTER!", domain)
215
216         logger.debug("Marking domain='%s' as handled ...", domain)
217         domains.append(domain)
218
219         if not validators.domain(domain, rfc_2782=True):
220             logger.warning("domain='%s' is not a valid domain - SKIPPED!", domain)
221             continue
222         elif blacklist.is_blacklisted(domain):
223             logger.debug("domain='%s' is blacklisted - SKIPPED!", domain)
224             continue
225         elif blocks.is_instance_blocked(blocker, domain, severity):
226             logger.debug("blocker='%s' has already blocked domain='%s' with severity='%s' - SKIPPED!", blocker, domain, severity)
227             continue
228
229         logger.debug("Processing domain='%s',blocker='%s',command='%s' ...", domain, blocker, command)
230         processed = process_instance(domain, blocker, command)
231         logger.debug("processed='%s'", processed)
232
233         if process_block(blocker, domain, reason, severity) and _bot_enabled:
234             logger.debug("Appending blocked='%s',reason='%s' for blocker='%s' ...", domain, reason, blocker)
235             blockdict.append({
236                 "blocked": domain,
237                 "reason" : reason,
238             })
239
240         logger.debug("reject_media='%s',reject_reports='%s'", reject_media, reject_reports)
241         if reject_media:
242             logger.debug("blocker='%s',domain='%s',reject_media=True", blocker, domain)
243             process_block(blocker, domain, None, "reject_media")
244
245         if reject_reports:
246             logger.debug("blocker='%s',domain='%s',reject_reports=True", blocker, domain)
247             process_block(blocker, domain, None, "reject_reports")
248
249         logger.debug("Invoking commit() ...")
250         database.connection.commit()
251
252     logger.debug("blocker='%s'", blocker)
253     if blocklists.has(blocker):
254         logger.debug("Invoking instances.set_total_blocks(%s, domains()=%d) ...", blocker, len(domains))
255         instances.set_total_blocks(blocker, domains)
256
257     logger.debug("Checking if blocker='%s' has pending updates ...", blocker)
258     if instances.has_pending(blocker):
259         logger.debug("Flushing updates for blocker='%s' ...", blocker)
260         instances.update(blocker)
261
262     logger.debug("config.get(bot_enabled)='%s',blockdict()=%d", _bot_enabled, len(blockdict))
263     if _bot_enabled and len(blockdict) > 0:
264         logger.info("Sending bot POST for blocker='%s',blockdict()=%d ...", blocker, len(blockdict))
265         network.send_bot_post(blocker, blockdict)
266
267     logger.debug("EXIT!")
268
269 def csv_instance(instance: str, url: str, command: str) -> None:
270     logger.debug("instance='%s',url='%s',command='%s' - CALLED!", instance, url, command)
271     domain_helper.raise_on(instance)
272
273     if not isinstance(url, str):
274         raise TypeError(f"url[]='{url}' has not expected type 'str'")
275     elif url in [None, ""]:
276         raise ValueError("Parameter 'url' is an empty string")
277     elif not validators.url(url):
278         raise ValueError(f"Parameter url='{url}' is not a valid URL")
279     elif not isinstance(command, str):
280         raise TypeError(f"command[]='{command}' has not expected type 'str'")
281     elif command == "":
282         raise ValueError("Parameter 'command' is an empty string")
283     elif blacklist.is_blacklisted(instance):
284         raise RuntimeError(f"instance='{instance}' is blacklisted but function was invoked")
285
286     # Init local variables
287     domains = []
288
289     logger.debug("Setting last_instance_fetch for instance='%s' ...", instance)
290     instances.set_last_instance_fetch(instance)
291
292     # Fetch this URL
293     logger.info("Fetching url='%s' for instance='%s' ...", url, instance)
294     rows = network.fetch_csv_rows(url)
295
296     logger.info("Checking %d CSV lines ...", len(rows))
297     for row in rows:
298         logger.debug("row[%s]='%s'", type(row), row)
299         domain = None
300
301         if "#domain" in row and row["#domain"] not in [None, ""]:
302             domain = tidyup.domain(row["#domain"])
303         elif "domain" in row and row["domain"] not in [None, ""]:
304             domain = tidyup.domain(row["domain"])
305         elif "Domain" in row and row["Domain"] not in [None, ""]:
306             domain = tidyup.domain(row["Domain"])
307         else:
308             logger.warning("row='%s' does not contain domain column - SKIPPED!", row)
309             continue
310
311         logger.debug("domain='%s'", domain)
312         if domain in [None, ""]:
313             logger.debug("domain='%s' is empty - SKIPPED!", domain)
314             continue
315         elif not domain_helper.is_tld_wanted(domain):
316             logger.debug("domain='%s' has an unwanted TLD - SKIPPED!", domain)
317             continue
318         elif domain.find("*") >= 0 or domain.find("?") >= 0:
319             logger.debug("domain='%s' is obfuscated - Invoking utils.deobfuscate(%s, %s) ...", domain, domain, instance)
320             domain = utils.deobfuscate(domain, instance)
321             logger.debug("domain='%s' - AFTER!", domain)
322
323         logger.debug("Marking domain='%s' as handled ...", domain)
324         domains.append(domain)
325
326         if not validators.domain(domain, rfc_2782=True):
327             logger.warning("domain='%s' is not a valid domain - SKIPPED!", domain)
328             continue
329         elif blacklist.is_blacklisted(domain):
330             logger.debug("domain='%s' is blacklisted - SKIPPED!", domain)
331             continue
332         elif instances.is_registered(domain):
333             logger.debug("domain='%s' is already registered - SKIPPED!", domain)
334             continue
335
336         logger.debug("Processing domain='%s',instance='%s',command='%s' ...", domain, instance, command)
337         processed = process_instance(domain, instance, command)
338         logger.debug("processed='%s'", processed)
339
340         logger.debug("Invoking commit() ...")
341         database.connection.commit()
342
343     logger.debug("Invoking instances.set_total_peers(%s, domains()=%d) ...", instance, len(domains))
344     instances.set_total_peers(instance, domains)
345
346     logger.debug("Checking if instance='%s' has pending updates ...", instance)
347     if instances.has_pending(instance):
348         logger.debug("Flushing updates for instance='%s' ...", instance)
349         instances.update(instance)
350
351     logger.debug("EXIT!")