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/>.
28 "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() - fba.config["recheck_block"]]
31 rows = fba.cursor.fetchall()
32 print(f"INFO: Checking {len(rows)} entries ...")
33 for blocker, software, origin, nodeinfo_url in rows:
34 # NOISY-DEBUG: print("DEBUG: BEFORE blocker,software,origin,nodeinfo_url:", blocker, software, origin, nodeinfo_url)
36 blocker = fba.tidyup(blocker)
37 # NOISY-DEBUG: print("DEBUG: AFTER blocker,software:", blocker, software)
40 print("WARNING: blocker is now empty!")
42 elif fba.is_blacklisted(blocker):
43 print(f"WARNING: blocker='{blocker}' is blacklisted now!")
46 # NOISY-DEBUG: print(f"DEBUG: blocker='{blocker}'")
47 fba.update_last_blocked(blocker)
49 if software == "pleroma":
50 print("INFO: blocker:", blocker)
53 json = fba.fetch_nodeinfo(blocker, nodeinfo_url)
55 print("WARNING: Could not fetch nodeinfo from blocker:", blocker)
58 print("DEBUG: Updating nodeinfo:", blocker)
59 fba.update_last_nodeinfo(blocker)
61 federation = json["metadata"]["federation"]
63 if "enabled" in federation:
64 # NOISY-DEBUG: print("DEBUG: Instance has no block list to analyze:", blocker)
67 if "mrf_simple" in federation:
68 for block_level, blocks in (
69 {**federation["mrf_simple"],
70 **{"quarantined_instances": federation["quarantined_instances"]}}
72 # NOISY-DEBUG: print("DEBUG: block_level, blocks():", block_level, len(blocks))
73 block_level = fba.tidyup(block_level)
74 # NOISY-DEBUG: print("DEBUG: BEFORE block_level:", block_level)
77 print("WARNING: block_level is now empty!")
80 for blocked in blocks:
81 # NOISY-DEBUG: print("DEBUG: BEFORE blocked:", blocked)
82 blocked = fba.tidyup(blocked)
83 # NOISY-DEBUG: print("DEBUG: AFTER blocked:", blocked)
86 print("WARNING: blocked is empty after fba.tidyup():", blocker, block_level)
89 if blocked.count("*") > 1:
90 # -ACK!-oma also started obscuring domains without hash
92 "SELECT domain, nodeinfo_url FROM instances WHERE domain LIKE ? ORDER BY rowid LIMIT 1", [blocked.replace("*", "_")]
94 searchres = fba.cursor.fetchone()
95 # NOISY-DEBUG: print("DEBUG: searchres[]:", type(searchres))
97 blocked = searchres[0]
98 nodeinfo_url = searchres[1]
99 # NOISY-DEBUG: print("DEBUG: Looked up domain:", blocked)
101 # NOISY-DEBUG: print("DEBUG: Looking up instance by domain:", blocked)
102 if not fba.is_instance_registered(blocked):
103 # NOISY-DEBUG: print(f"DEBUG: Domain blocked='{blocked}' wasn't found, adding ..., blocker='{blocker}',origin='{origin}',nodeinfo_url='{nodeinfo_url}'")
104 fba.add_instance(blocked, blocker, origin, nodeinfo_url)
107 "SELECT * FROM blocks WHERE blocker = ? AND blocked = ? AND block_level = ? LIMIT 1",
115 if fba.cursor.fetchone() == None:
116 # NOISY-DEBUG: print("DEBUG: Blocking:", blocker, blocked, block_level)
117 fba.block_instance(blocker, blocked, "unknown", block_level)
119 if block_level == "reject":
120 # NOISY-DEBUG: print("DEBUG: Adding to blockdict:", blocked)
127 # NOISY-DEBUG: print("DEBUG: Updating last_seen:", blocker, blocked, block_level)
128 fba.update_last_seen(blocker, blocked, block_level)
130 fba.connection.commit()
133 if "mrf_simple_info" in federation:
134 # NOISY-DEBUG: print("DEBUG: Found mrf_simple_info:", blocker)
135 for block_level, info in (
136 {**federation["mrf_simple_info"],
137 **(federation["quarantined_instances_info"]
138 if "quarantined_instances_info" in federation
141 # NOISY-DEBUG: print("DEBUG: block_level, info.items():", block_level, len(info.items()))
142 block_level = fba.tidyup(block_level)
143 # NOISY-DEBUG: print("DEBUG: BEFORE block_level:", block_level)
145 if block_level == "":
146 print("WARNING: block_level is now empty!")
149 for blocked, reason in info.items():
150 # NOISY-DEBUG: print("DEBUG: BEFORE blocked:", blocked)
151 blocked = fba.tidyup(blocked)
152 # NOISY-DEBUG: print("DEBUG: AFTER blocked:", blocked)
155 print("WARNING: blocked is empty after fba.tidyup():", blocker, block_level)
157 elif blocked.count("*") > 1:
158 # same domain guess as above, but for reasons field
160 "SELECT domain, origin, nodeinfo_url FROM instances WHERE domain LIKE ? ORDER BY rowid LIMIT 1", [blocked.replace("*", "_")]
162 searchres = fba.cursor.fetchone()
164 if searchres != None:
165 blocked = searchres[0]
166 origin = searchres[1]
167 nodeinfo_url = searchres[2]
169 # NOISY-DEBUG: print("DEBUG: Looking up instance by domain:", blocked)
170 if not fba.is_instance_registered(blocked):
171 # NOISY-DEBUG: print(f"DEBUG: Domain blocked='{blocked}' wasn't found, adding ..., blocker='{blocker}',origin='{origin}',nodeinfo_url='{nodeinfo_url}'")
172 fba.add_instance(blocked, blocker, origin, nodeinfo_url)
174 # NOISY-DEBUG: print("DEBUG: Updating block reason:", blocker, blocked, reason["reason"])
175 fba.update_block_reason(reason["reason"], blocker, blocked, block_level)
177 for entry in blockdict:
178 if entry["blocked"] == blocked:
179 # NOISY-DEBUG: print("DEBUG: Updating entry reason:", blocked)
180 entry["reason"] = reason["reason"]
182 fba.connection.commit()
183 except Exception as e:
184 print(f"ERROR: blocker='{blocker}',software='{software}',exception[{type(e)}]:'{str(e)}'")
185 elif software == "mastodon":
186 print("INFO: blocker:", blocker)
188 # json endpoint for newer mastodongs
192 "media_removal" : [],
193 "followers_only": [],
197 # handling CSRF, I've saw at least one server requiring it to access the endpoint
198 # NOISY-DEBUG: print("DEBUG: Fetching meta:", blocker)
199 meta = bs4.BeautifulSoup(
200 reqto.get(f"https://{blocker}/about", headers=fba.headers, timeout=(fba.config["connection_timeout"], config["read_timeout"])).text,
204 csrf = meta.find("meta", attrs={"name": "csrf-token"})["content"]
205 # NOISY-DEBUG: print("DEBUG: Adding CSRF token:", blocker, csrf)
206 reqheaders = {**fba.api_headers, **{"X-CSRF-Token": csrf}}
208 # NOISY-DEBUG: print("DEBUG: No CSRF token found, using normal headers:", blocker)
209 reqheaders = fba.api_headers
211 # NOISY-DEBUG: print("DEBUG: Quering API domain_blocks:", blocker)
212 blocks = reqto.get(f"https://{blocker}/api/v1/instance/domain_blocks", headers=reqheaders, timeout=(fba.config["connection_timeout"], config["read_timeout"])).json()
214 # NOISY-DEBUG: print("DEBUG: blocks():", len(blocks))
217 'domain': block['domain'],
218 'hash' : block['digest'],
219 'reason': block['comment']
222 # NOISY-DEBUG: print("DEBUG: severity,domain,hash,comment:", block['severity'], block['domain'], block['digest'], block['comment'])
223 if block['severity'] == 'suspend':
224 json['reject'].append(entry)
225 elif block['severity'] == 'silence':
226 json['followers_only'].append(entry)
227 elif block['severity'] == 'reject_media':
228 json['media_removal'].append(entry)
229 elif block['severity'] == 'reject_reports':
230 json['report_removal'].append(entry)
232 print("WARNING: Unknown severity:", block['severity'], block['domain'])
234 # NOISY-DEBUG: print("DEBUG: Failed, Trying mastodon-specific fetches:", blocker)
235 json = fba.get_mastodon_blocks(blocker)
237 # NOISY-DEBUG: print("DEBUG: json.items():", blocker, len(json.items()))
238 for block_level, blocks in json.items():
239 # NOISY-DEBUG: print("DEBUG: blocker,block_level,blocks():", blocker, block_level, len(blocks))
240 block_level = fba.tidyup(block_level)
241 # NOISY-DEBUG: print("DEBUG: AFTER-block_level:", block_level)
242 if block_level == "":
243 print("WARNING: block_level is empty, blocker:", blocker)
246 for instance in blocks:
247 blocked, blocked_hash, reason = instance.values()
248 # NOISY-DEBUG: print("DEBUG: blocked,hash,reason:", blocked, blocked_hash, reason)
249 blocked = fba.tidyup(blocked)
250 # NOISY-DEBUG: print("DEBUG: AFTER-blocked:", blocked)
253 print("WARNING: blocked is empty:", blocker)
255 elif blocked.count("*") < 1:
256 # No obsfucation for this instance
258 "SELECT hash FROM instances WHERE domain = ? LIMIT 1", [blocked]
261 if fba.cursor.fetchone() == None:
262 # NOISY-DEBUG: print("DEBUG: Hash wasn't found, adding:", blocked, blocker)
263 fba.add_instance(blocked, blocker, origin, nodeinfo_url)
265 # Doing the hash search for instance names as well to tidy up DB
267 "SELECT domain, origin, nodeinfo_url FROM instances WHERE hash = ? LIMIT 1", [blocked_hash]
269 searchres = fba.cursor.fetchone()
271 if searchres != None:
272 # NOISY-DEBUG: print("DEBUG: Updating domain: ", searchres[0])
273 blocked = searchres[0]
274 origin = searchres[1]
275 nodeinfo_url = searchres[2]
277 # NOISY-DEBUG: print("DEBUG: Looking up instance by domain:", blocked)
278 if not fba.is_instance_registered(blocked):
279 # NOISY-DEBUG: print(f"DEBUG: Domain blocked='{blocked}' wasn't found, adding ..., blocker='{blocker}',origin='{origin}',nodeinfo_url='{nodeinfo_url}'")
280 fba.add_instance(blocked, blocker, origin, nodeinfo_url)
283 "SELECT * FROM blocks WHERE blocker = ? AND blocked = ? AND block_level = ? LIMIT 1",
286 blocked if blocked.count("*") <= 1 else blocked_hash,
291 if fba.cursor.fetchone() == None:
292 fba.block_instance(blocker, blocked if blocked.count("*") <= 1 else blocked_hash, reason, block_level)
294 if block_level == "reject":
301 fba.update_last_seen(blocker, blocked if blocked.count("*") <= 1 else blocked_hash, block_level)
304 # NOISY-DEBUG: print("DEBUG: Updating block reason:", blocker, blocked, reason)
305 fba.update_block_reason(reason, blocker, blocked if blocked.count("*") <= 1 else blocked_hash, block_level)
307 fba.connection.commit()
308 except Exception as e:
309 print(f"ERROR: blocker='{blocker}',software='{software}',exception[{type(e)}]:'{str(e)}'")
310 elif software == "friendica" or software == "misskey" or software == "bookwyrm" or software == "takahe":
311 print("INFO: blocker:", blocker)
313 if software == "friendica":
314 json = fba.get_friendica_blocks(blocker)
315 elif software == "misskey":
316 json = fba.get_misskey_blocks(blocker)
317 elif software == "bookwyrm":
318 print("WARNING: bookwyrm is not fully supported for fetching blacklist!", blocker)
319 #json = fba.get_bookwyrm_blocks(blocker)
320 elif software == "takahe":
321 print("WARNING: takahe is not fully supported for fetching blacklist!", blocker)
322 #json = fba.get_takahe_blocks(blocker)
324 for block_level, blocks in json.items():
325 # NOISY-DEBUG: print("DEBUG: blocker,block_level,blocks():", blocker, block_level, len(blocks))
326 block_level = fba.tidyup(block_level)
327 # NOISY-DEBUG: print("DEBUG: AFTER-block_level:", block_level)
328 if block_level == "":
329 print("WARNING: block_level is empty, blocker:", blocker)
332 for instance in blocks:
333 blocked, reason = instance.values()
334 # NOISY-DEBUG: print("DEBUG: BEFORE blocked:", blocked)
335 blocked = fba.tidyup(blocked)
336 # NOISY-DEBUG: print("DEBUG: AFTER blocked:", blocked)
339 print("WARNING: blocked is empty:", blocker)
341 elif blocked.count("*") > 0:
342 # Some friendica servers also obscure domains without hash
344 "SELECT domain, origin, nodeinfo_url FROM instances WHERE domain LIKE ? ORDER BY rowid LIMIT 1", [blocked.replace("*", "_")]
347 searchres = fba.cursor.fetchone()
349 if searchres != None:
350 blocked = searchres[0]
351 origin = searchres[1]
352 nodeinfo_url = searchres[2]
354 if blocked.count("?") > 0:
355 # Some obscure them with question marks, not sure if that's dependent on version or not
357 "SELECT domain, origin, nodeinfo_url FROM instances WHERE domain LIKE ? ORDER BY rowid LIMIT 1", [blocked.replace("?", "_")]
359 searchres = fba.cursor.fetchone()
360 if searchres != None:
361 blocked = searchres[0]
362 origin = searchres[1]
363 nodeinfo_url = searchres[2]
365 # NOISY-DEBUG: print("DEBUG: AFTER-blocked:", blocked)
366 if not fba.is_instance_registered(blocked):
367 # NOISY-DEBUG: print("DEBUG: Hash wasn't found, adding:", blocked, blocker)
368 fba.add_instance(blocked, blocker, origin, nodeinfo_url)
371 "SELECT * FROM blocks WHERE blocker = ? AND blocked = ?",
375 if fba.cursor.fetchone() == None:
376 fba.block_instance(blocker, blocked, reason, block_level)
378 if block_level == "reject":
385 fba.update_last_seen(blocker, blocked, block_level)
388 # NOISY-DEBUG: print("DEBUG: Updating block reason:", blocker, blocked, reason)
389 fba.update_block_reason(reason, blocker, blocked, block_level)
391 fba.connection.commit()
392 except Exception as e:
393 print(f"ERROR: blocker='{blocker}',software='{software}',exception[{type(e)}]:'{str(e)}'")
394 elif software == "gotosocial":
395 print("INFO: blocker:", blocker)
398 federation = reqto.get(f"https://{blocker}{get_peers_url}?filter=suspended", headers=fba.api_headers, timeout=(fba.config["connection_timeout"], config["read_timeout"])).json()
400 if (federation == None):
401 print("WARNING: No valid response:", blocker);
402 elif "error" in federation:
403 print("WARNING: API returned error:", federation["error"])
405 # NOISY-DEBUG: print("DEBUG: Checking fenderation():", len(federation))
406 for peer in federation:
407 blocked = peer["domain"].lower()
408 # NOISY-DEBUG: print("DEBUG: BEFORE blocked:", blocked)
409 blocked = fba.tidyup(blocked)
410 # NOISY-DEBUG: print("DEBUG: AFTER blocked:", blocked)
413 print("WARNING: blocked is empty:", blocker)
415 elif blocked.count("*") > 0:
416 # GTS does not have hashes for obscured domains, so we have to guess it
418 "SELECT domain, origin, nodeinfo_url FROM instances WHERE domain LIKE ? ORDER BY rowid LIMIT 1", [blocked.replace("*", "_")]
420 searchres = fba.cursor.fetchone()
422 if searchres != None:
423 blocked = searchres[0]
424 origin = searchres[1]
425 nodeinfo_url = searchres[2]
427 if not fba.is_instance_registered(blocked):
428 # NOISY-DEBUG: print(f"DEBUG: Domain blocked='{blocked}' wasn't found, adding ..., blocker='{blocker}',origin='{origin}',nodeinfo_url='{nodeinfo_url}'")
429 fba.add_instance(blocked, blocker, origin, nodeinfo_url)
432 "SELECT * FROM blocks WHERE blocker = ? AND blocked = ? AND block_level = ? LIMIT 1",
440 if fba.cursor.fetchone() == None:
441 # NOISY-DEBUG: print(f"DEBUG: blocker='{blocker}' is blocking '{blocked}' for unknown reason at this point")
442 fba.block_instance(blocker, blocked, "unknown", "reject")
450 fba.update_last_seen(blocker, blocked, "reject")
452 if "public_comment" in peer:
453 # NOISY-DEBUG: print("DEBUG: Updating block reason:", blocker, blocked, peer["public_comment"])
454 fba.update_block_reason(peer["public_comment"], blocker, blocked, "reject")
456 for entry in blockdict:
457 if entry["blocked"] == blocked:
458 # NOISY-DEBUG: print(f"DEBUG: Setting block reason for blocked='{blocked}':'{peer['public_comment']}'")
459 entry["reason"] = peer["public_comment"]
461 fba.connection.commit()
462 except Exception as e:
463 print(f"ERROR: blocker='{blocker}',software='{software}',exception[{type(e)}]:'{str(e)}'")
465 print("WARNING: Unknown software:", blocker, software)
467 if fba.config["bot_enabled"] and len(blockdict) > 0:
468 send_bot_post(blocker, blockdict)
472 fba.connection.close()