]> 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(
585         f"{config['bot_instance']}/api/v1/statuses",
586         data={
587             "status"      : message,
588             "visibility"  : config['bot_visibility'],
589             "content_type": "text/plain"
590         },
591         headers=botheaders,
592         timeout=10
593     ).json()
594
595     return True
596
597 def get_mastodon_blocks(domain: str) -> dict:
598     # NOISY-DEBUG: print("DEBUG: Fetching mastodon blocks from domain:", domain)
599     blocks = {
600         "Suspended servers": [],
601         "Filtered media"   : [],
602         "Limited servers"  : [],
603         "Silenced servers" : [],
604     }
605
606     translations = {
607         "Silenced instances"            : "Silenced servers",
608         "Suspended instances"           : "Suspended servers",
609         "Gesperrte Server"              : "Suspended servers",
610         "Gefilterte Medien"             : "Filtered media",
611         "Stummgeschaltete Server"       : "Silenced servers",
612         "停止済みのサーバー"            : "Suspended servers",
613         "制限中のサーバー"              : "Limited servers",
614         "メディアを拒否しているサーバー": "Filtered media",
615         "サイレンス済みのサーバー"      : "Silenced servers",
616         "שרתים מושעים"                  : "Suspended servers",
617         "מדיה מסוננת"                   : "Filtered media",
618         "שרתים מוגבלים"                 : "Silenced servers",
619         "Serveurs suspendus"            : "Suspended servers",
620         "Médias filtrés"                : "Filtered media",
621         "Serveurs limités"              : "Limited servers",
622         "Serveurs modérés"              : "Limited servers",
623     }
624
625     try:
626         doc = bs4.BeautifulSoup(
627             reqto.get(f"https://{domain}/about/more", headers=headers, timeout=(config["connection_timeout"], config["read_timeout"])).text,
628             "html.parser",
629         )
630     except BaseException as e:
631         print("ERROR: Cannot fetch from domain:", domain, e)
632         update_last_error(domain, e)
633         return {}
634
635     for header in doc.find_all("h3"):
636         header_text = header.text
637
638         if header_text in translations:
639             header_text = translations[header_text]
640
641         if header_text in blocks or header_text.lower() in blocks:
642             # replaced find_next_siblings with find_all_next to account for instances that e.g. hide lists in dropdown menu
643             for line in header.find_all_next("table")[0].find_all("tr")[1:]:
644                 blocks[header_text].append(
645                     {
646                         "domain": tidyup(line.find("span").text),
647                         "hash"  : tidyup(line.find("span")["title"][9:]),
648                         "reason": tidyup(line.find_all("td")[1].text),
649                     }
650                 )
651
652     # NOISY-DEBUG: print("DEBUG: Returning blocks for domain:", domain)
653     return {
654         "reject"        : blocks["Suspended servers"],
655         "media_removal" : blocks["Filtered media"],
656         "followers_only": blocks["Limited servers"] + blocks["Silenced servers"],
657     }
658
659 def get_friendica_blocks(domain: str) -> dict:
660     # NOISY-DEBUG: print("DEBUG: Fetching friendica blocks from domain:", domain)
661     blocks = []
662
663     try:
664         doc = bs4.BeautifulSoup(
665             reqto.get(f"https://{domain}/friendica", headers=headers, timeout=(config["connection_timeout"], config["read_timeout"])).text,
666             "html.parser",
667         )
668     except BaseException as e:
669         print("WARNING: Failed to fetch /friendica from domain:", domain, e)
670         update_last_error(domain, e)
671         return {}
672
673     blocklist = doc.find(id="about_blocklist")
674
675     # Prevents exceptions:
676     if blocklist is None:
677         # NOISY-DEBUG: print("DEBUG: Instance has no block list:", domain)
678         return {}
679
680     for line in blocklist.find("table").find_all("tr")[1:]:
681         blocks.append({
682             "domain": tidyup(line.find_all("td")[0].text),
683             "reason": tidyup(line.find_all("td")[1].text)
684         })
685
686     # NOISY-DEBUG: print("DEBUG: Returning blocks() for domain:", domain, len(blocks))
687     return {
688         "reject": blocks
689     }
690
691 def get_misskey_blocks(domain: str) -> dict:
692     # NOISY-DEBUG: print("DEBUG: Fetching misskey blocks from domain:", domain)
693     blocks = {
694         "suspended": [],
695         "blocked"  : []
696     }
697
698     counter = 0
699     step = 99
700     while True:
701         # iterating through all "suspended" (follow-only in its terminology)
702         # instances page-by-page, since that troonware doesn't support
703         # sending them all at once
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                     "suspended": 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                     "suspended": 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                 # just in case
730                 if instance["isSuspended"]:
731                     blocks["suspended"].append(
732                         {
733                             "domain": tidyup(instance["host"]),
734                             # no reason field, nothing
735                             "reason": ""
736                         }
737                     )
738
739             if len(doc) < step:
740                 # NOISY-DEBUG: print("DEBUG: End of request:", len(doc), step)
741                 break
742
743             # NOISY-DEBUG: print("DEBUG: Raising counter by step:", step)
744             counter = counter + step
745
746         except BaseException as e:
747             print("WARNING: Caught error, exiting loop:", domain, e)
748             update_last_error(domain, e)
749             counter = 0
750             break
751
752     while True:
753         # same shit, different asshole ("blocked" aka full suspend)
754         try:
755             if counter == 0:
756                 # NOISY-DEBUG: print("DEBUG: Sending JSON API request to domain,step,counter:", domain, step, counter)
757                 doc = post_json_api(domain,"/api/federation/instances", json.dumps({
758                     "sort"   : "+caughtAt",
759                     "host"   : None,
760                     "blocked": True,
761                     "limit"  : step
762                 }))
763             else:
764                 # NOISY-DEBUG: print("DEBUG: Sending JSON API request to domain,step,counter:", domain, step, counter)
765                 doc = post_json_api(domain,"/api/federation/instances", json.dumps({
766                     "sort"   : "+caughtAt",
767                     "host"   : None,
768                     "blocked": True,
769                     "limit"  : step,
770                     "offset" : counter-1
771                 }))
772
773             # NOISY-DEBUG: print("DEBUG: doc():", len(doc))
774             if len(doc) == 0:
775                 # NOISY-DEBUG: print("DEBUG: Returned zero bytes, exiting loop:", domain)
776                 break
777
778             for instance in doc:
779                 if instance["isBlocked"]:
780                     blocks["blocked"].append({
781                         "domain": tidyup(instance["host"]),
782                         "reason": ""
783                     })
784
785             if len(doc) < step:
786                 # NOISY-DEBUG: print("DEBUG: End of request:", len(doc), step)
787                 break
788
789             # NOISY-DEBUG: print("DEBUG: Raising counter by step:", step)
790             counter = counter + step
791
792         except BaseException as e:
793             print("ERROR: Exception during POST:", domain, e)
794             update_last_error(domain, e)
795             counter = 0
796             break
797
798     # NOISY-DEBUG: print("DEBUG: Returning for domain,blocked(),suspended():", domain, len(blocks["blocked"]), len(blocks["suspended"]))
799     return {
800         "reject"        : blocks["blocked"],
801         "followers_only": blocks["suspended"]
802     }
803
804 def tidyup(string: str) -> str:
805     # some retards put their blocks in variable case
806     string = string.lower().strip()
807
808     # other retards put the port
809     string = re.sub("\:\d+$", "", string)
810
811     # bigger retards put the schema in their blocklist, sometimes even without slashes
812     string = re.sub("^https?\:(\/*)", "", string)
813
814     # and trailing slash
815     string = re.sub("\/$", "", string)
816
817     # and the @
818     string = re.sub("^\@", "", string)
819
820     # the biggest retards of them all try to block individual users
821     string = re.sub("(.+)\@", "", string)
822
823     return string