]> git.mxchange.org Git - fba.git/blob - fba.py
Continued:
[fba.git] / fba.py
1 from bs4 import BeautifulSoup
2 from hashlib import sha256
3
4 import reqto
5 import re
6 import sqlite3
7 import json
8 import sys
9 import time
10
11 with open("config.json") as f:
12     config = json.loads(f.read())
13
14 # Don't check these, known trolls/flooders/testing/developing
15 blacklist = [
16     # Floods network with fake nodes as "research" project
17     "activitypub-troll.cf",
18     # Similar troll
19     "gab.best",
20     # Similar troll
21     "4chan.icu",
22     # Flooder (?)
23     "social.shrimpcam.pw",
24     # Flooder (?)
25     "mastotroll.netz.org",
26     # Testing/developing installations
27     "ngrok.io",
28 ]
29
30 # Array with pending errors needed to be written to database
31 pending_errors = {
32 }
33
34 # "rel" identifiers (no real URLs)
35 nodeinfo_identifier = [
36     "http://nodeinfo.diaspora.software/ns/schema/2.1",
37     "http://nodeinfo.diaspora.software/ns/schema/2.0",
38     "http://nodeinfo.diaspora.software/ns/schema/1.1",
39     "http://nodeinfo.diaspora.software/ns/schema/1.0",
40 ]
41
42 # HTTP headers for requests
43 headers = {
44     "user-agent": config["useragent"],
45 }
46
47 # Found info from node, such as nodeinfo URL, detection mode that needs to be
48 # written to database. Both arrays must be filled at the same time or else
49 # update_nodeinfos() will fail
50 nodeinfos = {
51     # Detection mode: AUTO_DISCOVERY or STATIC_CHECKS
52     "detection_mode": {},
53     # Found nodeinfo URL
54     "nodeinfo_url": {},
55     # Where to fetch peers (other instances)
56     "get_peers_url": {},
57 }
58
59 # URL for fetching peers
60 get_peers_url = "/api/v1/instance/peers"
61
62 # Connect to database
63 connection = sqlite3.connect("blocks.db")
64 cursor = connection.cursor()
65
66 def is_blacklisted(domain: str) -> bool:
67     blacklisted = False
68     for peer in blacklist:
69         if peer in domain:
70             blacklisted = True
71
72     return blacklisted
73
74 def get_hash(domain: str) -> str:
75     return sha256(domain.encode("utf-8")).hexdigest()
76
77 def update_last_blocked(domain: str):
78     # NOISY-DEBUG: print("DEBUG: Updating last_blocked for domain", domain)
79     try:
80         cursor.execute("UPDATE instances SET last_blocked = ?, last_updated = ? WHERE domain = ? LIMIT 1", [
81             time.time(),
82             time.time(),
83             domain
84         ])
85
86         if cursor.rowcount == 0:
87             print("WARNING: Did not update any rows:", domain)
88
89     except BaseException as e:
90         print("ERROR: failed SQL query:", domain, e)
91         sys.exit(255)
92
93     # NOISY-DEBUG: print("DEBUG: EXIT!")
94
95 def update_nodeinfos(domain: str):
96     # NOISY-DEBUG: print("DEBUG: Updating nodeinfo for domain:", domain)
97     sql_string = ''
98     fields = list()
99     for key in nodeinfos:
100         # NOISY-DEBUG: print("DEBUG: key:", key)
101         if domain in nodeinfos[key]:
102            # NOISY-DEBUG: print(f"DEBUG: Adding '{nodeinfos[key][domain]}' for key='{key}' ...")
103            fields.append(nodeinfos[key][domain])
104            sql_string += f" {key} = ?,"
105
106     sql_string = sql_string[:-1]
107     fields.append(domain)
108     # NOISY-DEBUG: print(f"DEBUG: sql_string='{sql_string}',fields()={len(fields)}")
109
110     sql = "UPDATE instances SET" + sql_string + " WHERE domain = ? LIMIT 1"
111     # NOISY-DEBUG: print("DEBUG: sql:", sql)
112
113     try:
114         # NOISY-DEBUG: print("DEBUG: Executing SQL:", sql)
115         cursor.execute(sql, fields)
116         # NOISY-DEBUG: print(f"DEBUG: Success! (rowcount={cursor.rowcount })")
117
118         if cursor.rowcount == 0:
119             print("WARNING: Did not update any rows:", domain)
120
121     except BaseException as e:
122         print(f"ERROR: failed SQL query: domain='{domain}',sql='{sql}',exception='{e}'")
123         sys.exit(255)
124
125     # NOISY-DEBUG: print("DEBUG: Deleting nodeinfos for domain:", domain)
126     for key in nodeinfos:
127         try:
128             # NOISY-DEBUG: print("DEBUG: Deleting key:", key)
129             del nodeinfos[key][domain]
130         except:
131             pass
132
133     # NOISY-DEBUG: print("DEBUG: EXIT!")
134
135 def update_last_error(domain: str, res: any):
136     # NOISY-DEBUG: print("DEBUG: domain,res[]:", domain, type(res))
137     try:
138         # NOISY-DEBUG: print("DEBUG: BEFORE res[]:", type(res))
139         if isinstance(res, BaseException) or isinstance(res, json.JSONDecodeError):
140             res = str(res)
141
142         # NOISY-DEBUG: print("DEBUG: AFTER res[]:", type(res))
143         if type(res) is str:
144             cursor.execute("UPDATE instances SET last_status_code = 999, last_error_details = ?, last_updated = ? WHERE domain = ? LIMIT 1", [
145                 res,
146                 time.time(),
147                 domain
148             ])
149         else:
150             cursor.execute("UPDATE instances SET last_status_code = ?, last_error_details = ?, last_updated = ? WHERE domain = ? LIMIT 1", [
151                 res.status_code,
152                 res.reason,
153                 time.time(),
154                 domain
155             ])
156
157         if cursor.rowcount == 0:
158             # NOISY-DEBUG: print("DEBUG: Did not update any rows:", domain)
159             pending_errors[domain] = res
160
161     except BaseException as e:
162         print("ERROR: failed SQL query:", domain, e)
163         sys.exit(255)
164
165     # NOISY-DEBUG: print(f"DEBUG: Deleting domain='{domain}' from pending_errors")
166     del pending_errors[domain]
167     # NOISY-DEBUG: print("DEBUG: EXIT!")
168
169 def update_last_nodeinfo(domain: str):
170     # NOISY-DEBUG: print("DEBUG: Updating last_nodeinfo for domain:", domain)
171     try:
172         cursor.execute("UPDATE instances SET last_nodeinfo = ?, last_updated = ? WHERE domain = ? LIMIT 1", [
173             time.time(),
174             time.time(),
175             domain
176         ])
177
178         if cursor.rowcount == 0:
179             print("WARNING: Did not update any rows:", domain)
180
181     except BaseException as e:
182         print("ERROR: failed SQL query:", domain, e)
183         sys.exit(255)
184
185     connection.commit()
186     # NOISY-DEBUG: print("DEBUG: EXIT!")
187
188 def get_peers(domain: str, software: str) -> list:
189     # NOISY-DEBUG: print("DEBUG: Getting peers for domain:", domain, software)
190     peers = None
191
192     if software == "lemmy":
193         # NOISY-DEBUG: print(f"DEBUG: domain='{domain}' is Lemmy. fetching JSON ...")
194         try:
195             res = reqto.get(f"https://{domain}/api/v3/site", headers=headers, timeout=(config["connection_timeout"], config["read_timeout"]))
196
197             if res.ok and res.json() is not None:
198                 # NOISY-DEBUG: print("DEBUG: Success, res.json():", len(res.json()))
199                 json = res.json()
200
201                 if "federated_instances" in json and "linked" in json["federated_instances"]:
202                     # NOISY-DEBUG: print("DEBUG: Found federated_instances", domain)
203                     peers = json["federated_instances"]["linked"] + json["federated_instances"]["allowed"] + json["federated_instances"]["blocked"]
204
205         except BaseException as e:
206             print("WARNING: Exception during fetching JSON:", domain, e)
207
208         # NOISY-DEBUG: print("DEBUG: Returning peers[]:", type(peers))
209         return peers
210
211     try:
212         res = reqto.get(f"https://{domain}/api/v1/instance/peers", headers=headers, timeout=(config["connection_timeout"], config["read_timeout"]))
213
214         if not res.ok or res.status_code >= 400:
215             res = reqto.get(f"https://{domain}/api/v3/site", headers=headers, timeout=(config["connection_timeout"], config["read_timeout"]))
216
217             if "federated_instances" in json and "linked" in json["federated_instances"]:
218                 # NOISY-DEBUG: print("DEBUG: Found federated_instances", domain)
219                 peers = json["federated_instances"]["linked"] + json["federated_instances"]["allowed"] + json["federated_instances"]["blocked"]
220             else:
221                 print("WARNING: Could not reach any JSON API:", domain)
222                 update_last_error(domain, res)
223         else:
224             # NOISY-DEBUG: print("DEBUG: Querying API was successful:", domain, len(res.json()))
225             peers = res.json()
226             nodeinfos["get_peers_url"][domain] = get_peers_url
227
228     except BaseException as e:
229         print("WARNING: Some error during get():", domain, e)
230         update_last_error(domain, e)
231
232     update_last_nodeinfo(domain)
233
234     # NOISY-DEBUG: print("DEBUG: Returning peers[]:", type(peers))
235     return peers
236
237 def post_json_api(domain: str, path: str, data: str) -> list:
238     # NOISY-DEBUG: print("DEBUG: Sending POST to domain,path,data:", domain, path, data)
239     json = {}
240     try:
241         res = reqto.post(f"https://{domain}{path}", data=data, headers=headers, timeout=(config["connection_timeout"], config["read_timeout"]))
242
243         if not res.ok or res.status_code >= 400:
244             print("WARNING: Cannot query JSON API:", domain, path, data, res.status_code)
245             update_last_error(domain, res)
246             raise
247
248         update_last_nodeinfo(domain)
249         json = res.json()
250     except BaseException as e:
251         print("WARNING: Some error during post():", domain, path, data, e)
252
253     # NOISY-DEBUG: print("DEBUG: Returning json():", len(json))
254     return json
255
256 def fetch_nodeinfo(domain: str) -> list:
257     # NOISY-DEBUG: print("DEBUG: Fetching nodeinfo from domain:", domain)
258
259     nodeinfo = fetch_wellknown_nodeinfo(domain)
260     # NOISY-DEBUG: print("DEBUG: nodeinfo:", len(nodeinfo))
261
262     if len(nodeinfo) > 0:
263         # NOISY-DEBUG: print("DEBUG: Returning auto-discovered nodeinfo:", len(nodeinfo))
264         return nodeinfo
265
266     requests = [
267        f"https://{domain}/nodeinfo/2.1.json",
268        f"https://{domain}/nodeinfo/2.1",
269        f"https://{domain}/nodeinfo/2.0.json",
270        f"https://{domain}/nodeinfo/2.0",
271        f"https://{domain}/nodeinfo/1.0",
272        f"https://{domain}/api/v1/instance"
273     ]
274
275     json = {}
276     for request in requests:
277         try:
278             # NOISY-DEBUG: print("DEBUG: Fetching request:", request)
279             res = reqto.get(request, headers=headers, timeout=(config["connection_timeout"], config["read_timeout"]))
280
281             # NOISY-DEBUG: print("DEBUG: res.ok,res.json[]:", res.ok, type(res.json()))
282             if res.ok and res.json() is dict:
283                 # NOISY-DEBUG: print("DEBUG: Success:", request)
284                 json = res.json()
285                 nodeinfos["detection_mode"][domain] = "STATIC_CHECK"
286                 nodeinfos["nodeinfo_url"][domain] = request
287                 break
288             elif not res.ok or res.status_code >= 400:
289                 print("WARNING: Failed fetching nodeinfo from domain:", domain)
290                 update_last_error(domain, res)
291                 continue
292
293         except BaseException as e:
294             # NOISY-DEBUG: print("DEBUG: Cannot fetch API request:", request)
295             update_last_error(domain, e)
296             pass
297
298     # NOISY-DEBUG: print("DEBUG: json[]:", type(json))
299     if json is None or len(json) == 0:
300         print("WARNING: Failed fetching nodeinfo from domain:", domain)
301
302     # NOISY-DEBUG: print("DEBUG: Returning json[]:", type(json))
303     return json
304
305 def fetch_wellknown_nodeinfo(domain: str) -> list:
306     # NOISY-DEBUG: print("DEBUG: Fetching .well-known info for domain:", domain)
307     json = {}
308
309     try:
310         res = reqto.get(f"https://{domain}/.well-known/nodeinfo", headers=headers, timeout=(config["connection_timeout"], config["read_timeout"]))
311         # NOISY-DEBUG: print("DEBUG: domain,res.ok,res.json[]:", domain, res.ok, type(res.json()))
312         if res.ok and res.json() is dict:
313             nodeinfo = res.json()
314             # NOISY-DEBUG: print("DEBUG: Found entries:", len(nodeinfo), domain)
315             if "links" in nodeinfo:
316                 # NOISY-DEBUG: print("DEBUG: Found links in nodeinfo():", len(nodeinfo["links"]))
317                 for link in nodeinfo["links"]:
318                     # NOISY-DEBUG: print("DEBUG: rel,href:", link["rel"], link["href"])
319                     if link["rel"] in nodeinfo_identifier:
320                         # NOISY-DEBUG: print("DEBUG: Fetching nodeinfo from:", link["href"])
321                         res = reqto.get(link["href"])
322                         # NOISY-DEBUG: print("DEBUG: href,res.ok,res.status_code:", link["href"], res.ok, res.status_code)
323                         if res.ok and res.json() is dict:
324                             # NOISY-DEBUG: print("DEBUG: Found JSON nodeinfo():", len(res.json()))
325                             json = res.json()
326                             nodeinfos["detection_mode"][domain] = "AUTO_DISCOVERY"
327                             nodeinfos["nodeinfo_url"][domain] = link["href"]
328                             break
329                     else:
330                         print("WARNING: Unknown 'rel' value:", domain, link["rel"])
331             else:
332                 print("WARNING: nodeinfo does not contain 'links':", domain)
333
334     except BaseException as e:
335         print("WARNING: Failed fetching .well-known info:", domain)
336         update_last_error(domain, e)
337         pass
338
339     # NOISY-DEBUG: print("DEBUG: Returning json[]:", type(json))
340     return json
341
342 def determine_software(domain: str) -> str:
343     # NOISY-DEBUG: print("DEBUG: Determining software for domain:", domain)
344     software = None
345
346     json = fetch_nodeinfo(domain)
347     # NOISY-DEBUG: print("DEBUG: json[]:", type(json))
348
349     if json is None or len(json) == 0:
350         # NOISY-DEBUG: print("DEBUG: Could not determine software type:", domain)
351         return None
352
353     # NOISY-DEBUG: print("DEBUG: json():", len(json), json)
354     if "status" in json and json["status"] == "error" and "message" in json:
355         print("WARNING: JSON response is an error:", json["message"])
356         update_last_error(domain, json["message"])
357         return None
358     elif "software" not in json or "name" not in json["software"]:
359         print("WARNING: JSON response does not include [software][name], guessing ...")
360         found = 0
361         for element in {"uri", "title", "short_description", "description", "email", "version", "urls", "stats", "thumbnail", "languages", "contact_account", "registrations", "approval_required"}:
362             # NOISY-DEBUG: print("DEBUG: element:", element)
363             if element in json:
364                 found = found + 1
365
366         # NOISY-DEBUG: print("DEBUG: Found elements:", found)
367         if found == len(json):
368             # NOISY-DEBUG: print("DEBUG: Maybe is Mastodon:", domain)
369             return "mastodon"
370
371         print(f"WARNING: Cannot guess software type: domain='{domain}',found={found},json()={len(json)}")
372         return None
373
374     software = tidyup(json["software"]["name"])
375
376     # NOISY-DEBUG: print("DEBUG: tidyup software:", software)
377     if software in ["akkoma", "rebased"]:
378         # NOISY-DEBUG: print("DEBUG: Setting pleroma:", domain, software)
379         software = "pleroma"
380     elif software in ["hometown", "ecko"]:
381         # NOISY-DEBUG: print("DEBUG: Setting mastodon:", domain, software)
382         software = "mastodon"
383     elif software in ["calckey", "groundpolis", "foundkey", "cherrypick", "meisskey"]:
384         # NOISY-DEBUG: print("DEBUG: Setting misskey:", domain, software)
385         software = "misskey"
386     elif software.find("/") > 0:
387         print("WARNING: Spliting of path:", software)
388         software = software.split("/")[-1];
389     elif software.find("|") > 0:
390         print("WARNING: Spliting of path:", software)
391         software = software.split("|")[0].strip();
392
393     if software == "":
394         print("WARNING: tidyup() left no software name behind:", domain)
395         software = None
396
397     # NOISY-DEBUG: print("DEBUG: Returning domain,software:", domain, software)
398     return software
399
400 def update_block_reason(reason: str, blocker: str, blocked: str, block_level: str):
401     # NOISY-DEBUG: print("DEBUG: Updating block reason:", reason, blocker, blocked, block_level)
402     try:
403         cursor.execute(
404             "UPDATE blocks SET reason = ?, last_seen = ? WHERE blocker = ? AND blocked = ? AND block_level = ? AND reason = ''",
405             (
406                 reason,
407                 time.time(),
408                 blocker,
409                 blocked,
410                 block_level
411             ),
412         )
413
414         if cursor.rowcount == 0:
415             print("WARNING: Did not update any rows:", domain)
416
417     except baseException as e:
418         print("ERROR: failed SQL query:", reason, blocker, blocked, block_level, e)
419         sys.exit(255)
420
421 def update_last_seen(blocker: str, blocked: str, block_level: str):
422     # NOISY-DEBUG: print("DEBUG: Updating last_seen for:", blocker, blocked, block_level)
423     try:
424         cursor.execute(
425             "UPDATE blocks SET last_seen = ? WHERE blocker = ? AND blocked = ? AND block_level = ?",
426             (
427                 time.time(),
428                 blocker,
429                 blocked,
430                 block_level
431             )
432         )
433
434         if cursor.rowcount == 0:
435             print("WARNING: Did not update any rows:", domain)
436
437     except BaseException as e:
438         print("ERROR: failed SQL query:", last_seen, blocker, blocked, block_level, e)
439         sys.exit(255)
440
441     # NOISY-DEBUG: print("DEBUG: EXIT!")
442
443 def block_instance(blocker: str, blocked: str, reason: str, block_level: str):
444     # NOISY-DEBUG: print("DEBUG: blocker,blocked,reason,block_level:", blocker, blocked, reason, block_level)
445     if blocker.find("@") > 0:
446         print("WARNING: Bad blocker:", blocker)
447         raise
448     elif blocked.find("@") > 0:
449         print("WARNING: Bad blocked:", blocked)
450         raise
451
452     print("INFO: New block:", blocker, blocked, reason, block_level, first_added, last_seen)
453     try:
454         cursor.execute(
455             "INSERT INTO blocks (blocker, blocked, reason, block_level, first_added, last_seen) VALUES(?, ?, ?, ?, ?, ?)",
456              (
457                  blocker,
458                  blocked,
459                  reason,
460                  block_level,
461                  time.time(),
462                  time.time()
463              ),
464         )
465
466     except BaseException as e:
467         print("ERROR: failed SQL query:", blocker, blocked, reason, block_level, first_added, last_seen, e)
468         sys.exit(255)
469
470     # NOISY-DEBUG: print("DEBUG: EXIT!")
471
472 def add_instance(domain: str, origin: str, originator: str):
473     # NOISY-DEBUG: print("DEBUG: domain,origin:", domain, origin, originator)
474     if domain.find("@") > 0:
475         print("WARNING: Bad domain name:", domain)
476         raise
477     elif origin is not None and origin.find("@") > 0:
478         print("WARNING: Bad origin name:", origin)
479         raise
480
481     software = determine_software(domain)
482     # NOISY-DEBUG: print("DEBUG: Determined software:", software)
483
484     print(f"INFO: Adding new instance {domain} (origin: {origin})")
485     try:
486         cursor.execute(
487             "INSERT INTO instances (domain, origin, originator, hash, software, first_seen) VALUES (?, ?, ?, ?, ?, ?)",
488             (
489                domain,
490                origin,
491                originator,
492                get_hash(domain),
493                software,
494                time.time()
495             ),
496         )
497
498         if domain in nodeinfos["nodeinfo_url"]:
499             # NOISY-DEBUG # NOISY-DEBUG: print("DEBUG: domain has pending nodeinfo being updated:", domain)
500             update_nodeinfos(domain)
501
502         if domain in pending_errors:
503             # NOISY-DEBUG: print("DEBUG: domain has pending error being updated:", domain)
504             update_last_error(domain, pending_errors[domain])
505             del pending_errors[domain]
506
507     except BaseException as e:
508         print("ERROR: failed SQL query:", domain, e)
509         sys.exit(255)
510     else:
511         # NOISY-DEBUG: print("DEBUG: Updating nodeinfo for domain:", domain)
512         update_last_nodeinfo(domain)
513
514     # NOISY-DEBUG: print("DEBUG: EXIT!")
515
516 def send_bot_post(instance: str, blocks: dict):
517     message = instance + " has blocked the following instances:\n\n"
518     truncated = False
519
520     if len(blocks) > 20:
521         truncated = True
522         blocks = blocks[0 : 19]
523
524     for block in blocks:
525         if block["reason"] == None or block["reason"] == '':
526             message = message + block["blocked"] + " with unspecified reason\n"
527         else:
528             if len(block["reason"]) > 420:
529                 block["reason"] = block["reason"][0:419] + "[…]"
530
531             message = message + block["blocked"] + ' for "' + block["reason"].replace("@", "@\u200b") + '"\n'
532
533     if truncated:
534         message = message + "(the list has been truncated to the first 20 entries)"
535
536     botheaders = {**headers, **{"Authorization": "Bearer " + config["bot_token"]}}
537
538     req = reqto.post(f"{config['bot_instance']}/api/v1/statuses",
539         data={"status":message, "visibility":config['bot_visibility'], "content_type":"text/plain"},
540         headers=botheaders, timeout=10).json()
541
542     return True
543
544 def get_mastodon_blocks(domain: str) -> dict:
545     # NOISY-DEBUG: print("DEBUG: Fetching mastodon blocks from domain:", domain)
546     blocks = {
547         "Suspended servers": [],
548         "Filtered media"   : [],
549         "Limited servers"  : [],
550         "Silenced servers" : [],
551     }
552
553     translations = {
554         "Silenced instances"            : "Silenced servers",
555         "Suspended instances"           : "Suspended servers",
556         "Gesperrte Server"              : "Suspended servers",
557         "Gefilterte Medien"             : "Filtered media",
558         "Stummgeschaltete Server"       : "Silenced servers",
559         "停止済みのサーバー"            : "Suspended servers",
560         "制限中のサーバー"              : "Limited servers",
561         "メディアを拒否しているサーバー": "Filtered media",
562         "サイレンス済みのサーバー"      : "Silenced servers",
563         "שרתים מושעים"                  : "Suspended servers",
564         "מדיה מסוננת"                   : "Filtered media",
565         "שרתים מוגבלים"                 : "Silenced servers",
566         "Serveurs suspendus"            : "Suspended servers",
567         "Médias filtrés"                : "Filtered media",
568         "Serveurs limités"              : "Silenced servers",
569     }
570
571     try:
572         doc = BeautifulSoup(
573             reqto.get(f"https://{domain}/about/more", headers=headers, timeout=(config["connection_timeout"], config["read_timeout"])).text,
574             "html.parser",
575         )
576     except BaseException as e:
577         print("ERROR: Cannot fetch from domain:", domain, e)
578         update_last_error(domain, e)
579         return {}
580
581     for header in doc.find_all("h3"):
582         header_text = header.text
583
584         if header_text in translations:
585             header_text = translations[header_text]
586
587         if header_text in blocks or header_text.lower() in blocks:
588             # replaced find_next_siblings with find_all_next to account for instances that e.g. hide lists in dropdown menu
589             for line in header.find_all_next("table")[0].find_all("tr")[1:]:
590                 blocks[header_text].append(
591                     {
592                         "domain": tidyup(line.find("span").text),
593                         "hash"  : tidyup(line.find("span")["title"][9:]),
594                         "reason": tidyup(line.find_all("td")[1].text),
595                     }
596                 )
597
598     # NOISY-DEBUG: print("DEBUG: Returning blocks for domain:", domain)
599     return {
600         "reject"        : blocks["Suspended servers"],
601         "media_removal" : blocks["Filtered media"],
602         "followers_only": blocks["Limited servers"] + blocks["Silenced servers"],
603     }
604
605 def get_friendica_blocks(domain: str) -> dict:
606     # NOISY-DEBUG: print("DEBUG: Fetching friendica blocks from domain:", domain)
607     blocks = []
608
609     try:
610         doc = BeautifulSoup(
611             reqto.get(f"https://{domain}/friendica", headers=headers, timeout=(config["connection_timeout"], config["read_timeout"])).text,
612             "html.parser",
613         )
614     except baseException as e:
615         print("WARNING: Failed to fetch /friendica from domain:", domain, e)
616         update_last_error(domain, e)
617         return {}
618
619     blocklist = doc.find(id="about_blocklist")
620
621     # Prevents exceptions:
622     if blocklist is None:
623         # NOISY-DEBUG: print("DEBUG: Instance has no block list:", domain)
624         return {}
625
626     for line in blocklist.find("table").find_all("tr")[1:]:
627         blocks.append({
628             "domain": tidyup(line.find_all("td")[0].text),
629             "reason": tidyup(line.find_all("td")[1].text)
630         })
631
632     # NOISY-DEBUG: print("DEBUG: Returning blocks() for domain:", domain, len(blocks))
633     return {
634         "reject": blocks
635     }
636
637 def get_misskey_blocks(domain: str) -> dict:
638     # NOISY-DEBUG: print("DEBUG: Fetching misskey blocks from domain:", domain)
639     blocks = {
640         "suspended": [],
641         "blocked"  : []
642     }
643
644     counter = 0
645     step = 99
646     while True:
647         # iterating through all "suspended" (follow-only in its terminology)
648         # instances page-by-page, since that troonware doesn't support
649         # sending them all at once
650         try:
651             if counter == 0:
652                 # NOISY-DEBUG: print("DEBUG: Sending JSON API request to domain,step,counter:", domain, step, counter)
653                 doc = post_json_api(domain, "/api/federation/instances/", json.dumps({
654                     "sort"     : "+caughtAt",
655                     "host"     : None,
656                     "suspended": True,
657                     "limit"    : step
658                 }))
659             else:
660                 # NOISY-DEBUG: print("DEBUG: Sending JSON API request to domain,step,counter:", domain, step, counter)
661                 doc = post_json_api(domain, "/api/federation/instances/", json.dumps({
662                     "sort"     : "+caughtAt",
663                     "host"     : None,
664                     "suspended": True,
665                     "limit"    : step,
666                     "offset"   : counter-1
667                 }))
668
669             # NOISY-DEBUG: print("DEBUG: doc():", len(doc))
670             if len(doc) == 0:
671                 # NOISY-DEBUG: print("DEBUG: Returned zero bytes, exiting loop:", domain)
672                 break
673
674             for instance in doc:
675                 # just in case
676                 if instance["isSuspended"]:
677                     blocks["suspended"].append(
678                         {
679                             "domain": tidyup(instance["host"]),
680                             # no reason field, nothing
681                             "reason": ""
682                         }
683                     )
684
685             if len(doc) < step:
686                 # NOISY-DEBUG: print("DEBUG: End of request:", len(doc), step)
687                 break
688
689             # NOISY-DEBUG: print("DEBUG: Raising counter by step:", step)
690             counter = counter + step
691
692         except BaseException as e:
693             print("WARNING: Caught error, exiting loop:", domain, e)
694             update_last_error(domain, e)
695             counter = 0
696             break
697
698     while True:
699         # same shit, different asshole ("blocked" aka full suspend)
700         try:
701             if counter == 0:
702                 # NOISY-DEBUG: print("DEBUG: Sending JSON API request to domain,step,counter:", domain, step, counter)
703                 doc = post_json_api(domain,"/api/federation/instances", json.dumps({
704                     "sort"   : "+caughtAt",
705                     "host"   : None,
706                     "blocked": True,
707                     "limit"  : step
708                 }))
709             else:
710                 # NOISY-DEBUG: print("DEBUG: Sending JSON API request to domain,step,counter:", domain, step, counter)
711                 doc = post_json_api(domain,"/api/federation/instances", json.dumps({
712                     "sort"   : "+caughtAt",
713                     "host"   : None,
714                     "blocked": True,
715                     "limit"  : step,
716                     "offset" : counter-1
717                 }))
718
719             # NOISY-DEBUG: print("DEBUG: doc():", len(doc))
720             if len(doc) == 0:
721                 # NOISY-DEBUG: print("DEBUG: Returned zero bytes, exiting loop:", domain)
722                 break
723
724             for instance in doc:
725                 if instance["isBlocked"]:
726                     blocks["blocked"].append({
727                         "domain": tidyup(instance["host"]),
728                         "reason": ""
729                     })
730
731             if len(doc) < step:
732                 # NOISY-DEBUG: print("DEBUG: End of request:", len(doc), step)
733                 break
734
735             # NOISY-DEBUG: print("DEBUG: Raising counter by step:", step)
736             counter = counter + step
737
738         except BaseException as e:
739             print("ERROR: Exception during POST:", domain, e)
740             update_last_error(domain, e)
741             counter = 0
742             break
743
744     # NOISY-DEBUG: print("DEBUG: Returning for domain,blocked(),suspended():", domain, len(blocks["blocked"]), len(blocks["suspended"]))
745     return {
746         "reject"        : blocks["blocked"],
747         "followers_only": blocks["suspended"]
748     }
749
750 def tidyup(string: str) -> str:
751     # some retards put their blocks in variable case
752     string = string.lower().strip()
753
754     # other retards put the port
755     string = re.sub("\:\d+$", "", string)
756
757     # bigger retards put the schema in their blocklist, sometimes even without slashes
758     string = re.sub("^https?\:(\/*)", "", string)
759
760     # and trailing slash
761     string = re.sub("\/$", "", string)
762
763     # and the @
764     string = re.sub("^\@", "", string)
765
766     # the biggest retards of them all try to block individual users
767     string = re.sub("(.+)\@", "", string)
768
769     return string