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