]> 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_instance_data() will fail
77 instance_data = {
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     # Found total peers
84     "total_peers"        : {},
85     # Last fetched instances
86     "last_instance_fetch": {},
87     # Last updated
88     "last_updated"       : {},
89     # Last blocked
90     "last_blocked"       : {},
91     # Last nodeinfo (fetched)
92     "last_nodeinfo"      : {},
93     # Last status code
94     "last_status_code"   : {},
95     # Last error details
96     "last_error_details" : {},
97 }
98
99 language_mapping = {
100     # English -> English
101     "Silenced instances"            : "Silenced servers",
102     "Suspended instances"           : "Suspended servers",
103     "Limited instances"             : "Limited servers",
104     # Mappuing German -> English
105     "Gesperrte Server"              : "Suspended servers",
106     "Gefilterte Medien"             : "Filtered media",
107     "Stummgeschaltete Server"       : "Silenced servers",
108     # Japanese -> English
109     "停止済みのサーバー"            : "Suspended servers",
110     "制限中のサーバー"              : "Limited servers",
111     "メディアを拒否しているサーバー": "Filtered media",
112     "サイレンス済みのサーバー"      : "Silenced servers",
113     # ??? -> English
114     "שרתים מושעים"                  : "Suspended servers",
115     "מדיה מסוננת"                   : "Filtered media",
116     "שרתים מוגבלים"                 : "Silenced servers",
117     # French -> English
118     "Serveurs suspendus"            : "Suspended servers",
119     "Médias filtrés"                : "Filtered media",
120     "Serveurs limités"              : "Limited servers",
121     "Serveurs modérés"              : "Limited servers",
122 }
123
124 # URL for fetching peers
125 get_peers_url = "/api/v1/instance/peers"
126
127 # Cache for redundant SQL queries
128 cache = {}
129
130 # Connect to database
131 connection = sqlite3.connect("blocks.db")
132 cursor = connection.cursor()
133
134 # Pattern instance for version numbers
135 patterns = [
136     # semantic version number (with v|V) prefix)
137     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-]+)*))?)?$"),
138     # non-sematic, e.g. 1.2.3.4
139     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*))?)$"),
140     # non-sematic, e.g. 2023-05[-dev]
141     re.compile("^(?P<year>[1-9]{1}[0-9]{3})\.(?P<month>[0-9]{2})(-dev){0,1}$"),
142     # non-semantic, e.g. abcdef0
143     re.compile("^[a-f0-9]{7}$"),
144 ]
145
146 ##### Cache #####
147
148 def is_cache_initialized(key: str) -> bool:
149     return key in cache
150
151 def set_all_cache_key(key: str, rows: list, value: any):
152     # NOISY-DEBUG: print(f"DEBUG: key='{key}',rows()={len(rows)},value[]={type(value)} - CALLED!")
153     if type(key) != str:
154         raise ValueError("Parameter key[]='{type(key)}' is not 'str'")
155     elif not is_cache_initialized(key):
156         # NOISY-DEBUG: print(f"DEBUG: Cache for key='{key}' not initialized.")
157         cache[key] = {}
158
159     for sub in rows:
160         # NOISY-DEBUG: print(f"DEBUG: Setting key='{key}',sub[{type(sub)}]='{sub}'")
161
162         if isinstance(sub, tuple):
163             cache[key][sub[0]] = value
164         else:
165             print(f"WARNING: Unsupported type row[]='{type(row)}'")
166
167     # NOISY-DEBUG: print("DEBUG: EXIT!")
168
169 def set_cache_key(key: str, sub: str, value: any):
170     if type(key) != str:
171         raise ValueError("Parameter key[]='{type(key)}' is not 'str'")
172     elif type(sub) != str:
173         raise ValueError("Parameter sub[]='{type(sub)}' is not 'str'")
174     elif not is_cache_initialized(key):
175         print(f"WARNING: Bad method call, key='{key}' is not initialized yet.")
176         raise Exception(f"Cache for key='{key}' is not initialized, but function called")
177
178     cache[key][sub] = value
179
180 def is_cache_key_set(key: str, sub: str) -> bool:
181     if type(key) != str:
182         raise ValueError("Parameter key[]='{type(key)}' is not 'str'")
183     elif type(sub) != str:
184         raise ValueError("Parameter sub[]='{type(sub)}' is not 'str'")
185     elif not is_cache_initialized(key):
186         print(f"WARNING: Bad method call, key='{key}' is not initialized yet.")
187         raise Exception(f"Cache for key='{key}' is not initialized, but function called")
188
189     return sub in cache[key]
190
191 ##### Other functions #####
192
193 def is_primitive(var: any) -> bool:
194     # NOISY-DEBUG: print(f"DEBUG: var[]='{type(var)}' - CALLED!")
195     return type(var) in {int, str, float, bool} or var == None
196
197 def set_instance_data(key: str, domain: str, value: any):
198     # NOISY-DEBUG: print(f"DEBUG: key='{key}',domain='{domain}',value[]='{type(value)}' - CALLED!")
199     if type(key) != str:
200         raise ValueError("Parameter key[]='{type(key)}' is not 'str'")
201     elif key == "":
202         raise ValueError(f"Parameter 'key' cannot be empty")
203     elif type(domain) != str:
204         raise ValueError("Parameter domain[]='{type(domain)}' is not 'str'")
205     elif domain == "":
206         raise ValueError(f"Parameter 'domain' cannot be empty")
207     elif not key in instance_data:
208         raise ValueError(f"key='{key}' not found in instance_data")
209     elif not is_primitive(value):
210         raise ValueError(f"value[]='{type(value)}' is not a primitive type")
211
212     # Set it
213     instance_data[key][domain] = value
214
215 def add_peers(rows: dict) -> list:
216     # DEBUG: print(f"DEBUG: rows()={len(rows)} - CALLED!")
217     peers = list()
218     for element in ["linked", "allowed", "blocked"]:
219         # DEBUG: print(f"DEBUG: Checking element='{element}'")
220         if element in rows and rows[element] != None:
221             # DEBUG: print(f"DEBUG: Adding {len(rows[element])} peer(s) to peers list ...")
222             for peer in rows[element]:
223                 # DEBUG: print(f"DEBUG: peer='{peer}' - BEFORE!")
224                 peer = tidyup(peer)
225
226                 # DEBUG: print(f"DEBUG: peer='{peer}' - AFTER!")
227                 if is_blacklisted(peer):
228                     # DEBUG: print(f"DEBUG: peer='{peer}' is blacklisted, skipped!")
229                     continue
230
231                 # DEBUG: print(f"DEBUG: Adding peer='{peer}' ...")
232                 peers.append(peer)
233
234     # DEBUG: print(f"DEBUG: peers()={len(peers)} - EXIT!")
235     return peers
236
237 def remove_version(software: str) -> str:
238     # DEBUG: print(f"DEBUG: software='{software}' - CALLED!")
239     if not "." in software and " " not in software:
240         print(f"WARNING: software='{software}' does not contain a version number.")
241         return software
242
243     temp = software
244     if ";" in software:
245         temp = software.split(";")[0]
246     elif "," in software:
247         temp = software.split(",")[0]
248     elif " - " in software:
249         temp = software.split(" - ")[0]
250
251     # DEBUG: print(f"DEBUG: software='{software}'")
252     version = None
253     if " " in software:
254         version = temp.split(" ")[-1]
255     elif "/" in software:
256         version = temp.split("/")[-1]
257     elif "-" in software:
258         version = temp.split("-")[-1]
259     else:
260         # DEBUG: print(f"DEBUG: Was not able to find common seperator, returning untouched software='{software}'")
261         return software
262
263     matches = None
264     match = None
265     # DEBUG: print(f"DEBUG: Checking {len(patterns)} patterns ...")
266     for pattern in patterns:
267         # Run match()
268         match = pattern.match(version)
269
270         # DEBUG: print(f"DEBUG: match[]={type(match)}")
271         if type(match) is re.Match:
272             break
273
274     # DEBUG: print(f"DEBUG: version[{type(version)}]='{version}',match='{match}'")
275     if type(match) is not re.Match:
276         print(f"WARNING: version='{version}' does not match regex, leaving software='{software}' untouched.")
277         return software
278
279     # DEBUG: print(f"DEBUG: Found valid version number: '{version}', removing it ...")
280     end = len(temp) - len(version) - 1
281
282     # DEBUG: print(f"DEBUG: end[{type(end)}]={end}")
283     software = temp[0:end].strip()
284     if " version" in software:
285         # DEBUG: print(f"DEBUG: software='{software}' contains word ' version'")
286         software = strip_until(software, " version")
287
288     # DEBUG: print(f"DEBUG: software='{software}' - EXIT!")
289     return software
290
291 def strip_powered_by(software: str) -> str:
292     # DEBUG: print(f"DEBUG: software='{software}' - CALLED!")
293     if software == "":
294         print(f"ERROR: Bad method call, 'software' is empty")
295         raise Exception("Parameter 'software' is empty")
296     elif not "powered by" in software:
297         print(f"WARNING: Cannot find 'powered by' in '{software}'!")
298         return software
299
300     start = software.find("powered by ")
301     # DEBUG: print(f"DEBUG: start[{type(start)}]='{start}'")
302
303     software = software[start + 11:].strip()
304     # DEBUG: print(f"DEBUG: software='{software}'")
305
306     software = strip_until(software, " - ")
307
308     # DEBUG: print(f"DEBUG: software='{software}' - EXIT!")
309     return software
310
311 def strip_hosted_on(software: str) -> str:
312     # DEBUG: print(f"DEBUG: software='{software}' - CALLED!")
313     if software == "":
314         print(f"ERROR: Bad method call, 'software' is empty")
315         raise Exception("Parameter 'software' is empty")
316     elif not "hosted on" in software:
317         print(f"WARNING: Cannot find 'hosted on' in '{software}'!")
318         return software
319
320     end = software.find("hosted on ")
321     # DEBUG: print(f"DEBUG: end[{type(end)}]='{end}'")
322
323     software = software[0, start].strip()
324     # DEBUG: print(f"DEBUG: software='{software}'")
325
326     software = strip_until(software, " - ")
327
328     # DEBUG: print(f"DEBUG: software='{software}' - EXIT!")
329     return software
330
331 def strip_until(software: str, until: str) -> str:
332     # DEBUG: print(f"DEBUG: software='{software}',until='{until}' - CALLED!")
333     if software == "":
334         print(f"ERROR: Bad method call, 'software' is empty")
335         raise Exception("Parameter 'software' is empty")
336     elif until == "":
337         print(f"ERROR: Bad method call, 'until' is empty")
338         raise Exception("Parameter 'until' is empty")
339     elif not until in software:
340         print(f"WARNING: Cannot find '{until}' in '{software}'!")
341         return software
342
343     # Next, strip until part
344     end = software.find(until)
345
346     # DEBUG: print(f"DEBUG: end[{type(end)}]='{end}'")
347     if end > 0:
348         software = software[0:end].strip()
349
350     # DEBUG: print(f"DEBUG: software='{software}' - EXIT!")
351     return software
352
353 def is_blacklisted(domain: str) -> bool:
354     if type(domain) != str:
355         raise ValueError(f"Parameter domain[]={type(domain)} is not 'str'")
356     elif domain == "":
357         raise ValueError(f"Parameter 'domain' cannot be empty")
358
359     blacklisted = False
360     for peer in blacklist:
361         if peer in domain:
362             blacklisted = True
363
364     return blacklisted
365
366 def remove_pending_error(domain: str):
367     if type(domain) != str:
368         raise ValueError(f"Parameter domain[]={type(domain)} is not 'str'")
369     elif domain == "":
370         raise ValueError(f"Parameter 'domain' cannot be empty")
371
372     try:
373         # Prevent updating any pending errors, nodeinfo was found
374         del pending_errors[domain]
375
376     except:
377         pass
378
379 def get_hash(domain: str) -> str:
380     if type(domain) != str:
381         raise ValueError(f"Parameter domain[]={type(domain)} is not 'str'")
382     elif domain == "":
383         raise ValueError(f"Parameter 'domain' cannot be empty")
384
385     return hashlib.sha256(domain.encode("utf-8")).hexdigest()
386
387 def update_last_blocked(domain: str):
388     if type(domain) != str:
389         raise ValueError(f"Parameter domain[]={type(domain)} is not 'str'")
390     elif domain == "":
391         raise ValueError(f"Parameter 'domain' cannot be empty")
392
393     # DEBUG: print("DEBUG: Updating last_blocked for domain", domain)
394     set_instance_data("last_blocked", domain, time.time())
395
396     # Running pending updated
397     # DEBUG: print(f"DEBUG: Invoking update_instance_data({domain}) ...")
398     update_instance_data(domain)
399
400     # DEBUG: print("DEBUG: EXIT!")
401
402 def has_pending_instance_data(domain: str) -> bool:
403     # DEBUG: print(f"DEBUG: domain='{domain}' - CALLED!")
404     if type(domain) != str:
405         raise ValueError(f"Parameter domain[]={type(domain)} is not 'str'")
406     elif domain == "":
407         raise ValueError(f"Parameter 'domain' cannot be empty")
408
409     has_pending = False
410     for key in instance_data:
411         # DEBUG: print(f"DEBUG: key='{key}',domain='{domain}',instance_data[key]()='{len(instance_data[key])}'")
412         if domain in instance_data[key]:
413             has_pending = True
414             break
415
416     # DEBUG: print(f"DEBUG: has_pending='{has_pending}' - EXIT!")
417     return has_pending
418
419 def update_instance_data(domain: str):
420     if type(domain) != str:
421         raise ValueError(f"Parameter domain[]={type(domain)} is not 'str'")
422     elif domain == "":
423         raise ValueError(f"Parameter 'domain' cannot be empty")
424
425     # DEBUG: print(f"DEBUG: Updating nodeinfo for domain='{domain}' ...")
426     sql_string = ''
427     fields = list()
428     for key in instance_data:
429         # DEBUG: print("DEBUG: key:", key)
430         if domain in instance_data[key]:
431            # DEBUG: print(f"DEBUG: Adding '{instance_data[key][domain]}' for key='{key}' ...")
432            fields.append(instance_data[key][domain])
433            sql_string += f" {key} = ?,"
434
435     fields.append(domain)
436
437     if sql_string == '':
438         raise ValueError(f"No fields have been set, but method invoked, domain='{domain}'")
439
440     # DEBUG: print(f"DEBUG: sql_string='{sql_string}',fields()={len(fields)}")
441     sql_string = "UPDATE instances SET" + sql_string + " last_updated = TIME() WHERE domain = ? LIMIT 1"
442     # DEBUG: print("DEBUG: sql_string:", sql_string)
443
444     try:
445         # DEBUG: print("DEBUG: Executing SQL:", sql_string)
446         cursor.execute(sql_string, fields)
447         # DEBUG: print(f"DEBUG: Success! (rowcount={cursor.rowcount })")
448
449         if cursor.rowcount == 0:
450             print(f"WARNING: Did not update any rows: domain='{domain}',fields()={len(fields)} - EXIT!")
451             return
452
453         connection.commit()
454
455         # DEBUG: print("DEBUG: Deleting instance_data for domain:", domain)
456         for key in instance_data:
457             try:
458                 # DEBUG: print("DEBUG: Deleting key:", key)
459                 del instance_data[key][domain]
460             except:
461                 pass
462
463     except BaseException as e:
464         print(f"ERROR: failed SQL query: domain='{domain}',sql_string='{sql_string}',exception[{type(e)}]:'{str(e)}'")
465         sys.exit(255)
466
467     # DEBUG: print("DEBUG: EXIT!")
468
469 def log_error(domain: str, res: any):
470     # DEBUG: print("DEBUG: domain,res[]:", domain, type(res))
471     if type(domain) != str:
472         raise ValueError(f"Parameter domain[]={type(domain)} is not 'str'")
473     elif domain == "":
474         raise ValueError(f"Parameter 'domain' cannot be empty")
475
476     try:
477         # DEBUG: print("DEBUG: BEFORE res[]:", type(res))
478         if isinstance(res, BaseException) or isinstance(res, json.JSONDecodeError):
479             res = str(res)
480
481         # DEBUG: print("DEBUG: AFTER res[]:", type(res))
482         if type(res) is str:
483             cursor.execute("INSERT INTO error_log (domain, error_code, error_message, created) VALUES (?, 999, ?, ?)",[
484                 domain,
485                 res,
486                 time.time()
487             ])
488         else:
489             cursor.execute("INSERT INTO error_log (domain, error_code, error_message, created) VALUES (?, ?, ?, ?)",[
490                 domain,
491                 res.status_code,
492                 res.reason,
493                 time.time()
494             ])
495
496         # Cleanup old entries
497         # DEBUG: print(f"DEBUG: Purging old records (distance: {config['error_log_cleanup']})")
498         cursor.execute("DELETE FROM error_log WHERE created < ?", [time.time() - config["error_log_cleanup"]])
499     except BaseException as e:
500         print(f"ERROR: failed SQL query: domain='{domain}',exception[{type(e)}]:'{str(e)}'")
501         sys.exit(255)
502
503     # DEBUG: print("DEBUG: EXIT!")
504
505 def update_last_error(domain: str, res: any):
506     # DEBUG: print("DEBUG: domain,res[]:", domain, type(res))
507     if type(domain) != str:
508         raise ValueError(f"Parameter domain[]={type(domain)} is not 'str'")
509     elif domain == "":
510         raise ValueError(f"Parameter 'domain' cannot be empty")
511
512     # DEBUG: print("DEBUG: BEFORE res[]:", type(res))
513     if isinstance(res, BaseException) or isinstance(res, json.JSONDecodeError):
514         res = str(res)
515
516     # DEBUG: print("DEBUG: AFTER res[]:", type(res))
517     if type(res) is str:
518         # DEBUG: print(f"DEBUG: Setting last_error_details='{res}'");
519         set_instance_data("last_status_code"  , domain, 999)
520         set_instance_data("last_error_details", domain, res)
521     else:
522         # DEBUG: print(f"DEBUG: Setting last_error_details='{res.reason}'");
523         set_instance_data("last_status_code"  , domain, res.status_code)
524         set_instance_data("last_error_details", domain, res.reason)
525
526     # Running pending updated
527     # DEBUG: print(f"DEBUG: Invoking update_instance_data({domain}) ...")
528     update_instance_data(domain)
529
530     log_error(domain, res)
531
532     # DEBUG: print("DEBUG: EXIT!")
533
534 def update_last_instance_fetch(domain: str):
535     if type(domain) != str:
536         raise ValueError(f"Parameter domain[]={type(domain)} is not 'str'")
537     elif domain == "":
538         raise ValueError(f"Parameter 'domain' cannot be empty")
539
540     # DEBUG: print("DEBUG: Updating last_instance_fetch for domain:", domain)
541     set_instance_data("last_instance_fetch", domain, time.time())
542
543     # Running pending updated
544     # DEBUG: print(f"DEBUG: Invoking update_instance_data({domain}) ...")
545     update_instance_data(domain)
546
547     # DEBUG: print("DEBUG: EXIT!")
548
549 def update_last_nodeinfo(domain: str):
550     if type(domain) != str:
551         raise ValueError(f"Parameter domain[]={type(domain)} is not 'str'")
552     elif domain == "":
553         raise ValueError(f"Parameter 'domain' cannot be empty")
554
555     # DEBUG: print("DEBUG: Updating last_nodeinfo for domain:", domain)
556     set_instance_data("last_nodeinfo", domain, time.time())
557     set_instance_data("last_updated" , domain, time.time())
558
559     # Running pending updated
560     # DEBUG: print(f"DEBUG: Invoking update_instance_data({domain}) ...")
561     update_instance_data(domain)
562
563     # DEBUG: print("DEBUG: EXIT!")
564
565 def get_peers(domain: str, software: str) -> list:
566     if type(domain) != str:
567         raise ValueError(f"Parameter domain[]={type(domain)} is not 'str'")
568     elif domain == "":
569         raise ValueError(f"Parameter 'domain' cannot be empty")
570     elif type(software) != str and software != None:
571         raise ValueError(f"software[]={type(software)} is not 'str'")
572
573     # DEBUG: print(f"DEBUG: domain='{domain}',software='{software}' - CALLED!")
574     peers = list()
575
576     if software == "misskey":
577         # DEBUG: print(f"DEBUG: domain='{domain}' is misskey, sending API POST request ...")
578         offset = 0
579         step = config["misskey_offset"]
580
581         # iterating through all "suspended" (follow-only in its terminology)
582         # instances page-by-page, since that troonware doesn't support
583         # sending them all at once
584         while True:
585             # DEBUG: print(f"DEBUG: Fetching offset='{offset}' from '{domain}' ...")
586             if offset == 0:
587                 fetched = post_json_api(domain, "/api/federation/instances", json.dumps({
588                     "sort" : "+pubAt",
589                     "host" : None,
590                     "limit": step
591                 }), {"Origin": domain})
592             else:
593                 fetched = post_json_api(domain, "/api/federation/instances", json.dumps({
594                     "sort"  : "+pubAt",
595                     "host"  : None,
596                     "limit" : step,
597                     "offset": offset - 1
598                 }), {"Origin": domain})
599
600             # DEBUG: print("DEBUG: fetched():", len(fetched))
601             if len(fetched) == 0:
602                 # DEBUG: print("DEBUG: Returned zero bytes, exiting loop:", domain)
603                 break
604             elif len(fetched) != config["misskey_offset"]:
605                 # DEBUG: print(f"DEBUG: Fetched '{len(fetched)}' row(s) but expected: '{config['misskey_offset']}'")
606                 offset = offset + (config["misskey_offset"] - len(fetched))
607             else:
608                 # DEBUG: print("DEBUG: Raising offset by step:", step)
609                 offset = offset + step
610
611             # Check records
612             # DEBUG: print(f"DEBUG: fetched({len(fetched)})[]={type(fetched)}")
613             if isinstance(fetched, dict) and "error" in fetched and "message" in fetched["error"]:
614                 print(f"WARNING: post_json_api() returned error: {fetched['error']['message']}")
615                 update_last_error(domain, fetched["error"]["message"])
616                 break
617
618             for row in fetched:
619                 # DEBUG: print(f"DEBUG: row():{len(row)}")
620                 if not "host" in row:
621                     print(f"WARNING: row()={len(row)} does not contain element 'host': {row},domain='{domain}'")
622                     continue
623                 elif type(row["host"]) != str:
624                     print(f"WARNING: row[host][]={type(row['host'])} is not 'str'")
625                     continue
626                 elif is_blacklisted(row["host"]):
627                     # DEBUG: print(f"DEBUG: row[host]='{row['host']}' is blacklisted. domain='{domain}'")
628                     continue
629
630                 # DEBUG: print(f"DEBUG: Adding peer: '{row['host']}'")
631                 peers.append(row["host"])
632
633         # DEBUG: print(f"DEBUG: Adding '{len(peers)}' for domain='{domain}'")
634         set_instance_data("total_peers", domain, len(peers))
635
636         # DEBUG: print(f"DEBUG: Updating last_instance_fetch for domain='{domain}' ...")
637         update_last_instance_fetch(domain)
638
639         # DEBUG: print("DEBUG: Returning peers[]:", type(peers))
640         return peers
641     elif software == "lemmy":
642         # DEBUG: print(f"DEBUG: domain='{domain}' is Lemmy, fetching JSON ...")
643         try:
644             res = reqto.get(f"https://{domain}/api/v3/site", headers=api_headers, timeout=(config["connection_timeout"], config["read_timeout"]))
645
646             data = res.json()
647             # DEBUG: print(f"DEBUG: res.ok={res.ok},res.status_code='{res.status_code}',data[]='{type(data)}'")
648             if not res.ok or res.status_code >= 400:
649                 print("WARNING: Could not reach any JSON API:", domain)
650                 update_last_error(domain, res)
651             elif res.ok and isinstance(data, list):
652                 # DEBUG: print(f"DEBUG: domain='{domain}' returned a list: '{data}'")
653                 sys.exit(255)
654             elif "federated_instances" in data:
655                 # DEBUG: print(f"DEBUG: Found federated_instances for domain='{domain}'")
656                 peers = peers + add_peers(data["federated_instances"])
657                 # DEBUG: print("DEBUG: Added instance(s) to peers")
658             else:
659                 print("WARNING: JSON response does not contain 'federated_instances':", domain)
660                 update_last_error(domain, res)
661
662         except BaseException as e:
663             print(f"WARNING: Exception during fetching JSON: domain='{domain}',exception[{type(e)}]:'{str(e)}'")
664
665         # DEBUG: print(f"DEBUG: Adding '{len(peers)}' for domain='{domain}'")
666         set_instance_data("total_peers", domain, len(peers))
667
668         # DEBUG: print(f"DEBUG: Updating last_instance_fetch for domain='{domain}' ...")
669         update_last_instance_fetch(domain)
670
671         # DEBUG: print("DEBUG: Returning peers[]:", type(peers))
672         return peers
673     elif software == "peertube":
674         # DEBUG: print(f"DEBUG: domain='{domain}' is a PeerTube, fetching JSON ...")
675
676         start = 0
677         for mode in ["followers", "following"]:
678             # DEBUG: print(f"DEBUG: domain='{domain}',mode='{mode}'")
679             while True:
680                 try:
681                     res = reqto.get(f"https://{domain}/api/v1/server/{mode}?start={start}&count=100", headers=api_headers, timeout=(config["connection_timeout"], config["read_timeout"]))
682
683                     data = res.json()
684                     # DEBUG: print(f"DEBUG: res.ok={res.ok},res.status_code='{res.status_code}',data[]='{type(data)}'")
685                     if res.ok and isinstance(data, dict):
686                         # DEBUG: print("DEBUG: Success, data:", len(data))
687                         if "data" in data:
688                             # DEBUG: print(f"DEBUG: Found {len(data['data'])} record(s).")
689                             for record in data["data"]:
690                                 # DEBUG: print(f"DEBUG: record()={len(record)}")
691                                 if mode in record and "host" in record[mode]:
692                                     # DEBUG: print(f"DEBUG: Found host={record[mode]['host']}, adding ...")
693                                     peers.append(record[mode]["host"])
694                                 else:
695                                     print(f"WARNING: record from '{domain}' has no '{mode}' or 'host' record: {record}")
696
697                             if len(data["data"]) < 100:
698                                 # DEBUG: print("DEBUG: Reached end of JSON response:", domain)
699                                 break
700
701                         # Continue with next row
702                         start = start + 100
703
704                 except BaseException as e:
705                     print(f"WARNING: Exception during fetching JSON: domain='{domain}',exception[{type(e)}]:'{str(e)}'")
706
707         # DEBUG: print(f"DEBUG: Adding '{len(peers)}' for domain='{domain}'")
708         set_instance_data("total_peers", domain, len(peers))
709
710         # DEBUG: print(f"DEBUG: Updating last_instance_fetch for domain='{domain}' ...")
711         update_last_instance_fetch(domain)
712
713         # DEBUG: print("DEBUG: Returning peers[]:", type(peers))
714         return peers
715
716     # DEBUG: print(f"DEBUG: Fetching get_peers_url='{get_peers_url}' from '{domain}' ...")
717     try:
718         res = reqto.get(f"https://{domain}{get_peers_url}", headers=api_headers, timeout=(config["connection_timeout"], config["read_timeout"]))
719
720         data = res.json()
721         # DEBUG: print(f"DEBUG: res.ok={res.ok},res.status_code={res.status_code},data[]='{type(data)}'")
722         if not res.ok or res.status_code >= 400:
723             # DEBUG: print(f"DEBUG: Was not able to fetch '{get_peers_url}', trying alternative ...")
724             res = reqto.get(f"https://{domain}/api/v3/site", headers=api_headers, timeout=(config["connection_timeout"], config["read_timeout"]))
725
726             data = res.json()
727             # DEBUG: print(f"DEBUG: res.ok={res.ok},res.status_code={res.status_code},data[]='{type(data)}'")
728             if not res.ok or res.status_code >= 400:
729                 print("WARNING: Could not reach any JSON API:", domain)
730                 update_last_error(domain, res)
731             elif res.ok and isinstance(data, list):
732                 # DEBUG: print(f"DEBUG: domain='{domain}' returned a list: '{data}'")
733                 sys.exit(255)
734             elif "federated_instances" in data:
735                 # DEBUG: print(f"DEBUG: Found federated_instances for domain='{domain}'")
736                 peers = peers + add_peers(data["federated_instances"])
737                 # DEBUG: print("DEBUG: Added instance(s) to peers")
738             else:
739                 print("WARNING: JSON response does not contain 'federated_instances':", domain)
740                 update_last_error(domain, res)
741         else:
742             # DEBUG: print("DEBUG: Querying API was successful:", domain, len(data))
743             peers = data
744
745     except BaseException as e:
746         print("WARNING: Some error during get():", domain, e)
747         update_last_error(domain, e)
748
749     # DEBUG: print(f"DEBUG: Adding '{len(peers)}' for domain='{domain}'")
750     set_instance_data("total_peers", domain, len(peers))
751
752     # DEBUG: print(f"DEBUG: Updating last_instance_fetch for domain='{domain}' ...")
753     update_last_instance_fetch(domain)
754
755     # DEBUG: print("DEBUG: Returning peers[]:", type(peers))
756     return peers
757
758 def post_json_api(domain: str, path: str, parameter: str, extra_headers: dict = {}) -> dict:
759     if type(domain) != str:
760         raise ValueError(f"Parameter domain[]={type(domain)} is not 'str'")
761     elif domain == "":
762         raise ValueError(f"Parameter 'domain' cannot be empty")
763     elif type(path) != str:
764         raise ValueError(f"path[]={type(path)} is not 'str'")
765     elif path == "":
766         raise ValueError(f"path cannot be empty")
767     elif type(parameter) != str:
768         raise ValueError(f"parameter[]={type(parameter)} is not 'str'")
769
770     # DEBUG: print("DEBUG: Sending POST to domain,path,parameter:", domain, path, parameter, extra_headers)
771     data = {}
772     try:
773         res = reqto.post(f"https://{domain}{path}", data=parameter, headers={**api_headers, **extra_headers}, timeout=(config["connection_timeout"], config["read_timeout"]))
774
775         data = res.json()
776         # DEBUG: print(f"DEBUG: res.ok={res.ok},res.status_code={res.status_code},data[]='{type(data)}'")
777         if not res.ok or res.status_code >= 400:
778             print(f"WARNING: Cannot query JSON API: domain='{domain}',path='{path}',parameter()={len(parameter)},res.status_code='{res.status_code}',data[]='{type(data)}'")
779             update_last_error(domain, res)
780
781     except BaseException as e:
782         print(f"WARNING: Some error during post(): domain='{domain}',path='{path}',parameter()={len(parameter)},exception[{type(e)}]:'{str(e)}'")
783
784     # DEBUG: print(f"DEBUG: Returning data({len(data)})=[]:{type(data)}")
785     return data
786
787 def fetch_nodeinfo(domain: str, path: str = None) -> list:
788     if type(domain) != str:
789         raise ValueError(f"Parameter domain[]={type(domain)} is not 'str'")
790     elif domain == "":
791         raise ValueError(f"Parameter 'domain' cannot be empty")
792
793     # DEBUG: print("DEBUG: Fetching nodeinfo from domain,path:", domain, path)
794
795     nodeinfo = fetch_wellknown_nodeinfo(domain)
796     # DEBUG: print("DEBUG: nodeinfo:", nodeinfo)
797
798     if len(nodeinfo) > 0:
799         # DEBUG: print("DEBUG: Returning auto-discovered nodeinfo:", len(nodeinfo))
800         return nodeinfo
801
802     requests = [
803        f"https://{domain}/nodeinfo/2.1.json",
804        f"https://{domain}/nodeinfo/2.1",
805        f"https://{domain}/nodeinfo/2.0.json",
806        f"https://{domain}/nodeinfo/2.0",
807        f"https://{domain}/nodeinfo/1.0",
808        f"https://{domain}/api/v1/instance"
809     ]
810
811     data = {}
812     for request in requests:
813         if path != None and path != "" and request != path:
814             # DEBUG: print(f"DEBUG: path='{path}' does not match request='{request}' - SKIPPED!")
815             continue
816
817         try:
818             # DEBUG: print("DEBUG: Fetching request:", request)
819             res = reqto.get(request, headers=api_headers, timeout=(config["connection_timeout"], config["read_timeout"]))
820
821             data = res.json()
822             # DEBUG: print(f"DEBUG: res.ok={res.ok},res.status_code={res.status_code},data[]='{type(data)}'")
823             if res.ok and isinstance(data, dict):
824                 # DEBUG: print("DEBUG: Success:", request)
825                 set_instance_data("detection_mode", domain, "STATIC_CHECK")
826                 set_instance_data("nodeinfo_url"  , domain, request)
827                 break
828             elif res.ok and isinstance(data, list):
829                 # DEBUG: print(f"DEBUG: domain='{domain}' returned a list: '{data}'")
830                 sys.exit(255)
831             elif not res.ok or res.status_code >= 400:
832                 print("WARNING: Failed fetching nodeinfo from domain:", domain)
833                 update_last_error(domain, res)
834                 continue
835
836         except BaseException as e:
837             # DEBUG: print("DEBUG: Cannot fetch API request:", request)
838             update_last_error(domain, e)
839             pass
840
841     # DEBUG: print("DEBUG: Returning data[]:", type(data))
842     return data
843
844 def fetch_wellknown_nodeinfo(domain: str) -> list:
845     if type(domain) != str:
846         raise ValueError(f"Parameter domain[]={type(domain)} is not 'str'")
847     elif domain == "":
848         raise ValueError(f"Parameter 'domain' cannot be empty")
849
850     # DEBUG: print("DEBUG: Fetching .well-known info for domain:", domain)
851     data = {}
852
853     try:
854         res = reqto.get(f"https://{domain}/.well-known/nodeinfo", headers=api_headers, timeout=(config["connection_timeout"], config["read_timeout"]))
855
856         data = res.json()
857         # DEBUG: print("DEBUG: domain,res.ok,data[]:", domain, res.ok, type(data))
858         if res.ok and isinstance(data, dict):
859             nodeinfo = data
860             # DEBUG: print("DEBUG: Found entries:", len(nodeinfo), domain)
861             if "links" in nodeinfo:
862                 # DEBUG: print("DEBUG: Found links in nodeinfo():", len(nodeinfo["links"]))
863                 for link in nodeinfo["links"]:
864                     # DEBUG: print("DEBUG: rel,href:", link["rel"], link["href"])
865                     if link["rel"] in nodeinfo_identifier:
866                         # DEBUG: print("DEBUG: Fetching nodeinfo from:", link["href"])
867                         res = reqto.get(link["href"])
868
869                         data = res.json()
870                         # DEBUG: print("DEBUG: href,res.ok,res.status_code:", link["href"], res.ok, res.status_code)
871                         if res.ok and isinstance(data, dict):
872                             # DEBUG: print("DEBUG: Found JSON nodeinfo():", len(data))
873                             set_instance_data("detection_mode", domain, "AUTO_DISCOVERY")
874                             set_instance_data("nodeinfo_url"  , domain, link["href"])
875                             break
876                     else:
877                         print("WARNING: Unknown 'rel' value:", domain, link["rel"])
878             else:
879                 print("WARNING: nodeinfo does not contain 'links':", domain)
880
881     except BaseException as e:
882         print("WARNING: Failed fetching .well-known info:", domain)
883         update_last_error(domain, e)
884         pass
885
886     # DEBUG: print("DEBUG: Returning data[]:", type(data))
887     return data
888
889 def fetch_generator_from_path(domain: str, path: str = "/") -> str:
890     if type(domain) != str:
891         raise ValueError(f"Parameter domain[]={type(domain)} is not 'str'")
892     elif domain == "":
893         raise ValueError(f"Parameter 'domain' cannot be empty")
894     elif type(path) != str:
895         raise ValueError(f"path[]={type(path)} is not 'str'")
896     elif path == "":
897         raise ValueError(f"Parameter 'domain' cannot be empty")
898
899     # DEBUG: print(f"DEBUG: domain='{domain}',path='{path}' - CALLED!")
900     software = None
901
902     try:
903         # DEBUG: print(f"DEBUG: Fetching path='{path}' from '{domain}' ...")
904         res = reqto.get(f"https://{domain}{path}", headers=headers, timeout=(config["connection_timeout"], config["read_timeout"]))
905
906         # DEBUG: print("DEBUG: domain,res.ok,res.status_code,res.text[]:", domain, res.ok, res.status_code, type(res.text))
907         if res.ok and res.status_code < 300 and len(res.text) > 0:
908             # DEBUG: print("DEBUG: Search for <meta name='generator'>:", domain)
909             doc = bs4.BeautifulSoup(res.text, "html.parser")
910
911             # DEBUG: print("DEBUG: doc[]:", type(doc))
912             generator = doc.find("meta", {"name": "generator"})
913             site_name = doc.find("meta", {"property": "og:site_name"})
914
915             # DEBUG: print(f"DEBUG: generator='{generator}',site_name='{site_name}'")
916             if isinstance(generator, bs4.element.Tag):
917                 # DEBUG: print("DEBUG: Found generator meta tag:", domain)
918                 software = tidyup(generator.get("content"))
919                 print(f"INFO: domain='{domain}' is generated by '{software}'")
920                 set_instance_data("detection_mode", domain, "GENERATOR")
921                 remove_pending_error(domain)
922             elif isinstance(site_name, bs4.element.Tag):
923                 # DEBUG: print("DEBUG: Found property=og:site_name:", domain)
924                 sofware = tidyup(site_name.get("content"))
925                 print(f"INFO: domain='{domain}' has og:site_name='{software}'")
926                 set_instance_data("detection_mode", domain, "SITE_NAME")
927                 remove_pending_error(domain)
928
929     except BaseException as e:
930         # DEBUG: print(f"DEBUG: Cannot fetch / from '{domain}':", e)
931         update_last_error(domain, e)
932         pass
933
934     # DEBUG: print(f"DEBUG: software[]={type(software)}")
935     if type(software) is str and software == "":
936         # DEBUG: print(f"DEBUG: Corrected empty string to None for software of domain='{domain}'")
937         software = None
938     elif type(software) is str and ("." in software or " " in software):
939         # DEBUG: print(f"DEBUG: software='{software}' may contain a version number, domain='{domain}', removing it ...")
940         software = remove_version(software)
941
942     # DEBUG: print(f"DEBUG: software[]={type(software)}")
943     if type(software) is str and " powered by " in software:
944         # DEBUG: print(f"DEBUG: software='{software}' has 'powered by' in it")
945         software = remove_version(strip_powered_by(software))
946     elif type(software) is str and " hosted on " in software:
947         # DEBUG: print(f"DEBUG: software='{software}' has 'hosted on' in it")
948         software = remove_version(strip_hosted_on(software))
949     elif type(software) is str and " by " in software:
950         # DEBUG: print(f"DEBUG: software='{software}' has ' by ' in it")
951         software = strip_until(software, " by ")
952     elif type(software) is str and " see " in software:
953         # DEBUG: print(f"DEBUG: software='{software}' has ' see ' in it")
954         software = strip_until(software, " see ")
955
956     # DEBUG: print(f"DEBUG: software='{software}' - EXIT!")
957     return software
958
959 def determine_software(domain: str, path: str = None) -> str:
960     if type(domain) != str:
961         raise ValueError(f"Parameter domain[]={type(domain)} is not 'str'")
962     elif domain == "":
963         raise ValueError(f"Parameter 'domain' cannot be empty")
964
965     # DEBUG: print("DEBUG: Determining software for domain,path:", domain, path)
966     software = None
967
968     # DEBUG: print(f"DEBUG: Fetching nodeinfo from '{domain}' ...")
969     data = fetch_nodeinfo(domain, path)
970
971     # DEBUG: print("DEBUG: data[]:", type(data))
972     if not isinstance(data, dict) or len(data) == 0:
973         # DEBUG: print("DEBUG: Could not determine software type:", domain)
974         return fetch_generator_from_path(domain)
975
976     # DEBUG: print("DEBUG: data():", len(data), data)
977     if "status" in data and data["status"] == "error" and "message" in data:
978         print("WARNING: JSON response is an error:", data["message"])
979         update_last_error(domain, data["message"])
980         return fetch_generator_from_path(domain)
981     elif "message" in data:
982         print("WARNING: JSON response contains only a message:", data["message"])
983         update_last_error(domain, data["message"])
984         return fetch_generator_from_path(domain)
985     elif "software" not in data or "name" not in data["software"]:
986         # DEBUG: print(f"DEBUG: JSON response from domain='{domain}' does not include [software][name], fetching / ...")
987         software = fetch_generator_from_path(domain)
988
989         # DEBUG: print(f"DEBUG: Generator for domain='{domain}' is: {software}, EXIT!")
990         return software
991
992     software = tidyup(data["software"]["name"])
993
994     # DEBUG: print("DEBUG: sofware after tidyup():", software)
995     if software in ["akkoma", "rebased"]:
996         # DEBUG: print("DEBUG: Setting pleroma:", domain, software)
997         software = "pleroma"
998     elif software in ["hometown", "ecko"]:
999         # DEBUG: print("DEBUG: Setting mastodon:", domain, software)
1000         software = "mastodon"
1001     elif software in ["calckey", "groundpolis", "foundkey", "cherrypick", "meisskey"]:
1002         # DEBUG: print("DEBUG: Setting misskey:", domain, software)
1003         software = "misskey"
1004     elif software.find("/") > 0:
1005         print("WARNING: Spliting of slash:", software)
1006         software = software.split("/")[-1];
1007     elif software.find("|") > 0:
1008         print("WARNING: Spliting of pipe:", software)
1009         software = tidyup(software.split("|")[0]);
1010     elif "powered by" in software:
1011         # DEBUG: print(f"DEBUG: software='{software}' has 'powered by' in it")
1012         software = strip_powered_by(software)
1013     elif type(software) is str and " by " in software:
1014         # DEBUG: print(f"DEBUG: software='{software}' has ' by ' in it")
1015         software = strip_until(software, " by ")
1016     elif type(software) is str and " see " in software:
1017         # DEBUG: print(f"DEBUG: software='{software}' has ' see ' in it")
1018         software = strip_until(software, " see ")
1019
1020     # DEBUG: print(f"DEBUG: software[]={type(software)}")
1021     if software == "":
1022         print("WARNING: tidyup() left no software name behind:", domain)
1023         software = None
1024
1025     # DEBUG: print(f"DEBUG: software[]={type(software)}")
1026     if str(software) == "":
1027         # DEBUG: print(f"DEBUG: software for '{domain}' was not detected, trying generator ...")
1028         software = fetch_generator_from_path(domain)
1029     elif len(str(software)) > 0 and ("." in software or " " in software):
1030         # DEBUG: print(f"DEBUG: software='{software}' may contain a version number, domain='{domain}', removing it ...")
1031         software = remove_version(software)
1032
1033     # DEBUG: print(f"DEBUG: software[]={type(software)}")
1034     if type(software) is str and "powered by" in software:
1035         # DEBUG: print(f"DEBUG: software='{software}' has 'powered by' in it")
1036         software = remove_version(strip_powered_by(software))
1037
1038     # DEBUG: print("DEBUG: Returning domain,software:", domain, software)
1039     return software
1040
1041 def update_block_reason(reason: str, blocker: str, blocked: str, block_level: str):
1042     if type(reason) != str and reason != None:
1043         raise ValueError(f"Parameter reason[]='{type(reason)}' is not 'str'")
1044     elif type(blocker) != str:
1045         raise ValueError(f"Parameter blocker[]='{type(blocker)}' is not 'str'")
1046     elif type(blocked) != str:
1047         raise ValueError(f"Parameter blocked[]='{type(blocked)}' is not 'str'")
1048     elif type(block_level) != str:
1049         raise ValueError(f"Parameter block_level[]='{type(block_level)}' is not 'str'")
1050
1051     # DEBUG: print("DEBUG: Updating block reason:", reason, blocker, blocked, block_level)
1052     try:
1053         cursor.execute(
1054             "UPDATE blocks SET reason = ?, last_seen = ? WHERE blocker = ? AND blocked = ? AND block_level = ? AND reason = ''",
1055             (
1056                 reason,
1057                 time.time(),
1058                 blocker,
1059                 blocked,
1060                 block_level
1061             ),
1062         )
1063
1064         # DEBUG: print(f"DEBUG: cursor.rowcount={cursor.rowcount}")
1065         if cursor.rowcount == 0:
1066             print("WARNING: Did not update any rows:", blocker, blocked)
1067             return
1068
1069     except BaseException as e:
1070         print(f"ERROR: failed SQL query: reason='{reason}',blocker='{blocker}',blocked='{blocked}',block_level='{block_level}',exception[{type(e)}]:'{str(e)}'")
1071         sys.exit(255)
1072
1073     # DEBUG: print("DEBUG: EXIT!")
1074
1075 def update_last_seen(blocker: str, blocked: str, block_level: str):
1076     # DEBUG: print("DEBUG: Updating last_seen for:", blocker, blocked, block_level)
1077     try:
1078         cursor.execute(
1079             "UPDATE blocks SET last_seen = ? WHERE blocker = ? AND blocked = ? AND block_level = ?",
1080             (
1081                 time.time(),
1082                 blocker,
1083                 blocked,
1084                 block_level
1085             )
1086         )
1087
1088         if cursor.rowcount == 0:
1089             print("WARNING: Did not update any rows:", domain)
1090             return
1091
1092     except BaseException as e:
1093         print(f"ERROR: failed SQL query: last_seen='{last_seen}',blocker='{blocker}',blocked='{blocked}',block_level='{block_level}',exception[{type(e)}]:'{str(e)}'")
1094         sys.exit(255)
1095
1096     # DEBUG: print("DEBUG: EXIT!")
1097
1098 def block_instance(blocker: str, blocked: str, reason: str, block_level: str):
1099     # DEBUG: print("DEBUG: blocker,blocked,reason,block_level:", blocker, blocked, reason, block_level)
1100     if type(blocker) != str:
1101         raise ValueError(f"Parameter blocker[]={type(blocker)} is not 'str'")
1102     elif blocker == "":
1103         raise ValueError(f"Parameter 'blocker' cannot be empty")
1104     elif not validators.domain(blocker.split("/")[0]):
1105         raise ValueError(f"Bad blocker='{blocker}'")
1106     elif type(blocked) != str:
1107         raise ValueError(f"Parameter blocked[]={type(blocked)} is not 'str'")
1108     elif blocked == "":
1109         raise ValueError(f"Parameter 'blocked' cannot be empty")
1110     elif not validators.domain(blocked.split("/")[0]):
1111         raise ValueError(f"Bad blocked='{blocked}'")
1112     elif is_blacklisted(blocker):
1113         raise Exception(f"blocker='{blocker}' is blacklisted but function invoked")
1114     elif is_blacklisted(blocked):
1115         raise Exception(f"blocked='{blocked}' is blacklisted but function invoked")
1116
1117     print("INFO: New block:", blocker, blocked, reason, block_level)
1118     try:
1119         cursor.execute(
1120             "INSERT INTO blocks (blocker, blocked, reason, block_level, first_seen, last_seen) VALUES(?, ?, ?, ?, ?, ?)",
1121              (
1122                  blocker,
1123                  blocked,
1124                  reason,
1125                  block_level,
1126                  time.time(),
1127                  time.time()
1128              ),
1129         )
1130     except BaseException as e:
1131         print(f"ERROR: failed SQL query: blocker='{blocker}',blocked='{blocked}',reason='{reason}',block_level='{block_level}',exception[{type(e)}]:'{str(e)}'")
1132         sys.exit(255)
1133
1134     # DEBUG: print("DEBUG: EXIT!")
1135
1136 def is_instance_registered(domain: str) -> bool:
1137     if type(domain) != str:
1138         raise ValueError(f"Parameter domain[]={type(domain)} is not 'str'")
1139     elif domain == "":
1140         raise ValueError(f"Parameter 'domain' cannot be empty")
1141
1142     # NOISY-DEBUG: # DEBUG: print(f"DEBUG: domain='{domain}' - CALLED!")
1143     if not is_cache_initialized("is_registered"):
1144         # NOISY-DEBUG: # DEBUG: print(f"DEBUG: Cache for 'is_registered' not initialized, fetching all rows ...")
1145         try:
1146             cursor.execute("SELECT domain FROM instances")
1147
1148             # Check Set all
1149             set_all_cache_key("is_registered", cursor.fetchall(), True)
1150         except BaseException as e:
1151             print(f"ERROR: failed SQL query: domain='{domain}',exception[{type(e)}]:'{str(e)}'")
1152             sys.exit(255)
1153
1154     # Is cache found?
1155     registered = is_cache_key_set("is_registered", domain)
1156
1157     # NOISY-DEBUG: # DEBUG: print(f"DEBUG: registered='{registered}' - EXIT!")
1158     return registered
1159
1160 def add_instance(domain: str, origin: str, originator: str, path: str = None):
1161     if type(domain) != str:
1162         raise ValueError(f"Parameter domain[]={type(domain)} is not 'str'")
1163     elif domain == "":
1164         raise ValueError(f"Parameter 'domain' cannot be empty")
1165     elif type(origin) != str and origin != None:
1166         raise ValueError(f"origin[]={type(origin)} is not 'str'")
1167     elif type(originator) != str:
1168         raise ValueError(f"originator[]={type(originator)} is not 'str'")
1169     elif originator == "":
1170         raise ValueError(f"originator cannot be empty")
1171     elif not validators.domain(domain.split("/")[0]):
1172         raise ValueError(f"Bad domain name='{domain}'")
1173     elif origin is not None and not validators.domain(origin.split("/")[0]):
1174         raise ValueError(f"Bad origin name='{origin}'")
1175     elif is_blacklisted(domain):
1176         raise Exception(f"domain='{domain}' is blacklisted, but method invoked")
1177
1178     # DEBUG: print("DEBUG: domain,origin,originator,path:", domain, origin, originator, path)
1179     software = determine_software(domain, path)
1180     # DEBUG: print("DEBUG: Determined software:", software)
1181
1182     print(f"INFO: Adding instance domain='{domain}' (origin='{origin}',software='{software}')")
1183     try:
1184         cursor.execute(
1185             "INSERT INTO instances (domain, origin, originator, hash, software, first_seen) VALUES (?, ?, ?, ?, ?, ?)",
1186             (
1187                domain,
1188                origin,
1189                originator,
1190                get_hash(domain),
1191                software,
1192                time.time()
1193             ),
1194         )
1195
1196         set_cache_key("is_registered", domain, True)
1197
1198         if has_pending_instance_data(domain):
1199             # DEBUG: print(f"DEBUG: domain='{domain}' has pending nodeinfo being updated ...")
1200             set_instance_data("last_status_code"  , domain, None)
1201             set_instance_data("last_error_details", domain, None)
1202             update_instance_data(domain)
1203             remove_pending_error(domain)
1204
1205         if domain in pending_errors:
1206             # DEBUG: print("DEBUG: domain has pending error being updated:", domain)
1207             update_last_error(domain, pending_errors[domain])
1208             remove_pending_error(domain)
1209
1210     except BaseException as e:
1211         print(f"ERROR: failed SQL query: domain='{domain}',exception[{type(e)}]:'{str(e)}'")
1212         sys.exit(255)
1213     else:
1214         # DEBUG: print("DEBUG: Updating nodeinfo for domain:", domain)
1215         update_last_nodeinfo(domain)
1216
1217     # DEBUG: print("DEBUG: EXIT!")
1218
1219 def send_bot_post(instance: str, blocks: dict):
1220     message = instance + " has blocked the following instances:\n\n"
1221     truncated = False
1222
1223     if len(blocks) > 20:
1224         truncated = True
1225         blocks = blocks[0 : 19]
1226
1227     for block in blocks:
1228         if block["reason"] == None or block["reason"] == '':
1229             message = message + block["blocked"] + " with unspecified reason\n"
1230         else:
1231             if len(block["reason"]) > 420:
1232                 block["reason"] = block["reason"][0:419] + "[…]"
1233
1234             message = message + block["blocked"] + ' for "' + block["reason"].replace("@", "@\u200b") + '"\n'
1235
1236     if truncated:
1237         message = message + "(the list has been truncated to the first 20 entries)"
1238
1239     botheaders = {**api_headers, **{"Authorization": "Bearer " + config["bot_token"]}}
1240
1241     req = reqto.post(
1242         f"{config['bot_instance']}/api/v1/statuses",
1243         data={
1244             "status"      : message,
1245             "visibility"  : config['bot_visibility'],
1246             "content_type": "text/plain"
1247         },
1248         headers=botheaders,
1249         timeout=10
1250     ).json()
1251
1252     return True
1253
1254 def get_mastodon_blocks(domain: str) -> dict:
1255     if type(domain) != str:
1256         raise ValueError(f"Parameter domain[]={type(domain)} is not 'str'")
1257     elif domain == "":
1258         raise ValueError(f"Parameter 'domain' cannot be empty")
1259
1260     # DEBUG: print("DEBUG: Fetching mastodon blocks from domain:", domain)
1261     blocks = {
1262         "Suspended servers": [],
1263         "Filtered media"   : [],
1264         "Limited servers"  : [],
1265         "Silenced servers" : [],
1266     }
1267
1268     try:
1269         doc = bs4.BeautifulSoup(
1270             reqto.get(f"https://{domain}/about", headers=headers, timeout=(config["connection_timeout"], config["read_timeout"])).text,
1271             "html.parser",
1272         )
1273     except BaseException as e:
1274         print("ERROR: Cannot fetch from domain:", domain, e)
1275         update_last_error(domain, e)
1276         return {}
1277
1278     for header in doc.find_all("h3"):
1279         header_text = tidyup(header.text)
1280
1281         if header_text in language_mapping:
1282             # DEBUG: print(f"DEBUG: header_text='{header_text}'")
1283             header_text = language_mapping[header_text]
1284
1285         if header_text in blocks or header_text.lower() in blocks:
1286             # replaced find_next_siblings with find_all_next to account for instances that e.g. hide lists in dropdown menu
1287             for line in header.find_all_next("table")[0].find_all("tr")[1:]:
1288                 blocks[header_text].append(
1289                     {
1290                         "domain": tidyup(line.find("span").text),
1291                         "hash"  : tidyup(line.find("span")["title"][9:]),
1292                         "reason": tidyup(line.find_all("td")[1].text),
1293                     }
1294                 )
1295
1296     # DEBUG: print("DEBUG: Returning blocks for domain:", domain)
1297     return {
1298         "reject"        : blocks["Suspended servers"],
1299         "media_removal" : blocks["Filtered media"],
1300         "followers_only": blocks["Limited servers"] + blocks["Silenced servers"],
1301     }
1302
1303 def get_friendica_blocks(domain: str) -> dict:
1304     if type(domain) != str:
1305         raise ValueError(f"Parameter domain[]={type(domain)} is not 'str'")
1306     elif domain == "":
1307         raise ValueError(f"Parameter 'domain' cannot be empty")
1308
1309     # DEBUG: print("DEBUG: Fetching friendica blocks from domain:", domain)
1310     blocks = []
1311
1312     try:
1313         doc = bs4.BeautifulSoup(
1314             reqto.get(f"https://{domain}/friendica", headers=headers, timeout=(config["connection_timeout"], config["read_timeout"])).text,
1315             "html.parser",
1316         )
1317     except BaseException as e:
1318         print("WARNING: Failed to fetch /friendica from domain:", domain, e)
1319         update_last_error(domain, e)
1320         return {}
1321
1322     blocklist = doc.find(id="about_blocklist")
1323
1324     # Prevents exceptions:
1325     if blocklist is None:
1326         # DEBUG: print("DEBUG: Instance has no block list:", domain)
1327         return {}
1328
1329     for line in blocklist.find("table").find_all("tr")[1:]:
1330         # DEBUG: print(f"DEBUG: line='{line}'")
1331         blocks.append({
1332             "domain": tidyup(line.find_all("td")[0].text),
1333             "reason": tidyup(line.find_all("td")[1].text)
1334         })
1335
1336     # DEBUG: print("DEBUG: Returning blocks() for domain:", domain, len(blocks))
1337     return {
1338         "reject": blocks
1339     }
1340
1341 def get_misskey_blocks(domain: str) -> dict:
1342     if type(domain) != str:
1343         raise ValueError(f"Parameter domain[]={type(domain)} is not 'str'")
1344     elif domain == "":
1345         raise ValueError(f"Parameter 'domain' cannot be empty")
1346
1347     # DEBUG: print("DEBUG: Fetching misskey blocks from domain:", domain)
1348     blocks = {
1349         "suspended": [],
1350         "blocked"  : []
1351     }
1352
1353     offset = 0
1354     step = config["misskey_offset"]
1355     while True:
1356         # iterating through all "suspended" (follow-only in its terminology)
1357         # instances page-by-page, since that troonware doesn't support
1358         # sending them all at once
1359         try:
1360             # DEBUG: print(f"DEBUG: Fetching offset='{offset}' from '{domain}' ...")
1361             if offset == 0:
1362                 # DEBUG: print("DEBUG: Sending JSON API request to domain,step,offset:", domain, step, offset)
1363                 fetched = post_json_api(domain, "/api/federation/instances", json.dumps({
1364                     "sort"     : "+pubAt",
1365                     "host"     : None,
1366                     "suspended": True,
1367                     "limit"    : step
1368                 }), {"Origin": domain})
1369             else:
1370                 # DEBUG: print("DEBUG: Sending JSON API request to domain,step,offset:", domain, step, offset)
1371                 fetched = post_json_api(domain, "/api/federation/instances", json.dumps({
1372                     "sort"     : "+pubAt",
1373                     "host"     : None,
1374                     "suspended": True,
1375                     "limit"    : step,
1376                     "offset"   : offset - 1
1377                 }), {"Origin": domain})
1378
1379             # DEBUG: print("DEBUG: fetched():", len(fetched))
1380             if len(fetched) == 0:
1381                 # DEBUG: print("DEBUG: Returned zero bytes, exiting loop:", domain)
1382                 break
1383             elif len(fetched) != config["misskey_offset"]:
1384                 # DEBUG: print(f"DEBUG: Fetched '{len(fetched)}' row(s) but expected: '{config['misskey_offset']}'")
1385                 offset = offset + (config["misskey_offset"] - len(fetched))
1386             else:
1387                 # DEBUG: print("DEBUG: Raising offset by step:", step)
1388                 offset = offset + step
1389
1390             for instance in fetched:
1391                 # just in case
1392                 if instance["isSuspended"]:
1393                     blocks["suspended"].append(
1394                         {
1395                             "domain": tidyup(instance["host"]),
1396                             # no reason field, nothing
1397                             "reason": None
1398                         }
1399                     )
1400
1401         except BaseException as e:
1402             print("WARNING: Caught error, exiting loop:", domain, e)
1403             update_last_error(domain, e)
1404             offset = 0
1405             break
1406
1407     while True:
1408         # same shit, different asshole ("blocked" aka full suspend)
1409         try:
1410             if offset == 0:
1411                 # DEBUG: print("DEBUG: Sending JSON API request to domain,step,offset:", domain, step, offset)
1412                 fetched = post_json_api(domain,"/api/federation/instances", json.dumps({
1413                     "sort"   : "+pubAt",
1414                     "host"   : None,
1415                     "blocked": True,
1416                     "limit"  : step
1417                 }), {"Origin": domain})
1418             else:
1419                 # DEBUG: print("DEBUG: Sending JSON API request to domain,step,offset:", domain, step, offset)
1420                 fetched = post_json_api(domain,"/api/federation/instances", json.dumps({
1421                     "sort"   : "+pubAt",
1422                     "host"   : None,
1423                     "blocked": True,
1424                     "limit"  : step,
1425                     "offset" : offset-1
1426                 }), {"Origin": domain})
1427
1428             # DEBUG: print("DEBUG: fetched():", len(fetched))
1429             if len(fetched) == 0:
1430                 # DEBUG: print("DEBUG: Returned zero bytes, exiting loop:", domain)
1431                 break
1432             elif len(fetched) != config["misskey_offset"]:
1433                 # DEBUG: print(f"DEBUG: Fetched '{len(fetched)}' row(s) but expected: '{config['misskey_offset']}'")
1434                 offset = offset + (config["misskey_offset"] - len(fetched))
1435             else:
1436                 # DEBUG: print("DEBUG: Raising offset by step:", step)
1437                 offset = offset + step
1438
1439             for instance in fetched:
1440                 if instance["isBlocked"]:
1441                     blocks["blocked"].append({
1442                         "domain": tidyup(instance["host"]),
1443                         "reason": None
1444                     })
1445
1446         except BaseException as e:
1447             print("ERROR: Exception during POST:", domain, e)
1448             update_last_error(domain, e)
1449             offset = 0
1450             break
1451
1452     # DEBUG: print(f"DEBUG: Updating last_instance_fetch for domain='{domain}' ...")
1453     update_last_instance_fetch(domain)
1454
1455     # DEBUG: print("DEBUG: Returning for domain,blocked(),suspended():", domain, len(blocks["blocked"]), len(blocks["suspended"]))
1456     return {
1457         "reject"        : blocks["blocked"],
1458         "followers_only": blocks["suspended"]
1459     }
1460
1461 def tidyup(string: str) -> str:
1462     if type(string) != str:
1463         raise ValueError(f"Parameter string[]={type(string)} is not expected")
1464
1465     # some retards put their blocks in variable case
1466     string = string.lower().strip()
1467
1468     # other retards put the port
1469     string = re.sub("\:\d+$", "", string)
1470
1471     # bigger retards put the schema in their blocklist, sometimes even without slashes
1472     string = re.sub("^https?\:(\/*)", "", string)
1473
1474     # and trailing slash
1475     string = re.sub("\/$", "", string)
1476
1477     # and the @
1478     string = re.sub("^\@", "", string)
1479
1480     # the biggest retards of them all try to block individual users
1481     string = re.sub("(.+)\@", "", string)
1482
1483     return string