]> git.mxchange.org Git - fba.git/blob - fba.py
Continued:
[fba.git] / fba.py
1 # Fedi API Block - An aggregator for fetching blocking data from fediverse nodes
2 # Copyright (C) 2023 Free Software Foundation
3 #
4 # This program is free software: you can redistribute it and/or modify
5 # it under the terms of the GNU Affero General Public License as published
6 # by the Free Software Foundation, either version 3 of the License, or
7 # (at your option) any later version.
8 #
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12 # GNU Affero General Public License for more details.
13 #
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program.  If not, see <https://www.gnu.org/licenses/>.
16
17 import bs4
18 import hashlib
19 import re
20 import reqto
21 import json
22 import sqlite3
23 import sys
24 import time
25 import validators
26
27 with open("config.json") as f:
28     config = json.loads(f.read())
29
30 # Don't check these, known trolls/flooders/testing/developing
31 blacklist = [
32     # Floods network with fake nodes as "research" project
33     "activitypub-troll.cf",
34     # Similar troll
35     "gab.best",
36     # Similar troll
37     "4chan.icu",
38     # Flooder (?)
39     "social.shrimpcam.pw",
40     # Flooder (?)
41     "mastotroll.netz.org",
42     # Testing/developing installations
43     "ngrok.io",
44     "ngrok-free.app",
45     "misskeytest.chn.moe",
46 ]
47
48 # Array with pending errors needed to be written to database
49 pending_errors = {
50 }
51
52 # "rel" identifiers (no real URLs)
53 nodeinfo_identifier = [
54     "https://nodeinfo.diaspora.software/ns/schema/2.1",
55     "https://nodeinfo.diaspora.software/ns/schema/2.0",
56     "https://nodeinfo.diaspora.software/ns/schema/1.1",
57     "https://nodeinfo.diaspora.software/ns/schema/1.0",
58     "http://nodeinfo.diaspora.software/ns/schema/2.1",
59     "http://nodeinfo.diaspora.software/ns/schema/2.0",
60     "http://nodeinfo.diaspora.software/ns/schema/1.1",
61     "http://nodeinfo.diaspora.software/ns/schema/1.0",
62 ]
63
64 # HTTP headers for non-API requests
65 headers = {
66     "User-Agent": config["useragent"],
67 }
68 # HTTP headers for API requests
69 api_headers = {
70     "User-Agent": config["useragent"],
71     "Content-Type": "application/json",
72 }
73
74 # Found info from node, such as nodeinfo URL, detection mode that needs to be
75 # written to database. Both arrays must be filled at the same time or else
76 # update_nodeinfos() will fail
77 nodeinfos = {
78     # Detection mode: 'AUTO_DISCOVERY', 'STATIC_CHECKS' or 'GENERATOR'
79     # NULL means all detection methods have failed (maybe still reachable instance)
80     "detection_mode": {},
81     # Found nodeinfo URL
82     "nodeinfo_url": {},
83 }
84
85 language_mapping = {
86     # English -> English
87     "Silenced instances"            : "Silenced servers",
88     "Suspended instances"           : "Suspended servers",
89     "Limited instances"             : "Limited servers",
90     # Mappuing German -> English
91     "Gesperrte Server"              : "Suspended servers",
92     "Gefilterte Medien"             : "Filtered media",
93     "Stummgeschaltete Server"       : "Silenced servers",
94     # Japanese -> English
95     "停止済みのサーバー"            : "Suspended servers",
96     "制限中のサーバー"              : "Limited servers",
97     "メディアを拒否しているサーバー": "Filtered media",
98     "サイレンス済みのサーバー"      : "Silenced servers",
99     # ??? -> English
100     "שרתים מושעים"                  : "Suspended servers",
101     "מדיה מסוננת"                   : "Filtered media",
102     "שרתים מוגבלים"                 : "Silenced servers",
103     # French -> English
104     "Serveurs suspendus"            : "Suspended servers",
105     "Médias filtrés"                : "Filtered media",
106     "Serveurs limités"              : "Limited servers",
107     "Serveurs modérés"              : "Limited servers",
108 }
109
110 # URL for fetching peers
111 get_peers_url = "/api/v1/instance/peers"
112
113 # Cache for redundant SQL queries
114 cache = {}
115
116 # Connect to database
117 connection = sqlite3.connect("blocks.db")
118 cursor = connection.cursor()
119
120 # Pattern instance for version numbers
121 patterns = [
122     # semantic version number (with v|V) prefix)
123     re.compile("^(?P<version>v|V{0,1})(\.{0,1})(?P<major>0|[1-9]\d*)\.(?P<minor>0+|[1-9]\d*)(\.(?P<patch>0+|[1-9]\d*)(?:-(?P<prerelease>(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+(?P<buildmetadata>[0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?)?$"),
124     # non-sematic, e.g. 1.2.3.4
125     re.compile("^(?P<version>v|V{0,1})(\.{0,1})(?P<major>0|[1-9]\d*)\.(?P<minor>0+|[1-9]\d*)(\.(?P<patch>0+|[1-9]\d*)(\.(?P<subpatch>0|[1-9]\d*))?)$"),
126     # non-sematic, e.g. 2023-05[-dev]
127     re.compile("^(?P<year>[1-9]{1}[0-9]{3})\.(?P<month>[0-9]{2})(-dev){0,1}$"),
128     # non-semantic, e.g. abcdef0
129     re.compile("^[a-f0-9]{7}$"),
130 ]
131
132 ##### Cache #####
133
134 def is_cache_initialized(key: str) -> bool:
135     return key in cache
136
137 def set_all_cache_key(key: str, rows: list, value: any):
138     # DEBUG: print(f"DEBUG: key='{key}',rows()={len(rows)},value[]={type(value)} - CALLED!")
139     if not is_cache_initialized(key):
140         # DEBUG: print(f"DEBUG: Cache for key='{key}' not initialized.")
141         cache[key] = {}
142
143     for sub in rows:
144         # DEBUG: print(f"DEBUG: Setting key='{key}',sub[{type(sub)}]='{sub}'")
145
146         if isinstance(sub, tuple):
147             cache[key][sub[0]] = value
148         else:
149             print(f"WARNING: Unsupported type row[]='{type(row)}'")
150
151     # DEBUG: print("DEBUG: EXIT!")
152
153 def set_cache_key(key: str, sub: str, value: any):
154     if not is_cache_initialized(key):
155         print(f"WARNING: Bad method call, key='{key}' is not initialized yet.")
156         raise Exception(f"Cache for key='{key}' is not initialized, but function called")
157
158     cache[key][sub] = value
159
160 def is_cache_key_set(key: str, sub: str) -> bool:
161     if not is_cache_initialized(key):
162         print(f"WARNING: Bad method call, key='{key}' is not initialized yet.")
163         raise Exception(f"Cache for key='{key}' is not initialized, but function called")
164
165     return sub in cache[key]
166
167 def add_peers(rows: dict) -> list:
168     # DEBUG: print(f"DEBUG: rows()={len(rows)} - CALLED!")
169     peers = list()
170     for element in ["linked", "allowed", "blocked"]:
171         # DEBUG: print(f"DEBUG: Checking element='{element}'")
172         if element in rows and rows[element] != None:
173             # DEBUG: print(f"DEBUG: Adding {len(rows[element])} peer(s) to peers list ...")
174             for peer in rows[element]:
175                 # DEBUG: print(f"DEBUG: peer='{peer}' - BEFORE!")
176                 peer = tidyup(peer)
177
178                 # DEBUG: print(f"DEBUG: peer='{peer}' - AFTER!")
179                 if is_blacklisted(peer):
180                     # DEBUG: print(f"DEBUG: peer='{peer}' is blacklisted, skipped!")
181                     continue
182
183                 # DEBUG: print(f"DEBUG: Adding peer='{peer}' ...")
184                 peers.append(peer)
185
186     # DEBUG: print(f"DEBUG: peers()={len(peers)} - EXIT!")
187     return peers
188
189 def remove_version(software: str) -> str:
190     # DEBUG: print(f"DEBUG: software='{software}' - CALLED!")
191     if not "." in software and " " not in software:
192         print(f"WARNING: software='{software}' does not contain a version number.")
193         return software
194
195     temp = software
196     if ";" in software:
197         temp = software.split(";")[0]
198     elif "," in software:
199         temp = software.split(",")[0]
200     elif " - " in software:
201         temp = software.split(" - ")[0]
202
203     # DEBUG: print(f"DEBUG: software='{software}'")
204     version = None
205     if " " in software:
206         version = temp.split(" ")[-1]
207     elif "/" in software:
208         version = temp.split("/")[-1]
209     elif "-" in software:
210         version = temp.split("-")[-1]
211     else:
212         # DEBUG: print(f"DEBUG: Was not able to find common seperator, returning untouched software='{software}'")
213         return software
214
215     matches = None
216     match = None
217     # DEBUG: print(f"DEBUG: Checking {len(patterns)} patterns ...")
218     for pattern in patterns:
219         # Run match()
220         match = pattern.match(version)
221
222         # DEBUG: print(f"DEBUG: match[]={type(match)}")
223         if type(match) is re.Match:
224             break
225
226     # DEBUG: print(f"DEBUG: version[{type(version)}]='{version}',match='{match}'")
227     if type(match) is not re.Match:
228         print(f"WARNING: version='{version}' does not match regex, leaving software='{software}' untouched.")
229         return software
230
231     # DEBUG: print(f"DEBUG: Found valid version number: '{version}', removing it ...")
232     end = len(temp) - len(version) - 1
233
234     # DEBUG: print(f"DEBUG: end[{type(end)}]={end}")
235     software = temp[0:end].strip()
236     if " version" in software:
237         # DEBUG: print(f"DEBUG: software='{software}' contains word ' version'")
238         software = strip_until(software, " version")
239
240     # DEBUG: print(f"DEBUG: software='{software}' - EXIT!")
241     return software
242
243 def strip_powered_by(software: str) -> str:
244     # DEBUG: print(f"DEBUG: software='{software}' - CALLED!")
245     if software == "":
246         print(f"ERROR: Bad method call, 'software' is empty")
247         raise Exception("Parameter 'software' is empty")
248     elif not "powered by" in software:
249         print(f"WARNING: Cannot find 'powered by' in '{software}'!")
250         return software
251
252     start = software.find("powered by ")
253     # DEBUG: print(f"DEBUG: start[{type(start)}]='{start}'")
254
255     software = software[start + 11:].strip()
256     # DEBUG: print(f"DEBUG: software='{software}'")
257
258     software = strip_until(software, " - ")
259
260     # DEBUG: print(f"DEBUG: software='{software}' - EXIT!")
261     return software
262
263 def strip_until(software: str, until: str) -> str:
264     # DEBUG: print(f"DEBUG: software='{software}',until='{until}' - CALLED!")
265     if software == "":
266         print(f"ERROR: Bad method call, 'software' is empty")
267         raise Exception("Parameter 'software' is empty")
268     elif until == "":
269         print(f"ERROR: Bad method call, 'until' is empty")
270         raise Exception("Parameter 'until' is empty")
271     elif not until in software:
272         print(f"WARNING: Cannot find 'powered by' in '{software}'!")
273         return software
274
275     # Next, strip until part
276     end = software.find(until)
277
278     # DEBUG: print(f"DEBUG: end[{type(end)}]='{end}'")
279     if end > 0:
280         software = software[0:end].strip()
281
282     # DEBUG: print(f"DEBUG: software='{software}' - EXIT!")
283     return software
284
285 def is_blacklisted(domain: str) -> bool:
286     blacklisted = False
287     for peer in blacklist:
288         if peer in domain:
289             blacklisted = True
290
291     return blacklisted
292
293 def remove_pending_error(domain: str):
294     try:
295         # Prevent updating any pending errors, nodeinfo was found
296         del pending_errors[domain]
297
298     except:
299         pass
300
301 def get_hash(domain: str) -> str:
302     return hashlib.sha256(domain.encode("utf-8")).hexdigest()
303
304 def update_last_blocked(domain: str):
305     # DEBUG: print("DEBUG: Updating last_blocked for domain", domain)
306     try:
307         cursor.execute("UPDATE instances SET last_blocked = ?, last_updated = ? WHERE domain = ? LIMIT 1", [
308             time.time(),
309             time.time(),
310             domain
311         ])
312
313         if cursor.rowcount == 0:
314             print("WARNING: Did not update any rows:", domain)
315
316     except BaseException as e:
317         print(f"ERROR: failed SQL query: domain='{domain}',exception[{type(e)}]:'{str(e)}'")
318         sys.exit(255)
319
320     # DEBUG: print("DEBUG: EXIT!")
321
322 def has_pending_nodeinfos(domain: str) -> bool:
323     # DEBUG: print(f"DEBUG: domain='{domain}' - CALLED!")
324     has_pending = False
325     for key in nodeinfos:
326         # DEBUG: print(f"DEBUG: key='{key}',domain='{domain}',nodeinfos[key]='{nodeinfos[key]}'")
327         if domain in nodeinfos[key]:
328             has_pending = True
329             break
330
331     # DEBUG: print(f"DEBUG: has_pending='{has_pending}' - EXIT!")
332     return has_pending
333
334 def update_nodeinfos(domain: str):
335     # DEBUG: print("DEBUG: Updating nodeinfo for domain:", domain)
336     sql_string = ''
337     fields = list()
338     for key in nodeinfos:
339         # DEBUG: print("DEBUG: key:", key)
340         if domain in nodeinfos[key]:
341            # DEBUG: print(f"DEBUG: Adding '{nodeinfos[key][domain]}' for key='{key}' ...")
342            fields.append(nodeinfos[key][domain])
343            sql_string += f" {key} = ?,"
344
345     fields.append(domain)
346     # DEBUG: print(f"DEBUG: sql_string='{sql_string}',fields()={len(fields)}")
347
348     sql = "UPDATE instances SET" + sql_string + " last_status_code = NULL, last_error_details = NULL WHERE domain = ? LIMIT 1"
349     # DEBUG: print("DEBUG: sql:", sql)
350
351     try:
352         # DEBUG: print("DEBUG: Executing SQL:", sql)
353         cursor.execute(sql, fields)
354         # DEBUG: print(f"DEBUG: Success! (rowcount={cursor.rowcount })")
355
356         if cursor.rowcount == 0:
357             print("WARNING: Did not update any rows:", domain)
358
359     except BaseException as e:
360         print(f"ERROR: failed SQL query: domain='{domain}',sql='{sql}',exception[{type(e)}]:'{str(e)}'")
361         sys.exit(255)
362
363     # DEBUG: print("DEBUG: Deleting nodeinfos for domain:", domain)
364     for key in nodeinfos:
365         try:
366             # DEBUG: print("DEBUG: Deleting key:", key)
367             del nodeinfos[key][domain]
368         except:
369             pass
370
371     # DEBUG: print("DEBUG: EXIT!")
372
373 def log_error(domain: str, res: any):
374     # DEBUG: print("DEBUG: domain,res[]:", domain, type(res))
375     try:
376         # DEBUG: print("DEBUG: BEFORE res[]:", type(res))
377         if isinstance(res, BaseException) or isinstance(res, json.JSONDecodeError):
378             res = str(res)
379
380         # DEBUG: print("DEBUG: AFTER res[]:", type(res))
381         if type(res) is str:
382             cursor.execute("INSERT INTO error_log (domain, error_code, error_message, created) VALUES (?, 999, ?, ?)",[
383                 domain,
384                 res,
385                 time.time()
386             ])
387         else:
388             cursor.execute("INSERT INTO error_log (domain, error_code, error_message, created) VALUES (?, ?, ?, ?)",[
389                 domain,
390                 res.status_code,
391                 res.reason,
392                 time.time()
393             ])
394
395         # Cleanup old entries
396         # DEBUG: print(f"DEBUG: Purging old records (distance: {config['error_log_cleanup'])")
397         cursor.execute("DELETE FROM error_log WHERE created < ?", [time.time() - config["error_log_cleanup"]])
398     except BaseException as e:
399         print(f"ERROR: failed SQL query: domain='{domain}',exception[{type(e)}]:'{str(e)}'")
400         sys.exit(255)
401
402     # DEBUG: print("DEBUG: EXIT!")
403
404 def update_last_error(domain: str, res: any):
405     # DEBUG: print("DEBUG: domain,res[]:", domain, type(res))
406     try:
407         # DEBUG: print("DEBUG: BEFORE res[]:", type(res))
408         if isinstance(res, BaseException) or isinstance(res, json.JSONDecodeError):
409             res = str(res)
410
411         # DEBUG: print("DEBUG: AFTER res[]:", type(res))
412         if type(res) is str:
413             # DEBUG: print(f"DEBUG: Setting last_error_details='{res}'");
414             cursor.execute("UPDATE instances SET last_status_code = 999, last_error_details = ?, last_updated = ? WHERE domain = ? LIMIT 1", [
415                 res,
416                 time.time(),
417                 domain
418             ])
419         else:
420             # DEBUG: print(f"DEBUG: Setting last_error_details='{res.reason}'");
421             cursor.execute("UPDATE instances SET last_status_code = ?, last_error_details = ?, last_updated = ? WHERE domain = ? LIMIT 1", [
422                 res.status_code,
423                 res.reason,
424                 time.time(),
425                 domain
426             ])
427
428         if cursor.rowcount == 0:
429             # DEBUG: print("DEBUG: Did not update any rows:", domain)
430             pending_errors[domain] = res
431
432         log_error(domain, res)
433
434     except BaseException as e:
435         print(f"ERROR: failed SQL query: domain='{domain}',exception[{type(e)}]:'{str(e)}'")
436         sys.exit(255)
437
438     # DEBUG: print("DEBUG: EXIT!")
439
440 def update_last_instance_fetch(domain: str):
441     #print("DEBUG: Updating last_instance_fetch for domain:", domain)
442     try:
443         cursor.execute("UPDATE instances SET last_instance_fetch = ?, last_updated = ? WHERE domain = ? LIMIT 1", [
444             time.time(),
445             time.time(),
446             domain
447         ])
448
449         if cursor.rowcount == 0:
450             print("WARNING: Did not update any rows:", domain)
451
452     except BaseException as e:
453         print(f"ERROR: failed SQL query: domain='{domain}',exception[{type(e)}]:'{str(e)}'")
454         sys.exit(255)
455
456     connection.commit()
457     #print("DEBUG: EXIT!")
458
459 def update_last_nodeinfo(domain: str):
460     # DEBUG: print("DEBUG: Updating last_nodeinfo for domain:", domain)
461     try:
462         cursor.execute("UPDATE instances SET last_nodeinfo = ?, last_updated = ? WHERE domain = ? LIMIT 1", [
463             time.time(),
464             time.time(),
465             domain
466         ])
467
468         if cursor.rowcount == 0:
469             print("WARNING: Did not update any rows:", domain)
470
471     except BaseException as e:
472         print(f"ERROR: failed SQL query: domain='{domain}',exception[{type(e)}]:'{str(e)}'")
473         sys.exit(255)
474
475     connection.commit()
476     # DEBUG: print("DEBUG: EXIT!")
477
478 def get_peers(domain: str, software: str) -> list:
479     # DEBUG: print(f"DEBUG: domain='{domain}',software='{software}' - CALLED!")
480     peers = list()
481
482     if software == "misskey":
483         # DEBUG: print(f"DEBUG: domain='{domain}' is misskey, sending API POST request ...")
484         offset = 0
485         step = config["misskey_offset"]
486
487         # iterating through all "suspended" (follow-only in its terminology)
488         # instances page-by-page, since that troonware doesn't support
489         # sending them all at once
490         while True:
491             # DEBUG: print(f"DEBUG: Fetching offset='{offset}' from '{domain}' ...")
492             if offset == 0:
493                 fetched = post_json_api(domain, "/api/federation/instances", json.dumps({
494                     "sort" : "+pubAt",
495                     "host" : None,
496                     "limit": step
497                 }), {"Origin": domain})
498             else:
499                 fetched = post_json_api(domain, "/api/federation/instances", json.dumps({
500                     "sort"  : "+pubAt",
501                     "host"  : None,
502                     "limit" : step,
503                     "offset": offset - 1
504                 }), {"Origin": domain})
505
506             # DEBUG: print("DEBUG: fetched():", len(fetched))
507             if len(fetched) == 0:
508                 # DEBUG: print("DEBUG: Returned zero bytes, exiting loop:", domain)
509                 break
510             elif len(fetched) != config["misskey_offset"]:
511                 # DEBUG: print(f"DEBUG: Fetched '{len(fetched)}' row(s) but expected: '{config['misskey_offset']}'")
512                 offset = offset + (config["misskey_offset"] - len(fetched))
513             else:
514                 # DEBUG: print("DEBUG: Raising offset by step:", step)
515                 offset = offset + step
516
517             # Check records
518             # DEBUG: print(f"DEBUG: fetched({len(fetched)})[]={type(fetched)}")
519             if isinstance(fetched, dict) and "error" in fetched and "message" in fetched["error"]:
520                 print(f"WARNING: post_json_api() returned error: {fetched['error']['message']}")
521                 update_last_error(domain, fetched["error"]["message"])
522                 break
523             for row in fetched:
524                 # DEBUG: print(f"DEBUG: row():{len(row)}")
525                 if not "host" in row:
526                     print(f"WARNING: row()={len(row)} does not contain element 'host': {row},domain='{domain}'")
527                     continue
528                 elif is_blacklisted(row["host"]):
529                     # DEBUG: print(f"DEBUG: row[host]='{row['host']}' is blacklisted. domain='{domain}'")
530                     continue
531
532                 # DEBUG: print(f"DEBUG: Adding peer: '{row['host']}'")
533                 peers.append(row["host"])
534
535         update_last_instance_fetch(domain)
536
537         # DEBUG: print("DEBUG: Returning peers[]:", type(peers))
538         return peers
539     elif software == "lemmy":
540         # DEBUG: print(f"DEBUG: domain='{domain}' is Lemmy, fetching JSON ...")
541         try:
542             res = reqto.get(f"https://{domain}/api/v3/site", headers=api_headers, timeout=(config["connection_timeout"], config["read_timeout"]))
543
544             data = res.json()
545             # DEBUG: print(f"DEBUG: res.ok={res.ok},res.status_code='{res.status_code}',data[]='{type(data)}'")
546             if not res.ok or res.status_code >= 400:
547                 print("WARNING: Could not reach any JSON API:", domain)
548                 update_last_error(domain, res)
549             elif res.ok and isinstance(data, list):
550                 # DEBUG: print(f"DEBUG: domain='{domain}' returned a list: '{data}'")
551                 sys.exit(255)
552             elif "federated_instances" in data:
553                 # DEBUG: print(f"DEBUG: Found federated_instances for domain='{domain}'")
554                 peers = peers + add_peers(data["federated_instances"])
555                 # DEBUG: print("DEBUG: Added instance(s) to peers")
556             else:
557                 print("WARNING: JSON response does not contain 'federated_instances':", domain)
558                 update_last_error(domain, res)
559
560         except BaseException as e:
561             print(f"WARNING: Exception during fetching JSON: domain='{domain}',exception[{type(e)}]:'{str(e)}'")
562
563         update_last_instance_fetch(domain)
564
565         # DEBUG: print("DEBUG: Returning peers[]:", type(peers))
566         return peers
567     elif software == "peertube":
568         # DEBUG: print(f"DEBUG: domain='{domain}' is a PeerTube, fetching JSON ...")
569
570         start = 0
571         for mode in ["followers", "following"]:
572             # DEBUG: print(f"DEBUG: domain='{domain}',mode='{mode}'")
573             while True:
574                 try:
575                     res = reqto.get(f"https://{domain}/api/v1/server/{mode}?start={start}&count=100", headers=api_headers, timeout=(config["connection_timeout"], config["read_timeout"]))
576
577                     data = res.json()
578                     # DEBUG: print(f"DEBUG: res.ok={res.ok},res.status_code='{res.status_code}',data[]='{type(data)}'")
579                     if res.ok and isinstance(data, dict):
580                         # DEBUG: print("DEBUG: Success, data:", len(data))
581                         if "data" in data:
582                             # DEBUG: print(f"DEBUG: Found {len(data['data'])} record(s).")
583                             for record in data["data"]:
584                                 # DEBUG: print(f"DEBUG: record()={len(record)}")
585                                 if mode in record and "host" in record[mode]:
586                                     # DEBUG: print(f"DEBUG: Found host={record[mode]['host']}, adding ...")
587                                     peers.append(record[mode]["host"])
588                                 else:
589                                     print(f"WARNING: record from '{domain}' has no '{mode}' or 'host' record: {record}")
590
591                             if len(data["data"]) < 100:
592                                 # DEBUG: print("DEBUG: Reached end of JSON response:", domain)
593                                 break
594
595                         # Continue with next row
596                         start = start + 100
597
598                 except BaseException as e:
599                     print(f"WARNING: Exception during fetching JSON: domain='{domain}',exception[{type(e)}]:'{str(e)}'")
600
601             update_last_instance_fetch(domain)
602
603             # DEBUG: print("DEBUG: Returning peers[]:", type(peers))
604             return peers
605
606     # DEBUG: print(f"DEBUG: Fetching get_peers_url='{get_peers_url}' from '{domain}' ...")
607     try:
608         res = reqto.get(f"https://{domain}{get_peers_url}", headers=api_headers, timeout=(config["connection_timeout"], config["read_timeout"]))
609
610         data = res.json()
611         # DEBUG: print(f"DEBUG: res.ok={res.ok},res.status_code={res.status_code},data[]='{type(data)}'")
612         if not res.ok or res.status_code >= 400:
613             # DEBUG: print(f"DEBUG: Was not able to fetch '{get_peers_url}', trying alternative ...")
614             res = reqto.get(f"https://{domain}/api/v3/site", headers=api_headers, timeout=(config["connection_timeout"], config["read_timeout"]))
615
616             data = res.json()
617             # DEBUG: print(f"DEBUG: res.ok={res.ok},res.status_code={res.status_code},data[]='{type(data)}'")
618             if not res.ok or res.status_code >= 400:
619                 print("WARNING: Could not reach any JSON API:", domain)
620                 update_last_error(domain, res)
621             elif res.ok and isinstance(data, list):
622                 print(f"DEBUG: domain='{domain}' returned a list: '{data}'")
623                 sys.exit(255)
624             elif "federated_instances" in data:
625                 # DEBUG: print(f"DEBUG: Found federated_instances for domain='{domain}'")
626                 peers = peers + add_peers(data["federated_instances"])
627                 # DEBUG: print("DEBUG: Added instance(s) to peers")
628             else:
629                 print("WARNING: JSON response does not contain 'federated_instances':", domain)
630                 update_last_error(domain, res)
631         else:
632             # DEBUG: print("DEBUG: Querying API was successful:", domain, len(data))
633             peers = data
634
635     except BaseException as e:
636         print("WARNING: Some error during get():", domain, e)
637         update_last_error(domain, e)
638
639     update_last_instance_fetch(domain)
640
641     # DEBUG: print("DEBUG: Returning peers[]:", type(peers))
642     return peers
643
644 def post_json_api(domain: str, path: str, parameter: str, extra_headers: dict = {}) -> dict:
645     # DEBUG: print("DEBUG: Sending POST to domain,path,parameter:", domain, path, parameter, extra_headers)
646     data = {}
647     try:
648         res = reqto.post(f"https://{domain}{path}", data=parameter, headers={**api_headers, **extra_headers}, timeout=(config["connection_timeout"], config["read_timeout"]))
649
650         data = res.json()
651         # DEBUG: print(f"DEBUG: res.ok={res.ok},res.status_code={res.status_code},data[]='{type(data)}'")
652         if not res.ok or res.status_code >= 400:
653             print(f"WARNING: Cannot query JSON API: domain='{domain}',path='{path}',parameter()={len(parameter)},res.status_code='{res.status_code}',data[]='{type(data)}'")
654             update_last_error(domain, res)
655         else:
656             update_last_nodeinfo(domain)
657
658     except BaseException as e:
659         print(f"WARNING: Some error during post(): domain='{domain}',path='{path}',parameter()={len(parameter)},exception[{type(e)}]:'{str(e)}'")
660
661     # DEBUG: print(f"DEBUG: Returning data({len(data)})=[]:{type(data)}")
662     return data
663
664 def fetch_nodeinfo(domain: str, path: str = None) -> list:
665     # DEBUG: print("DEBUG: Fetching nodeinfo from domain,path:", domain, path)
666
667     nodeinfo = fetch_wellknown_nodeinfo(domain)
668     # DEBUG: print("DEBUG: nodeinfo:", len(nodeinfo))
669
670     if len(nodeinfo) > 0:
671         # DEBUG: print("DEBUG: Returning auto-discovered nodeinfo:", len(nodeinfo))
672         return nodeinfo
673
674     requests = [
675        f"https://{domain}/nodeinfo/2.1.json",
676        f"https://{domain}/nodeinfo/2.1",
677        f"https://{domain}/nodeinfo/2.0.json",
678        f"https://{domain}/nodeinfo/2.0",
679        f"https://{domain}/nodeinfo/1.0",
680        f"https://{domain}/api/v1/instance"
681     ]
682
683     data = {}
684     for request in requests:
685         if path != None and path != "" and request != path:
686             print(f"DEBUG: path='{path}' does not match request='{request}' - SKIPPED!")
687             continue
688
689         try:
690             # DEBUG: print("DEBUG: Fetching request:", request)
691             res = reqto.get(request, headers=api_headers, timeout=(config["connection_timeout"], config["read_timeout"]))
692
693             data = res.json()
694             # DEBUG: print(f"DEBUG: res.ok={res.ok},res.status_code={res.status_code},data[]='{type(data)}'")
695             if res.ok and isinstance(data, dict):
696                 # DEBUG: print("DEBUG: Success:", request)
697                 nodeinfos["detection_mode"][domain] = "STATIC_CHECK"
698                 nodeinfos["nodeinfo_url"][domain] = request
699                 break
700             elif res.ok and isinstance(data, list):
701                 # DEBUG: print(f"DEBUG: domain='{domain}' returned a list: '{data}'")
702                 sys.exit(255)
703             elif not res.ok or res.status_code >= 400:
704                 print("WARNING: Failed fetching nodeinfo from domain:", domain)
705                 update_last_error(domain, res)
706                 continue
707
708         except BaseException as e:
709             # DEBUG: print("DEBUG: Cannot fetch API request:", request)
710             update_last_error(domain, e)
711             pass
712
713     # DEBUG: print("DEBUG: Returning data[]:", type(data))
714     return data
715
716 def fetch_wellknown_nodeinfo(domain: str) -> list:
717     # DEBUG: print("DEBUG: Fetching .well-known info for domain:", domain)
718     data = {}
719
720     try:
721         res = reqto.get(f"https://{domain}/.well-known/nodeinfo", headers=api_headers, timeout=(config["connection_timeout"], config["read_timeout"]))
722
723         data = res.json()
724         # DEBUG: print("DEBUG: domain,res.ok,data[]:", domain, res.ok, type(data))
725         if res.ok and isinstance(data, dict):
726             nodeinfo = data
727             # DEBUG: print("DEBUG: Found entries:", len(nodeinfo), domain)
728             if "links" in nodeinfo:
729                 # DEBUG: print("DEBUG: Found links in nodeinfo():", len(nodeinfo["links"]))
730                 for link in nodeinfo["links"]:
731                     # DEBUG: print("DEBUG: rel,href:", link["rel"], link["href"])
732                     if link["rel"] in nodeinfo_identifier:
733                         # DEBUG: print("DEBUG: Fetching nodeinfo from:", link["href"])
734                         res = reqto.get(link["href"])
735
736                         data = res.json()
737                         # DEBUG: print("DEBUG: href,res.ok,res.status_code:", link["href"], res.ok, res.status_code)
738                         if res.ok and isinstance(data, dict):
739                             # DEBUG: print("DEBUG: Found JSON nodeinfo():", len(data))
740                             nodeinfos["detection_mode"][domain] = "AUTO_DISCOVERY"
741                             nodeinfos["nodeinfo_url"][domain] = link["href"]
742                             break
743                     else:
744                         print("WARNING: Unknown 'rel' value:", domain, link["rel"])
745             else:
746                 print("WARNING: nodeinfo does not contain 'links':", domain)
747
748     except BaseException as e:
749         print("WARNING: Failed fetching .well-known info:", domain)
750         update_last_error(domain, e)
751         pass
752
753     # DEBUG: print("DEBUG: Returning data[]:", type(data))
754     return data
755
756 def fetch_generator_from_path(domain: str, path: str = "/") -> str:
757     # DEBUG: print(f"DEBUG: domain='{domain}',path='{path}' - CALLED!")
758     software = None
759
760     try:
761         # DEBUG: print(f"DEBUG: Fetching path='{path}' from '{domain}' ...")
762         res = reqto.get(f"https://{domain}{path}", headers=headers, timeout=(config["connection_timeout"], config["read_timeout"]))
763
764         # DEBUG: print("DEBUG: domain,res.ok,res.status_code,res.text[]:", domain, res.ok, res.status_code, type(res.text))
765         if res.ok and res.status_code < 300 and len(res.text) > 0:
766             # DEBUG: print("DEBUG: Search for <meta name='generator'>:", domain)
767             doc = bs4.BeautifulSoup(res.text, "html.parser")
768
769             # DEBUG: print("DEBUG: doc[]:", type(doc))
770             tag = doc.find("meta", {"name": "generator"})
771
772             # DEBUG: print(f"DEBUG: tag[{type(tag)}: {tag}")
773             if isinstance(tag, bs4.element.Tag):
774                 # DEBUG: print("DEBUG: Found generator meta tag: ", domain)
775                 software = tidyup(tag.get("content"))
776                 print(f"INFO: domain='{domain}' is generated by '{software}'")
777                 nodeinfos["detection_mode"][domain] = "GENERATOR"
778                 remove_pending_error(domain)
779
780     except BaseException as e:
781         # DEBUG: print(f"DEBUG: Cannot fetch / from '{domain}':", e)
782         update_last_error(domain, e)
783         pass
784
785     # DEBUG: print(f"DEBUG: software[]={type(software)}")
786     if type(software) is str and software == "":
787         # DEBUG: print(f"DEBUG: Corrected empty string to None for software of domain='{domain}'")
788         software = None
789     elif type(software) is str and ("." in software or " " in software):
790         # DEBUG: print(f"DEBUG: software='{software}' may contain a version number, domain='{domain}', removing it ...")
791         software = remove_version(software)
792
793     # DEBUG: print(f"DEBUG: software[]={type(software)}")
794     if type(software) is str and "powered by" in software:
795         # DEBUG: print(f"DEBUG: software='{software}' has 'powered by' in it")
796         software = remove_version(strip_powered_by(software))
797     elif type(software) is str and " by " in software:
798         # DEBUG: print(f"DEBUG: software='{software}' has ' by ' in it")
799         software = strip_until(software, " by ")
800     elif type(software) is str and " see " in software:
801         # DEBUG: print(f"DEBUG: software='{software}' has ' see ' in it")
802         software = strip_until(software, " see ")
803
804     # DEBUG: print(f"DEBUG: software='{software}' - EXIT!")
805     return software
806
807 def determine_software(domain: str, path: str = None) -> str:
808     # DEBUG: print("DEBUG: Determining software for domain,path:", domain, path)
809     software = None
810
811     # DEBUG: print(f"DEBUG: Fetching nodeinfo from '{domain}' ...")
812     data = fetch_nodeinfo(domain, path)
813
814     # DEBUG: print("DEBUG: data[]:", type(data))
815     if not isinstance(data, dict) or len(data) == 0:
816         # DEBUG: print("DEBUG: Could not determine software type:", domain)
817         return fetch_generator_from_path(domain)
818
819     # DEBUG: print("DEBUG: data():", len(data), data)
820     if "status" in data and data["status"] == "error" and "message" in data:
821         print("WARNING: JSON response is an error:", data["message"])
822         update_last_error(domain, data["message"])
823         return fetch_generator_from_path(domain)
824     elif "software" not in data or "name" not in data["software"]:
825         # DEBUG: print(f"DEBUG: JSON response from {domain} does not include [software][name], fetching / ...")
826         software = fetch_generator_from_path(domain)
827
828         # DEBUG: print(f"DEBUG: Generator for domain='{domain}' is: {software}, EXIT!")
829         return software
830
831     software = tidyup(data["software"]["name"])
832
833     # DEBUG: print("DEBUG: sofware after tidyup():", software)
834     if software in ["akkoma", "rebased"]:
835         # DEBUG: print("DEBUG: Setting pleroma:", domain, software)
836         software = "pleroma"
837     elif software in ["hometown", "ecko"]:
838         # DEBUG: print("DEBUG: Setting mastodon:", domain, software)
839         software = "mastodon"
840     elif software in ["calckey", "groundpolis", "foundkey", "cherrypick", "meisskey"]:
841         # DEBUG: print("DEBUG: Setting misskey:", domain, software)
842         software = "misskey"
843     elif software.find("/") > 0:
844         print("WARNING: Spliting of slash:", software)
845         software = software.split("/")[-1];
846     elif software.find("|") > 0:
847         print("WARNING: Spliting of pipe:", software)
848         software = tidyup(software.split("|")[0]);
849     elif "powered by" in software:
850         # DEBUG: print(f"DEBUG: software='{software}' has 'powered by' in it")
851         software = strip_powered_by(software)
852     elif type(software) is str and " by " in software:
853         # DEBUG: print(f"DEBUG: software='{software}' has ' by ' in it")
854         software = strip_until(software, " by ")
855     elif type(software) is str and " see " in software:
856         # DEBUG: print(f"DEBUG: software='{software}' has ' see ' in it")
857         software = strip_until(software, " see ")
858
859     # DEBUG: print(f"DEBUG: software[]={type(software)}")
860     if software == "":
861         print("WARNING: tidyup() left no software name behind:", domain)
862         software = None
863
864     # DEBUG: print(f"DEBUG: software[]={type(software)}")
865     if str(software) == "":
866         # DEBUG: print(f"DEBUG: software for '{domain}' was not detected, trying generator ...")
867         software = fetch_generator_from_path(domain)
868     elif len(str(software)) > 0 and ("." in software or " " in software):
869         # DEBUG: print(f"DEBUG: software='{software}' may contain a version number, domain='{domain}', removing it ...")
870         software = remove_version(software)
871
872     # DEBUG: print(f"DEBUG: software[]={type(software)}")
873     if type(software) is str and "powered by" in software:
874         # DEBUG: print(f"DEBUG: software='{software}' has 'powered by' in it")
875         software = remove_version(strip_powered_by(software))
876
877     # DEBUG: print("DEBUG: Returning domain,software:", domain, software)
878     return software
879
880 def update_block_reason(reason: str, blocker: str, blocked: str, block_level: str):
881     # DEBUG: print("DEBUG: Updating block reason:", reason, blocker, blocked, block_level)
882     try:
883         cursor.execute(
884             "UPDATE blocks SET reason = ?, last_seen = ? WHERE blocker = ? AND blocked = ? AND block_level = ? AND reason = ''",
885             (
886                 reason,
887                 time.time(),
888                 blocker,
889                 blocked,
890                 block_level
891             ),
892         )
893
894         # DEBUG: print(f"DEBUG: cursor.rowcount={cursor.rowcount}")
895         if cursor.rowcount == 0:
896             print("WARNING: Did not update any rows:", domain)
897
898     except BaseException as e:
899         print(f"ERROR: failed SQL query: reason='{reason}',blocker='{blocker}',blocked='{blocked}',block_level='{block_level}',sql='{sql}',exception[{type(e)}]:'{str(e)}'")
900         sys.exit(255)
901
902     # DEBUG: print("DEBUG: EXIT!")
903
904 def update_last_seen(blocker: str, blocked: str, block_level: str):
905     # DEBUG: print("DEBUG: Updating last_seen for:", blocker, blocked, block_level)
906     try:
907         cursor.execute(
908             "UPDATE blocks SET last_seen = ? WHERE blocker = ? AND blocked = ? AND block_level = ?",
909             (
910                 time.time(),
911                 blocker,
912                 blocked,
913                 block_level
914             )
915         )
916
917         if cursor.rowcount == 0:
918             print("WARNING: Did not update any rows:", domain)
919
920     except BaseException as e:
921         print(f"ERROR: failed SQL query: last_seen='{last_seen}',blocker='{blocker}',blocked='{blocked}',block_level='{block_level}',exception[{type(e)}]:'{str(e)}'")
922         sys.exit(255)
923
924     # DEBUG: print("DEBUG: EXIT!")
925
926 def block_instance(blocker: str, blocked: str, reason: str, block_level: str):
927     # DEBUG: print("DEBUG: blocker,blocked,reason,block_level:", blocker, blocked, reason, block_level)
928     if not validators.domain(blocker.split("/")[0]):
929         print("WARNING: Bad blocker:", blocker)
930         raise
931     elif not validators.domain(blocked.split("/")[0]):
932         print("WARNING: Bad blocked:", blocked)
933         raise
934
935     print("INFO: New block:", blocker, blocked, reason, block_level, first_seen, last_seen)
936     try:
937         cursor.execute(
938             "INSERT INTO blocks (blocker, blocked, reason, block_level, first_seen, last_seen) VALUES(?, ?, ?, ?, ?, ?)",
939              (
940                  blocker,
941                  blocked,
942                  reason,
943                  block_level,
944                  time.time(),
945                  time.time()
946              ),
947         )
948
949     except BaseException as e:
950         print(f"ERROR: failed SQL query: blocker='{blocker}',blocked='{blocked}',reason='{reason}',block_level='{block_level}',first_seen='{first_seen}',last_seen='{last_seen}',exception[{type(e)}]:'{str(e)}'")
951         sys.exit(255)
952
953     # DEBUG: print("DEBUG: EXIT!")
954
955 def is_instance_registered(domain: str) -> bool:
956     # DEBUG: print(f"DEBUG: domain='{domain}' - CALLED!")
957     if not is_cache_initialized("is_registered"):
958         # DEBUG: print(f"DEBUG: Cache for 'is_registered' not initialized, fetching all rows ...")
959         try:
960             cursor.execute("SELECT domain FROM instances")
961
962             # Check Set all
963             set_all_cache_key("is_registered", cursor.fetchall(), True)
964         except BaseException as e:
965             print(f"ERROR: failed SQL query: domain='{domain}',exception[{type(e)}]:'{str(e)}'")
966             sys.exit(255)
967
968     # Is cache found?
969     registered = is_cache_key_set("is_registered", domain)
970
971     # DEBUG: print(f"DEBUG: registered='{registered}' - EXIT!")
972     return registered
973
974 def add_instance(domain: str, origin: str, originator: str, path: str = None):
975     # DEBUG: print("DEBUG: domain,origin,originator,path:", domain, origin, originator, path)
976     if not validators.domain(domain.split("/")[0]):
977         print("WARNING: Bad domain name:", domain)
978         raise
979     elif origin is not None and not validators.domain(origin.split("/")[0]):
980         print("WARNING: Bad origin name:", origin)
981         raise
982
983     software = determine_software(domain, path)
984     # DEBUG: print("DEBUG: Determined software:", software)
985
986     print(f"INFO: Adding instance {domain} (origin: {origin})")
987     try:
988         cursor.execute(
989             "INSERT INTO instances (domain, origin, originator, hash, software, first_seen) VALUES (?, ?, ?, ?, ?, ?)",
990             (
991                domain,
992                origin,
993                originator,
994                get_hash(domain),
995                software,
996                time.time()
997             ),
998         )
999
1000         set_cache_key("is_registered", domain, True)
1001
1002         if has_pending_nodeinfos(domain):
1003             # DEBUG: print(f"DEBUG: domain='{domain}' has pending nodeinfo being updated ...")
1004             update_nodeinfos(domain)
1005             remove_pending_error(domain)
1006
1007         if domain in pending_errors:
1008             # DEBUG: print("DEBUG: domain has pending error being updated:", domain)
1009             update_last_error(domain, pending_errors[domain])
1010             remove_pending_error(domain)
1011
1012     except BaseException as e:
1013         print(f"ERROR: failed SQL query: domain='{domain}',exception[{type(e)}]:'{str(e)}'")
1014         sys.exit(255)
1015     else:
1016         # DEBUG: print("DEBUG: Updating nodeinfo for domain:", domain)
1017         update_last_nodeinfo(domain)
1018
1019     # DEBUG: print("DEBUG: EXIT!")
1020
1021 def send_bot_post(instance: str, blocks: dict):
1022     message = instance + " has blocked the following instances:\n\n"
1023     truncated = False
1024
1025     if len(blocks) > 20:
1026         truncated = True
1027         blocks = blocks[0 : 19]
1028
1029     for block in blocks:
1030         if block["reason"] == None or block["reason"] == '':
1031             message = message + block["blocked"] + " with unspecified reason\n"
1032         else:
1033             if len(block["reason"]) > 420:
1034                 block["reason"] = block["reason"][0:419] + "[…]"
1035
1036             message = message + block["blocked"] + ' for "' + block["reason"].replace("@", "@\u200b") + '"\n'
1037
1038     if truncated:
1039         message = message + "(the list has been truncated to the first 20 entries)"
1040
1041     botheaders = {**api_headers, **{"Authorization": "Bearer " + config["bot_token"]}}
1042
1043     req = reqto.post(
1044         f"{config['bot_instance']}/api/v1/statuses",
1045         data={
1046             "status"      : message,
1047             "visibility"  : config['bot_visibility'],
1048             "content_type": "text/plain"
1049         },
1050         headers=botheaders,
1051         timeout=10
1052     ).json()
1053
1054     return True
1055
1056 def get_mastodon_blocks(domain: str) -> dict:
1057     # DEBUG: print("DEBUG: Fetching mastodon blocks from domain:", domain)
1058     blocks = {
1059         "Suspended servers": [],
1060         "Filtered media"   : [],
1061         "Limited servers"  : [],
1062         "Silenced servers" : [],
1063     }
1064
1065     try:
1066         doc = bs4.BeautifulSoup(
1067             reqto.get(f"https://{domain}/about/more", headers=headers, timeout=(config["connection_timeout"], config["read_timeout"])).text,
1068             "html.parser",
1069         )
1070     except BaseException as e:
1071         print("ERROR: Cannot fetch from domain:", domain, e)
1072         update_last_error(domain, e)
1073         return {}
1074
1075     for header in doc.find_all("h3"):
1076         header_text = tidyup(header.text)
1077
1078         if header_text in language_mapping:
1079             # DEBUG: print(f"DEBUG: header_text='{header_text}'")
1080             header_text = language_mapping[header_text]
1081
1082         if header_text in blocks or header_text.lower() in blocks:
1083             # replaced find_next_siblings with find_all_next to account for instances that e.g. hide lists in dropdown menu
1084             for line in header.find_all_next("table")[0].find_all("tr")[1:]:
1085                 blocks[header_text].append(
1086                     {
1087                         "domain": tidyup(line.find("span").text),
1088                         "hash"  : tidyup(line.find("span")["title"][9:]),
1089                         "reason": tidyup(line.find_all("td")[1].text),
1090                     }
1091                 )
1092
1093     # DEBUG: print("DEBUG: Returning blocks for domain:", domain)
1094     return {
1095         "reject"        : blocks["Suspended servers"],
1096         "media_removal" : blocks["Filtered media"],
1097         "followers_only": blocks["Limited servers"] + blocks["Silenced servers"],
1098     }
1099
1100 def get_friendica_blocks(domain: str) -> dict:
1101     # DEBUG: print("DEBUG: Fetching friendica blocks from domain:", domain)
1102     blocks = []
1103
1104     try:
1105         doc = bs4.BeautifulSoup(
1106             reqto.get(f"https://{domain}/friendica", headers=headers, timeout=(config["connection_timeout"], config["read_timeout"])).text,
1107             "html.parser",
1108         )
1109     except BaseException as e:
1110         print("WARNING: Failed to fetch /friendica from domain:", domain, e)
1111         update_last_error(domain, e)
1112         return {}
1113
1114     blocklist = doc.find(id="about_blocklist")
1115
1116     # Prevents exceptions:
1117     if blocklist is None:
1118         # DEBUG: print("DEBUG:Instance has no block list:", domain)
1119         return {}
1120
1121     for line in blocklist.find("table").find_all("tr")[1:]:
1122         # DEBUG: print(f"DEBUG: line='{line}'")
1123         blocks.append({
1124             "domain": tidyup(line.find_all("td")[0].text),
1125             "reason": tidyup(line.find_all("td")[1].text)
1126         })
1127
1128     # DEBUG: print("DEBUG: Returning blocks() for domain:", domain, len(blocks))
1129     return {
1130         "reject": blocks
1131     }
1132
1133 def get_misskey_blocks(domain: str) -> dict:
1134     # DEBUG: print("DEBUG: Fetching misskey blocks from domain:", domain)
1135     blocks = {
1136         "suspended": [],
1137         "blocked"  : []
1138     }
1139
1140     offset = 0
1141     step = config["misskey_offset"]
1142     while True:
1143         # iterating through all "suspended" (follow-only in its terminology)
1144         # instances page-by-page, since that troonware doesn't support
1145         # sending them all at once
1146         try:
1147             # DEBUG: print(f"DEBUG: Fetching offset='{offset}' from '{domain}' ...")
1148             if offset == 0:
1149                 # DEBUG: print("DEBUG: Sending JSON API request to domain,step,offset:", domain, step, offset)
1150                 fetched = post_json_api(domain, "/api/federation/instances", json.dumps({
1151                     "sort"     : "+pubAt",
1152                     "host"     : None,
1153                     "suspended": True,
1154                     "limit"    : step
1155                 }), {"Origin": domain})
1156             else:
1157                 # DEBUG: print("DEBUG: Sending JSON API request to domain,step,offset:", domain, step, offset)
1158                 fetched = post_json_api(domain, "/api/federation/instances", json.dumps({
1159                     "sort"     : "+pubAt",
1160                     "host"     : None,
1161                     "suspended": True,
1162                     "limit"    : step,
1163                     "offset"   : offset - 1
1164                 }), {"Origin": domain})
1165
1166             # DEBUG: print("DEBUG: fetched():", len(fetched))
1167             if len(fetched) == 0:
1168                 # DEBUG: print("DEBUG: Returned zero bytes, exiting loop:", domain)
1169                 break
1170             elif len(fetched) != config["misskey_offset"]:
1171                 # DEBUG: print(f"DEBUG: Fetched '{len(fetched)}' row(s) but expected: '{config['misskey_offset']}'")
1172                 offset = offset + (config["misskey_offset"] - len(fetched))
1173             else:
1174                 # DEBUG: print("DEBUG: Raising offset by step:", step)
1175                 offset = offset + step
1176
1177             for instance in fetched:
1178                 # just in case
1179                 if instance["isSuspended"]:
1180                     blocks["suspended"].append(
1181                         {
1182                             "domain": tidyup(instance["host"]),
1183                             # no reason field, nothing
1184                             "reason": None
1185                         }
1186                     )
1187
1188         except BaseException as e:
1189             print("WARNING: Caught error, exiting loop:", domain, e)
1190             update_last_error(domain, e)
1191             offset = 0
1192             break
1193
1194     while True:
1195         # same shit, different asshole ("blocked" aka full suspend)
1196         try:
1197             if offset == 0:
1198                 # DEBUG: print("DEBUG: Sending JSON API request to domain,step,offset:", domain, step, offset)
1199                 fetched = post_json_api(domain,"/api/federation/instances", json.dumps({
1200                     "sort"   : "+pubAt",
1201                     "host"   : None,
1202                     "blocked": True,
1203                     "limit"  : step
1204                 }), {"Origin": domain})
1205             else:
1206                 # DEBUG: print("DEBUG: Sending JSON API request to domain,step,offset:", domain, step, offset)
1207                 fetched = post_json_api(domain,"/api/federation/instances", json.dumps({
1208                     "sort"   : "+pubAt",
1209                     "host"   : None,
1210                     "blocked": True,
1211                     "limit"  : step,
1212                     "offset" : offset-1
1213                 }), {"Origin": domain})
1214
1215             # DEBUG: print("DEBUG: fetched():", len(fetched))
1216             if len(fetched) == 0:
1217                 # DEBUG: print("DEBUG: Returned zero bytes, exiting loop:", domain)
1218                 break
1219             elif len(fetched) != config["misskey_offset"]:
1220                 # DEBUG: print(f"DEBUG: Fetched '{len(fetched)}' row(s) but expected: '{config['misskey_offset']}'")
1221                 offset = offset + (config["misskey_offset"] - len(fetched))
1222             else:
1223                 # DEBUG: print("DEBUG: Raising offset by step:", step)
1224                 offset = offset + step
1225
1226             for instance in fetched:
1227                 if instance["isBlocked"]:
1228                     blocks["blocked"].append({
1229                         "domain": tidyup(instance["host"]),
1230                         "reason": None
1231                     })
1232
1233         except BaseException as e:
1234             print("ERROR: Exception during POST:", domain, e)
1235             update_last_error(domain, e)
1236             offset = 0
1237             break
1238
1239     # DEBUG: print("DEBUG: Returning for domain,blocked(),suspended():", domain, len(blocks["blocked"]), len(blocks["suspended"]))
1240     return {
1241         "reject"        : blocks["blocked"],
1242         "followers_only": blocks["suspended"]
1243     }
1244
1245 def tidyup(string: str) -> str:
1246     # some retards put their blocks in variable case
1247     string = string.lower().strip()
1248
1249     # other retards put the port
1250     string = re.sub("\:\d+$", "", string)
1251
1252     # bigger retards put the schema in their blocklist, sometimes even without slashes
1253     string = re.sub("^https?\:(\/*)", "", string)
1254
1255     # and trailing slash
1256     string = re.sub("\/$", "", string)
1257
1258     # and the @
1259     string = re.sub("^\@", "", string)
1260
1261     # the biggest retards of them all try to block individual users
1262     string = re.sub("(.+)\@", "", string)
1263
1264     return string