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