2 # -*- coding: utf-8 -*-
4 # Fedi API Block - An aggregator for fetching blocking data from fediverse nodes
5 # Copyright (C) 2023 Free Software Foundation
7 # This program is free software: you can redistribute it and/or modify
8 # it under the terms of the GNU Affero General Public License as published
9 # by the Free Software Foundation, either version 3 of the License, or
10 # (at your option) any later version.
12 # This program is distributed in the hope that it will be useful,
13 # but WITHOUT ANY WARRANTY; without even the implied warranty of
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 # GNU Affero General Public License for more details.
17 # You should have received a copy of the GNU Affero General Public License
18 # along with this program. If not, see <https://www.gnu.org/licenses/>.
32 "SELECT domain, software, origin, nodeinfo_url FROM instances WHERE software IN ('pleroma', 'mastodon', 'friendica', 'misskey', 'gotosocial', 'bookwyrm', 'takahe') AND (last_blocked IS NULL OR last_blocked < ?) ORDER BY rowid DESC", [time.time() - config.get("recheck_block")]
35 rows = fba.cursor.fetchall()
36 print(f"INFO: Checking {len(rows)} entries ...")
37 for blocker, software, origin, nodeinfo_url in rows:
38 # DEBUG: print("DEBUG: BEFORE blocker,software,origin,nodeinfo_url:", blocker, software, origin, nodeinfo_url)
40 blocker = fba.tidyup_domain(blocker)
41 # DEBUG: print("DEBUG: AFTER blocker,software:", blocker, software)
44 print("WARNING: blocker is now empty!")
46 elif fba.is_blacklisted(blocker):
47 print(f"WARNING: blocker='{blocker}' is blacklisted now!")
50 # DEBUG: print(f"DEBUG: blocker='{blocker}'")
51 fba.update_last_blocked(blocker)
53 if software == "pleroma":
54 print("INFO: blocker:", blocker)
57 json = fba.fetch_nodeinfo(blocker, nodeinfo_url)
59 print("WARNING: Could not fetch nodeinfo from blocker:", blocker)
61 elif not "metadata" in json:
62 print(f"WARNING: json()={len(json)} does not have key 'metadata', blocker='{blocker}'")
64 elif not "federation" in json["metadata"]:
65 print(f"WARNING: json()={len(json['metadata'])} does not have key 'federation', blocker='{blocker}'")
68 # DEBUG: print("DEBUG: Updating nodeinfo:", blocker)
69 fba.update_last_nodeinfo(blocker)
71 federation = json["metadata"]["federation"]
73 if "enabled" in federation:
74 # DEBUG: print("DEBUG: Instance has no block list to analyze:", blocker)
77 if "mrf_simple" in federation:
78 for block_level, blocks in (
79 {**federation["mrf_simple"],
80 **{"quarantined_instances": federation["quarantined_instances"]}}
82 # DEBUG: print("DEBUG: block_level, blocks():", block_level, len(blocks))
83 block_level = fba.tidyup_domain(block_level)
84 # DEBUG: print("DEBUG: BEFORE block_level:", block_level)
87 print("WARNING: block_level is now empty!")
90 # DEBUG: print(f"DEBUG: Checking {len(blocks)} entries from blocker='{blocker}',software='{software}',block_level='{block_level}' ...")
91 for blocked in blocks:
92 # DEBUG: print("DEBUG: BEFORE blocked:", blocked)
93 blocked = fba.tidyup_domain(blocked)
94 # DEBUG: print("DEBUG: AFTER blocked:", blocked)
97 print("WARNING: blocked is empty after fba.tidyup_domain():", blocker, block_level)
99 elif fba.is_blacklisted(blocked):
100 # DEBUG: print(f"DEBUG: blocked='{blocked}' is blacklisted - skipping!")
102 elif blocked.count("*") > 1:
103 # -ACK!-oma also started obscuring domains without hash
105 "SELECT domain, nodeinfo_url FROM instances WHERE domain LIKE ? ORDER BY rowid LIMIT 1", [blocked.replace("*", "_")]
107 searchres = fba.cursor.fetchone()
108 # DEBUG: print("DEBUG: searchres[]:", type(searchres))
110 if searchres == None:
111 print(f"WARNING: Cannot deobsfucate blocked='{blocked}' - SKIPPED!")
114 blocked = searchres[0]
115 nodeinfo_url = searchres[1]
116 # DEBUG: print("DEBUG: Looked up domain:", blocked)
117 elif not validators.domain(blocked):
118 print(f"WARNING: blocked='{blocked}',software='{software}' is not a valid domain name - skipped!")
121 # DEBUG: print("DEBUG: Looking up instance by domain:", blocked)
122 if not validators.domain(blocked):
123 print(f"WARNING: blocked='{blocked}',software='{software}' is not a valid domain name - skipped!")
125 elif not fba.is_instance_registered(blocked):
126 # DEBUG: print(f"DEBUG: Domain blocked='{blocked}' wasn't found, adding ..., blocker='{blocker}',origin='{origin}',nodeinfo_url='{nodeinfo_url}'")
127 fba.add_instance(blocked, blocker, sys.argv[0], nodeinfo_url)
129 if not fba.is_instance_blocked(blocker, blocked, block_level):
130 # DEBUG: print("DEBUG: Blocking:", blocker, blocked, block_level)
131 fba.block_instance(blocker, blocked, "unknown", block_level)
133 if block_level == "reject":
134 # DEBUG: print("DEBUG: Adding to blockdict:", blocked)
141 # DEBUG: print(f"DEBUG: Updating block last seen for blocker='{blocker}',blocked='{blocked}' ...")
142 fba.update_last_seen(blocker, blocked, block_level)
144 # DEBUG: print("DEBUG: Committing changes ...")
145 fba.connection.commit()
148 if "mrf_simple_info" in federation:
149 # DEBUG: print("DEBUG: Found mrf_simple_info:", blocker)
150 for block_level, info in (
151 {**federation["mrf_simple_info"],
152 **(federation["quarantined_instances_info"]
153 if "quarantined_instances_info" in federation
156 # DEBUG: print("DEBUG: block_level, info.items():", block_level, len(info.items()))
157 block_level = fba.tidyup_domain(block_level)
158 # DEBUG: print("DEBUG: BEFORE block_level:", block_level)
160 if block_level == "":
161 print("WARNING: block_level is now empty!")
164 # DEBUG: print(f"DEBUG: Checking {len(info.items())} entries from blocker='{blocker}',software='{software}',block_level='{block_level}' ...")
165 for blocked, reason in info.items():
166 # DEBUG: print("DEBUG: BEFORE blocked:", blocked)
167 blocked = fba.tidyup_domain(blocked)
168 # DEBUG: print("DEBUG: AFTER blocked:", blocked)
171 print("WARNING: blocked is empty after fba.tidyup_domain():", blocker, block_level)
173 elif fba.is_blacklisted(blocked):
174 # DEBUG: print(f"DEBUG: blocked='{blocked}' is blacklisted - skipping!")
176 elif blocked.count("*") > 1:
177 # same domain guess as above, but for reasons field
179 "SELECT domain, origin, nodeinfo_url FROM instances WHERE domain LIKE ? ORDER BY rowid LIMIT 1", [blocked.replace("*", "_")]
181 searchres = fba.cursor.fetchone()
183 if searchres == None:
184 print(f"WARNING: Cannot deobsfucate blocked='{blocked}' - SKIPPED!")
187 blocked = searchres[0]
188 origin = searchres[1]
189 nodeinfo_url = searchres[2]
190 elif not validators.domain(blocked):
191 print(f"WARNING: blocked='{blocked}',software='{software}' is not a valid domain name - skipped!")
194 # DEBUG: print("DEBUG: Looking up instance by domain:", blocked)
195 if not validators.domain(blocked):
196 print(f"WARNING: blocked='{blocked}',software='{software}' is not a valid domain name - skipped!")
198 elif not fba.is_instance_registered(blocked):
199 # DEBUG: print(f"DEBUG: Domain blocked='{blocked}' wasn't found, adding ..., blocker='{blocker}',origin='{origin}',nodeinfo_url='{nodeinfo_url}'")
200 fba.add_instance(blocked, blocker, sys.argv[0], nodeinfo_url)
202 # DEBUG: print("DEBUG: Updating block reason:", blocker, blocked, reason["reason"])
203 fba.update_block_reason(reason["reason"], blocker, blocked, block_level)
205 for entry in blockdict:
206 if entry["blocked"] == blocked:
207 # DEBUG: print("DEBUG: Updating entry reason:", blocked)
208 entry["reason"] = reason["reason"]
210 fba.connection.commit()
211 except Exception as e:
212 print(f"ERROR: blocker='{blocker}',software='{software}',exception[{type(e)}]:'{str(e)}'")
213 elif software == "mastodon":
214 print("INFO: blocker:", blocker)
216 # json endpoint for newer mastodongs
220 "media_removal" : [],
221 "followers_only": [],
225 # handling CSRF, I've saw at least one server requiring it to access the endpoint
226 # DEBUG: print("DEBUG: Fetching meta:", blocker)
227 meta = bs4.BeautifulSoup(
228 fba.get_response(blocker, "/", fba.headers, (config.get("connection_timeout"), config.get("read_timeout"))).text,
232 csrf = meta.find("meta", attrs={"name": "csrf-token"})["content"]
233 # DEBUG: print("DEBUG: Adding CSRF token:", blocker, csrf)
234 reqheaders = {**fba.api_headers, **{"X-CSRF-Token": csrf}}
235 except BaseException as e:
236 # DEBUG: print("DEBUG: No CSRF token found, using normal headers:", blocker, e)
237 reqheaders = fba.api_headers
239 # DEBUG: print("DEBUG: Querying API domain_blocks:", blocker)
240 blocks = fba.get_response(blocker, "/api/v1/instance/domain_blocks", reqheaders, (config.get("connection_timeout"), config.get("read_timeout"))).json()
242 print(f"INFO: Checking {len(blocks)} entries from blocker='{blocker}',software='{software}' ...")
245 'domain': block['domain'],
246 'hash' : block['digest'],
247 'reason': block['comment']
250 # DEBUG: print("DEBUG: severity,domain,hash,comment:", block['severity'], block['domain'], block['digest'], block['comment'])
251 if block['severity'] == 'suspend':
252 # DEBUG: print(f"DEBUG: Adding entry='{entry}' with severity='{block['severity']}' ...")
253 json['reject'].append(entry)
254 elif block['severity'] == 'silence':
255 # DEBUG: print(f"DEBUG: Adding entry='{entry}' with severity='{block['severity']}' ...")
256 json['followers_only'].append(entry)
257 elif block['severity'] == 'reject_media':
258 # DEBUG: print(f"DEBUG: Adding entry='{entry}' with severity='{block['severity']}' ...")
259 json['media_removal'].append(entry)
260 elif block['severity'] == 'reject_reports':
261 # DEBUG: print(f"DEBUG: Adding entry='{entry}' with severity='{block['severity']}' ...")
262 json['report_removal'].append(entry)
264 print("WARNING: Unknown severity:", block['severity'], block['domain'])
265 except BaseException as e:
266 # DEBUG: print(f"DEBUG: Failed, trying mastodon-specific fetches: blocker='{blocker}',exception[{type(e)}]={str(e)}")
267 json = fba.get_mastodon_blocks(blocker)
269 print(f"INFO: Checking {len(json.items())} entries from blocker='{blocker}',software='{software}' ...")
270 for block_level, blocks in json.items():
271 # DEBUG: print("DEBUG: blocker,block_level,blocks():", blocker, block_level, len(blocks))
272 block_level = fba.tidyup_domain(block_level)
273 # DEBUG: print("DEBUG: AFTER-block_level:", block_level)
274 if block_level == "":
275 print("WARNING: block_level is empty, blocker:", blocker)
278 # DEBUG: print(f"DEBUG: Checking {len(blocks)} entries from blocker='{blocker}',software='{software}',block_level='{block_level}' ...")
280 blocked, blocked_hash, reason = block.values()
281 # DEBUG: print("DEBUG: blocked,hash,reason:", blocked, blocked_hash, reason)
282 blocked = fba.tidyup_domain(blocked)
283 # DEBUG: print("DEBUG: AFTER-blocked:", blocked)
286 print("WARNING: blocked is empty:", blocker)
288 elif fba.is_blacklisted(blocked):
289 # DEBUG: print(f"DEBUG: blocked='{blocked}' is blacklisted - skipping!")
291 elif blocked.count("*") > 0:
292 # Doing the hash search for instance names as well to tidy up DB
294 "SELECT domain, origin, nodeinfo_url FROM instances WHERE hash = ? LIMIT 1", [blocked_hash]
296 searchres = fba.cursor.fetchone()
298 if searchres == None:
299 print(f"WARNING: Cannot deobsfucate blocked='{blocked}',blocked_hash='{blocked_hash}' - SKIPPED!")
302 # DEBUG: print("DEBUG: Updating domain: ", searchres[0])
303 blocked = searchres[0]
304 origin = searchres[1]
305 nodeinfo_url = searchres[2]
307 # DEBUG: print("DEBUG: Looking up instance by domain:", blocked)
308 if not validators.domain(blocked):
309 print(f"WARNING: blocked='{blocked}',software='{software}' is not a valid domain name - skipped!")
311 elif not fba.is_instance_registered(blocked):
312 # DEBUG: print(f"DEBUG: Domain blocked='{blocked}' wasn't found, adding ..., blocker='{blocker}',origin='{origin}',nodeinfo_url='{nodeinfo_url}'")
313 fba.add_instance(blocked, blocker, sys.argv[0], nodeinfo_url)
314 elif not validators.domain(blocked):
315 print(f"WARNING: blocked='{blocked}',software='{software}' is not a valid domain name - skipped!")
318 # DEBUG: print("DEBUG: Looking up instance by domain:", blocked)
319 if not validators.domain(blocked):
320 print(f"WARNING: blocked='{blocked}',software='{software}' is not a valid domain name - skipped!")
322 elif not fba.is_instance_registered(blocked):
323 # DEBUG: print("DEBUG: Hash wasn't found, adding:", blocked, blocker)
324 fba.add_instance(blocked, blocker, sys.argv[0], nodeinfo_url)
326 blocking = blocked if blocked.count("*") <= 1 else blocked_hash
327 # DEBUG: print(f"DEBUG: blocking='{blocking}',blocked='{blocked}',blocked_hash='{blocked_hash}'")
329 if not fba.is_instance_blocked(blocker, blocked, block_level):
330 # DEBUG: print("DEBUG: Blocking:", blocker, blocked, block_level)
331 fba.block_instance(blocker, blocking, reason, block_level)
333 if block_level == "reject":
339 # DEBUG: print(f"DEBUG: Updating block last seen and reason for blocker='{blocker}',blocking='{blocking}' ...")
340 fba.update_last_seen(blocker, blocking, block_level)
341 fba.update_block_reason(reason, blocker, blocking, block_level)
343 # DEBUG: print("DEBUG: Committing changes ...")
344 fba.connection.commit()
345 except Exception as e:
346 print(f"ERROR: blocker='{blocker}',software='{software}',exception[{type(e)}]:'{str(e)}'")
347 elif software == "friendica" or software == "misskey" or software == "bookwyrm" or software == "takahe":
348 print("INFO: blocker:", blocker)
350 if software == "friendica":
351 json = fba.get_friendica_blocks(blocker)
352 elif software == "misskey":
353 json = fba.get_misskey_blocks(blocker)
354 elif software == "bookwyrm":
355 print("WARNING: bookwyrm is not fully supported for fetching blacklist!", blocker)
356 #json = fba.get_bookwyrm_blocks(blocker)
358 elif software == "takahe":
359 print("WARNING: takahe is not fully supported for fetching blacklist!", blocker)
360 #json = fba.get_takahe_blocks(blocker)
363 print(f"INFO: Checking {len(json.items())} entries from blocker='{blocker}',software='{software}' ...")
364 for block_level, blocks in json.items():
365 # DEBUG: print("DEBUG: blocker,block_level,blocks():", blocker, block_level, len(blocks))
366 block_level = fba.tidyup_domain(block_level)
367 # DEBUG: print("DEBUG: AFTER-block_level:", block_level)
368 if block_level == "":
369 print("WARNING: block_level is empty, blocker:", blocker)
372 # DEBUG: print(f"DEBUG: Checking {len(blocks)} entries from blocker='{blocker}',software='{software}',block_level='{block_level}' ...")
374 blocked, reason = block.values()
375 # DEBUG: print("DEBUG: BEFORE blocked:", blocked)
376 blocked = fba.tidyup_domain(blocked)
377 # DEBUG: print("DEBUG: AFTER blocked:", blocked)
380 print("WARNING: blocked is empty:", blocker)
382 elif fba.is_blacklisted(blocked):
383 # DEBUG: print(f"DEBUG: blocked='{blocked}' is blacklisted - skipping!")
385 elif blocked.count("*") > 0:
386 # Some friendica servers also obscure domains without hash
388 "SELECT domain, origin, nodeinfo_url FROM instances WHERE domain LIKE ? ORDER BY rowid LIMIT 1", [blocked.replace("*", "_")]
391 searchres = fba.cursor.fetchone()
393 if searchres == None:
394 print(f"WARNING: Cannot deobsfucate blocked='{blocked}' - SKIPPED!")
397 blocked = searchres[0]
398 origin = searchres[1]
399 nodeinfo_url = searchres[2]
400 elif blocked.count("?") > 0:
401 # Some obscure them with question marks, not sure if that's dependent on version or not
403 "SELECT domain, origin, nodeinfo_url FROM instances WHERE domain LIKE ? ORDER BY rowid LIMIT 1", [blocked.replace("?", "_")]
406 searchres = fba.cursor.fetchone()
408 if searchres == None:
409 print(f"WARNING: Cannot deobsfucate blocked='{blocked}' - SKIPPED!")
412 blocked = searchres[0]
413 origin = searchres[1]
414 nodeinfo_url = searchres[2]
415 elif not validators.domain(blocked):
416 print(f"WARNING: blocked='{blocked}',software='{software}' is not a valid domain name - skipped!")
419 # DEBUG: print("DEBUG: Looking up instance by domain:", blocked)
420 if not validators.domain(blocked):
421 print(f"WARNING: blocked='{blocked}',software='{software}' is not a valid domain name - skipped!")
423 elif not fba.is_instance_registered(blocked):
424 # DEBUG: print("DEBUG: Hash wasn't found, adding:", blocked, blocker)
425 fba.add_instance(blocked, blocker, sys.argv[0], nodeinfo_url)
427 if not fba.is_instance_blocked(blocker, blocked, block_level):
428 fba.block_instance(blocker, blocked, reason, block_level)
430 if block_level == "reject":
436 # DEBUG: print(f"DEBUG: Updating block last seen and reason for blocker='{blocker}',blocked='{blocked}' ...")
437 fba.update_last_seen(blocker, blocked, block_level)
438 fba.update_block_reason(reason, blocker, blocked, block_level)
440 # DEBUG: print("DEBUG: Committing changes ...")
441 fba.connection.commit()
442 except Exception as e:
443 print(f"ERROR: blocker='{blocker}',software='{software}',exception[{type(e)}]:'{str(e)}'")
444 elif software == "gotosocial":
445 print("INFO: blocker:", blocker)
448 federation = fba.get_response(blocker, "{fba.get_peers_url}?filter=suspended", fba.api_headers, (config.get("connection_timeout"), config.get("read_timeout"))).json()
450 if (federation == None):
451 print("WARNING: No valid response:", blocker);
452 elif "error" in federation:
453 print("WARNING: API returned error:", federation["error"])
455 print(f"INFO: Checking {len(federation)} entries from blocker='{blocker}',software='{software}' ...")
456 for peer in federation:
457 blocked = peer["domain"].lower()
458 # DEBUG: print("DEBUG: BEFORE blocked:", blocked)
459 blocked = fba.tidyup_domain(blocked)
460 # DEBUG: print("DEBUG: AFTER blocked:", blocked)
463 print("WARNING: blocked is empty:", blocker)
465 elif fba.is_blacklisted(blocked):
466 # DEBUG: print(f"DEBUG: blocked='{blocked}' is blacklisted - skipping!")
468 elif blocked.count("*") > 0:
469 # GTS does not have hashes for obscured domains, so we have to guess it
471 "SELECT domain, origin, nodeinfo_url FROM instances WHERE domain LIKE ? ORDER BY rowid LIMIT 1", [blocked.replace("*", "_")]
473 searchres = fba.cursor.fetchone()
475 if searchres == None:
476 print(f"WARNING: Cannot deobsfucate blocked='{blocked}' - SKIPPED!")
479 blocked = searchres[0]
480 origin = searchres[1]
481 nodeinfo_url = searchres[2]
482 elif not validators.domain(blocked):
483 print(f"WARNING: blocked='{blocked}',software='{software}' is not a valid domain name - skipped!")
486 # DEBUG: print("DEBUG: Looking up instance by domain:", blocked)
487 if not validators.domain(blocked):
488 print(f"WARNING: blocked='{blocked}',software='{software}' is not a valid domain name - skipped!")
490 elif not fba.is_instance_registered(blocked):
491 # DEBUG: print(f"DEBUG: Domain blocked='{blocked}' wasn't found, adding ..., blocker='{blocker}',origin='{origin}',nodeinfo_url='{nodeinfo_url}'")
492 fba.add_instance(blocked, blocker, sys.argv[0], nodeinfo_url)
494 if not fba.is_instance_blocked(blocker, blocked, "reject"):
495 # DEBUG: print(f"DEBUG: blocker='{blocker}' is blocking '{blocked}' for unknown reason at this point")
496 fba.block_instance(blocker, blocked, "unknown", "reject")
503 # DEBUG: print(f"DEBUG: Updating block last seen for blocker='{blocker}',blocked='{blocked}' ...")
504 fba.update_last_seen(blocker, blocked, "reject")
506 if "public_comment" in peer:
507 # DEBUG: print("DEBUG: Updating block reason:", blocker, blocked, peer["public_comment"])
508 fba.update_block_reason(peer["public_comment"], blocker, blocked, "reject")
510 for entry in blockdict:
511 if entry["blocked"] == blocked:
512 # DEBUG: print(f"DEBUG: Setting block reason for blocked='{blocked}':'{peer['public_comment']}'")
513 entry["reason"] = peer["public_comment"]
515 # DEBUG: print("DEBUG: Committing changes ...")
516 fba.connection.commit()
517 except Exception as e:
518 print(f"ERROR: blocker='{blocker}',software='{software}',exception[{type(e)}]:'{str(e)}'")
520 print("WARNING: Unknown software:", blocker, software)
522 if config.get("bot_enabled") and len(blockdict) > 0:
523 send_bot_post(blocker, blockdict)