1 # Fedi API Block - An aggregator for fetching blocking data from fediverse nodes
2 # Copyright (C) 2023 Free Software Foundation
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.
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.
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/>.
21 from fba import database
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
31 from fba.http import federation
32 from fba.http import network
34 from fba.models import blocks
35 from fba.models import instances
37 logging.basicConfig(level=logging.INFO)
38 logger = logging.getLogger(__name__)
39 #logger.setLevel(logging.DEBUG)
41 # "Cache" configuration get() invocations
42 _bot_enabled = config.get("bot_enabled")
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)
49 if not isinstance(command, str):
50 raise TypeError(f"Parameter command[]='{type(command)}' has not expected type 'str'")
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")
58 logger.debug("blocked='%s' - BEFORE!", blocked)
59 blocked = utils.deobfuscate(blocked, blocker)
60 logger.debug("blocked='%s' - AFTER!", blocked)
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)
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)
71 elif not force and instances.is_recent(blocked):
72 logger.debug("blocked='%s' has been recently checked - EXIT!", blocked)
77 logger.info("Fetching instances for blocked='%s',blocker='%s',command='%s' ...", blocked, blocker, command)
78 federation.fetch_instances(blocked, blocker, None, command)
80 logger.debug("Setting processed=True for blocked='%s',blocker='%s' ...", blocked, blocker)
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)
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)
91 logger.debug("processed='%s' - EXIT!", processed)
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)
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")
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)
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)
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)
125 logger.debug("added='%s' - EXIT!", added)
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)
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'")
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")
145 # Init local variables
146 domains = blockdict = []
148 logger.debug("Setting last_blocked for blocker='%s' ...", blocker)
149 instances.set_last_blocked(blocker)
152 logger.info("Fetching url='%s' for blocker='%s' ...", url, blocker)
153 rows = network.fetch_csv_rows(url)
155 logger.info("Checking %d CSV lines ...", len(rows))
157 logger.debug("row[%s]='%s'", type(row), row)
160 domain = severity = reason = None
161 reject_media = reject_reports = False
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"])
170 logger.warning("row='%s' does not contain domain column - SKIPPED!", row)
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"])
178 logger.debug("row='%s' does not contain severity column, setting 'reject'", row)
179 severity = "rejected"
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"])
188 logger.debug("row='%s' has no reason/comment key provided", row)
190 if "#reject_media" in row and row["#reject_media"].lower() == "true":
192 elif "reject_media" in row and row["reject_media"].lower() == "true":
195 logger.debug("row='%s' for domain='%s' does not contain key '[#]reject_media'", row, domain)
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
202 logger.debug("row='%s' for domain='%s' does not contain key '[#]reject_reports'", row, domain)
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)
208 elif not domain_helper.is_tld_wanted(domain):
209 logger.debug("domain='%s' has an unwanted TLD - SKIPPED!", domain)
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)
216 logger.debug("Marking domain='%s' as handled ...", domain)
217 domains.append(domain)
219 if not validators.domain(domain, rfc_2782=True):
220 logger.warning("domain='%s' is not a valid domain - SKIPPED!", domain)
222 elif blacklist.is_blacklisted(domain):
223 logger.debug("domain='%s' is blacklisted - SKIPPED!", domain)
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)
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)
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)
240 logger.debug("reject_media='%s',reject_reports='%s'", reject_media, reject_reports)
242 logger.debug("blocker='%s',domain='%s',reject_media=True", blocker, domain)
243 process_block(blocker, domain, None, "reject_media")
246 logger.debug("blocker='%s',domain='%s',reject_reports=True", blocker, domain)
247 process_block(blocker, domain, None, "reject_reports")
249 logger.debug("Invoking commit() ...")
250 database.connection.commit()
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)
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)
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)
267 logger.debug("EXIT!")
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)
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'")
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")
286 # Init local variables
289 logger.debug("Setting last_instance_fetch for instance='%s' ...", instance)
290 instances.set_last_instance_fetch(instance)
293 logger.info("Fetching url='%s' for instance='%s' ...", url, instance)
294 rows = network.fetch_csv_rows(url)
296 logger.info("Checking %d CSV lines ...", len(rows))
298 logger.debug("row[%s]='%s'", type(row), row)
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"])
308 logger.warning("row='%s' does not contain domain column - SKIPPED!", row)
311 logger.debug("domain='%s'", domain)
312 if domain in [None, ""]:
313 logger.debug("domain='%s' is empty - SKIPPED!", domain)
315 elif not domain_helper.is_tld_wanted(domain):
316 logger.debug("domain='%s' has an unwanted TLD - SKIPPED!", domain)
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)
323 logger.debug("Marking domain='%s' as handled ...", domain)
324 domains.append(domain)
326 if not validators.domain(domain, rfc_2782=True):
327 logger.warning("domain='%s' is not a valid domain - SKIPPED!", domain)
329 elif blacklist.is_blacklisted(domain):
330 logger.debug("domain='%s' is blacklisted - SKIPPED!", domain)
332 elif instances.is_registered(domain):
333 logger.debug("domain='%s' is already registered - SKIPPED!", domain)
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)
340 logger.debug("Invoking commit() ...")
341 database.connection.commit()
343 logger.debug("Invoking instances.set_total_peers(%s, domains()=%d) ...", instance, len(domains))
344 instances.set_total_peers(instance, domains)
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)
351 logger.debug("EXIT!")