1 # Copyright (C) 2023 Free Software Foundation
3 # This program is free software: you can redistribute it and/or modify
4 # it under the terms of the GNU Affero General Public License as published
5 # by the Free Software Foundation, either version 3 of the License, or
6 # (at your option) any later version.
8 # This program is distributed in the hope that it will be useful,
9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # GNU Affero General Public License for more details.
13 # You should have received a copy of the GNU Affero General Public License
14 # along with this program. If not, see <https://www.gnu.org/licenses/>.
18 from urllib.parse import urlparse
26 from fba.helpers import config
27 from fba.helpers import tidyup
28 from fba.helpers import version
30 from fba.http import network
32 from fba.models import instances
34 from fba.networks import lemmy
35 from fba.networks import misskey
36 from fba.networks import peertube
38 logging.basicConfig(level=logging.INFO)
39 logger = logging.getLogger(__name__)
41 # "rel" identifiers (no real URLs)
42 nodeinfo_identifier = [
43 "https://nodeinfo.diaspora.software/ns/schema/2.1",
44 "https://nodeinfo.diaspora.software/ns/schema/2.0",
45 "https://nodeinfo.diaspora.software/ns/schema/1.1",
46 "https://nodeinfo.diaspora.software/ns/schema/1.0",
47 "http://nodeinfo.diaspora.software/ns/schema/2.1",
48 "http://nodeinfo.diaspora.software/ns/schema/2.0",
49 "http://nodeinfo.diaspora.software/ns/schema/1.1",
50 "http://nodeinfo.diaspora.software/ns/schema/1.0",
53 def fetch_instances(domain: str, origin: str, software: str, command: str, path: str = None):
54 logger.debug(f"domain='{domain}',origin='{origin}',software='{software}',path='{path}' - CALLED!")
55 if not isinstance(domain, str):
56 raise ValueError(f"Parameter domain[]='{type(domain)}' is not 'str'")
58 raise ValueError("Parameter 'domain' is empty")
59 elif domain.lower() != domain:
60 raise ValueError(f"Parameter domain='{domain}' must be all lower-case")
61 elif not validators.domain(domain.split("/")[0]):
62 raise ValueError(f"domain='{domain}' is not a valid domain")
63 elif domain.endswith(".arpa"):
64 raise ValueError(f"domain='{domain}' is a domain for reversed IP addresses, please don't crawl them!")
65 elif domain.endswith(".tld"):
66 raise ValueError(f"domain='{domain}' is a fake domain, please don't crawl them!")
67 elif not isinstance(origin, str) and origin is not None:
68 raise ValueError(f"Parameter origin[]='{type(origin)}' is not 'str'")
69 elif software is None:
70 logger.debug(f"Updating last_instance_fetch for domain='{domain}' ...")
71 instances.set_last_instance_fetch(domain)
73 logger.debug(f"software for domain='{domain}' is not set, determining ...")
76 software = determine_software(domain, path)
77 except network.exceptions as exception:
78 logger.warning("Exception '%s' during determining software type", type(exception))
79 instances.set_last_error(domain, exception)
81 logger.debug(f"Determined software='{software}' for domain='{domain}'")
82 elif not isinstance(software, str):
83 raise ValueError(f"Parameter software[]='{type(software)}' is not 'str'")
84 elif not isinstance(command, str):
85 raise ValueError(f"Parameter command[]='{type(command)}' is not 'str'")
87 raise ValueError("Parameter 'command' is empty")
88 elif not validators.domain(domain.split("/")[0]):
89 raise ValueError(f"domain='{domain}' is not a valid domain")
90 elif domain.endswith(".arpa"):
91 raise ValueError(f"domain='{domain}' is a domain for reversed IP addresses, please don't crawl them!")
92 elif domain.endswith(".tld"):
93 raise ValueError(f"domain='{domain}' is a fake domain")
95 if not instances.is_registered(domain):
96 logger.debug(f"Adding new domain='{domain}',origin='{origin}',command='{command}',path='{path}',software='{software}'")
97 instances.add(domain, origin, command, path, software)
99 logger.debug(f"Updating last_instance_fetch for domain='{domain}' ...")
100 instances.set_last_instance_fetch(domain)
102 logger.debug("Fetching instances for domain:", domain, software)
103 peerlist = fetch_peers(domain, software)
106 logger.warning("Cannot fetch peers:", domain)
108 elif instances.has_pending(domain):
109 logger.debug(f"domain='{domain}' has pending nodeinfo data, flushing ...")
110 instances.update_data(domain)
112 logger.info("Checking %d instances from domain='%s' ...", len(peerlist), domain)
113 for instance in peerlist:
114 logger.debug(f"instance='{instance}'")
116 # Skip "None" types as tidup.domain() cannot parse them
119 logger.debug(f"instance='{instance}' - BEFORE")
120 instance = tidyup.domain(instance)
121 logger.debug(f"instance='{instance}' - AFTER")
124 logger.warning(f"Empty instance after tidyup.domain(), domain='{domain}'")
126 elif not utils.is_domain_wanted(instance):
127 logger.debug("instance='%s' is not wanted - SKIPPED!", instance)
129 elif instance.find("/profile/") > 0 or instance.find("/users/") > 0:
130 logger.debug("instance='%s' is a link to a single user profile - SKIPPED!", instance)
132 elif not instances.is_registered(instance):
133 logger.debug("Adding new instance:", instance, domain)
134 instances.add(instance, domain, command)
136 logger.debug("EXIT!")
138 def fetch_peers(domain: str, software: str) -> list:
139 logger.debug(f"domain({len(domain)})='{domain}',software='{software}' - CALLED!")
140 if not isinstance(domain, str):
141 raise ValueError(f"Parameter domain[]='{type(domain)}' is not 'str'")
143 raise ValueError("Parameter 'domain' is empty")
144 elif domain.lower() != domain:
145 raise ValueError(f"Parameter domain='{domain}' must be all lower-case")
146 elif not validators.domain(domain.split("/")[0]):
147 raise ValueError(f"domain='{domain}' is not a valid domain")
148 elif domain.endswith(".arpa"):
149 raise ValueError(f"domain='{domain}' is a domain for reversed IP addresses, please don't crawl them!")
150 elif domain.endswith(".tld"):
151 raise ValueError(f"domain='{domain}' is a fake domain, please don't crawl them!")
152 elif not isinstance(software, str) and software is not None:
153 raise ValueError(f"software[]='{type(software)}' is not 'str'")
155 if software == "misskey":
156 logger.debug(f"Invoking misskey.fetch_peers({domain}) ...")
157 return misskey.fetch_peers(domain)
158 elif software == "lemmy":
159 logger.debug(f"Invoking lemmy.fetch_peers({domain}) ...")
160 return lemmy.fetch_peers(domain)
161 elif software == "peertube":
162 logger.debug(f"Invoking peertube.fetch_peers({domain}) ...")
163 return peertube.fetch_peers(domain)
165 # Init peers variable
168 # No CSRF by default, you don't have to add network.api_headers by yourself here
172 logger.debug("Checking CSRF for domain='%s'", domain)
173 headers = csrf.determine(domain, dict())
174 except network.exceptions as exception:
175 logger.warning(f"Exception '{type(exception)}' during checking CSRF (fetch_peers,{__name__}) - EXIT!")
176 instances.set_last_error(domain, exception)
179 logger.debug(f"Fetching peers from '{domain}',software='{software}' ...")
180 data = network.get_json_api(
182 "/api/v1/instance/peers",
184 (config.get("connection_timeout"), config.get("read_timeout"))
187 logger.debug("data[]='%s'", type(data))
188 if "error_message" in data:
189 logger.debug("Was not able to fetch peers, trying alternative ...")
190 data = network.get_json_api(
194 (config.get("connection_timeout"), config.get("read_timeout"))
197 logger.debug("data[]='%s'", type(data))
198 if "error_message" in data:
199 logger.warning(f"Could not reach any JSON API at domain='{domain}',status_code='{data['status_code']}',error_message='{data['error_message']}'")
200 elif "federated_instances" in data["json"]:
201 logger.debug(f"Found federated_instances for domain='{domain}'")
202 peers = peers + add_peers(data["json"]["federated_instances"])
203 logger.debug("Added instance(s) to peers")
205 message = "JSON response does not contain 'federated_instances' or 'error_message'"
206 logger.warning("message='%s',domain='%s'", message, domain)
207 instances.set_last_error(domain, message)
208 elif isinstance(data["json"], list):
209 logger.debug("Querying API was successful: domain='%s',data[json]()=%d", domain, len(data['json']))
212 logger.warning("Cannot parse data[json][]='%s'", type(data['json']))
214 logger.debug(f"Adding '{len(peers)}' for domain='{domain}'")
215 instances.set_total_peers(domain, peers)
217 logger.debug("Returning peers[]:", type(peers))
220 def fetch_nodeinfo(domain: str, path: str = None) -> dict:
221 logger.debug(f"domain='{domain}',path='{path}' - CALLED!")
222 if not isinstance(domain, str):
223 raise ValueError(f"Parameter domain[]='{type(domain)}' is not 'str'")
225 raise ValueError("Parameter 'domain' is empty")
226 elif domain.lower() != domain:
227 raise ValueError(f"Parameter domain='{domain}' must be all lower-case")
228 elif not validators.domain(domain.split("/")[0]):
229 raise ValueError(f"domain='{domain}' is not a valid domain")
230 elif domain.endswith(".arpa"):
231 raise ValueError(f"domain='{domain}' is a domain for reversed IP addresses, please don't crawl them!")
232 elif domain.endswith(".tld"):
233 raise ValueError(f"domain='{domain}' is a fake domain, please don't crawl them!")
234 elif not isinstance(path, str) and path is not None:
235 raise ValueError(f"Parameter path[]='{type(path)}' is not 'str'")
237 logger.debug(f"Fetching nodeinfo from domain='{domain}' ...")
238 nodeinfo = fetch_wellknown_nodeinfo(domain)
240 logger.debug(f"nodeinfo[{type(nodeinfo)}]({len(nodeinfo)}='{nodeinfo}'")
241 if "error_message" not in nodeinfo and "json" in nodeinfo and len(nodeinfo["json"]) > 0:
242 logger.debug(f"Found nodeinfo[json]()={len(nodeinfo['json'])} - EXIT!")
243 return nodeinfo["json"]
245 # No CSRF by default, you don't have to add network.api_headers by yourself here
250 logger.debug("Checking CSRF for domain='%s'", domain)
251 headers = csrf.determine(domain, dict())
252 except network.exceptions as exception:
253 logger.warning(f"Exception '{type(exception)}' during checking CSRF (nodeinfo,{__name__}) - EXIT!")
254 instances.set_last_error(domain, exception)
257 "error_message": f"exception[{type(exception)}]='{str(exception)}'",
258 "exception" : exception,
262 "/nodeinfo/2.1.json",
264 "/nodeinfo/2.0.json",
270 for request in request_paths:
271 logger.debug(f"path[{type(path)}]='{path}',request='{request}'")
272 if path is None or path == request or path == f"http://{domain}{path}" or path == f"https://{domain}{path}":
273 logger.debug(f"Fetching request='{request}' from domain='{domain}' ...")
274 if path == f"http://{domain}{path}" or path == f"https://{domain}{path}":
275 logger.debug(f"domain='{domain}',path='{path}' has protocol in path, splitting ...")
276 components = urlparse(path)
277 path = components.path
279 data = network.get_json_api(
283 (config.get("nodeinfo_connection_timeout"), config.get("nodeinfo_read_timeout"))
286 logger.debug("data[]='%s'", type(data))
287 if "error_message" not in data:
288 logger.debug("Success:", request)
289 instances.set_detection_mode(domain, "STATIC_CHECK")
290 instances.set_nodeinfo_url(domain, request)
293 logger.warning(f"Failed fetching nodeinfo from domain='{domain}',status_code='{data['status_code']}',error_message='{data['error_message']}'")
295 logger.debug("data()=%d - EXIT!", len(data))
298 def fetch_wellknown_nodeinfo(domain: str) -> dict:
299 logger.debug("domain(%d)='%s' - CALLED!", len(domain), domain)
300 if not isinstance(domain, str):
301 raise ValueError(f"Parameter domain[]='{type(domain)}' is not 'str'")
303 raise ValueError("Parameter 'domain' is empty")
304 elif domain.lower() != domain:
305 raise ValueError(f"Parameter domain='{domain}' must be all lower-case")
306 elif not validators.domain(domain.split("/")[0]):
307 raise ValueError(f"domain='{domain}' is not a valid domain")
308 elif domain.endswith(".arpa"):
309 raise ValueError(f"domain='{domain}' is a domain for reversed IP addresses, please don't crawl them!")
310 elif domain.endswith(".tld"):
311 raise ValueError(f"domain='{domain}' is a fake domain, please don't crawl them!")
313 # No CSRF by default, you don't have to add network.api_headers by yourself here
317 logger.debug("Checking CSRF for domain='%s'", domain)
318 headers = csrf.determine(domain, dict())
319 except network.exceptions as exception:
320 logger.warning(f"Exception '{type(exception)}' during checking CSRF (fetch_wellknown_nodeinfo,{__name__}) - EXIT!")
321 instances.set_last_error(domain, exception)
324 "error_message": type(exception),
325 "exception" : exception,
328 logger.debug("Fetching .well-known info for domain:", domain)
329 data = network.get_json_api(
331 "/.well-known/nodeinfo",
333 (config.get("nodeinfo_connection_timeout"), config.get("nodeinfo_read_timeout"))
336 if "error_message" not in data:
337 nodeinfo = data["json"]
338 logger.debug("Found entries:", len(nodeinfo), domain)
339 if "links" in nodeinfo:
340 logger.debug("Found links in nodeinfo():", len(nodeinfo["links"]))
341 for link in nodeinfo["links"]:
342 logger.debug(f"link[{type(link)}]='{link}'")
343 if not isinstance(link, dict) or not "rel" in link:
344 logger.warning(f"link[]='{type(link)}' is not 'dict' or no element 'rel' found")
345 elif link["rel"] in nodeinfo_identifier:
346 # Default is that 'href' has a complete URL, but some hosts don't send that
348 components = urlparse(link["href"])
350 logger.debug(f"components[{type(components)}]='{components}'")
351 if components.scheme == "" and components.netloc == "":
352 logger.debug(f"link[href]='{link['href']}' has no scheme and host name in it, prepending from domain='{domain}'")
353 url = f"https://{domain}{url}"
354 components = urlparse(url)
356 if not utils.is_domain_wanted(components.netloc):
357 logger.debug("components.netloc='%s' is not wanted - SKIPPED!", components.netloc)
360 logger.debug("Fetching nodeinfo from:", url)
361 data = network.fetch_api_url(
363 (config.get("connection_timeout"), config.get("read_timeout"))
366 logger.debug("href,data[]:", link["href"], type(data))
367 if "error_message" not in data and "json" in data:
368 logger.debug("Found JSON nodeinfo():", len(data))
369 instances.set_detection_mode(domain, "AUTO_DISCOVERY")
370 instances.set_nodeinfo_url(domain, link["href"])
373 instances.set_last_error(domain, data)
375 logger.warning("Unknown 'rel' value:", domain, link["rel"])
377 logger.warning("nodeinfo does not contain 'links':", domain)
379 logger.debug("Returning data[]:", type(data))
382 def fetch_generator_from_path(domain: str, path: str = "/") -> str:
383 logger.debug("domain(%d)='%s',path='%s' - CALLED!", len(domain), domain, path)
384 if not isinstance(domain, str):
385 raise ValueError(f"Parameter domain[]='{type(domain)}' is not 'str'")
387 raise ValueError("Parameter 'domain' is empty")
388 elif domain.lower() != domain:
389 raise ValueError(f"Parameter domain='{domain}' must be all lower-case")
390 elif not validators.domain(domain.split("/")[0]):
391 raise ValueError(f"domain='{domain}' is not a valid domain")
392 elif domain.endswith(".arpa"):
393 raise ValueError(f"domain='{domain}' is a domain for reversed IP addresses, please don't crawl them!")
394 elif domain.endswith(".tld"):
395 raise ValueError(f"domain='{domain}' is a fake domain, please don't crawl them!")
396 elif not isinstance(path, str):
397 raise ValueError(f"path[]='{type(path)}' is not 'str'")
399 raise ValueError("Parameter 'path' is empty")
401 logger.debug(f"domain='{domain}',path='{path}' - CALLED!")
404 logger.debug(f"Fetching path='{path}' from '{domain}' ...")
405 response = network.fetch_response(domain, path, network.web_headers, (config.get("connection_timeout"), config.get("read_timeout")))
407 logger.debug("response.ok='%s',response.status_code=%d,response.text()=%d", response.ok, response.status_code, len(response.text))
408 if response.ok and response.status_code < 300 and response.text.find("<html") > 0:
409 logger.debug(f"Parsing response.text()={len(response.text)} Bytes ...")
411 doc = bs4.BeautifulSoup(response.text, "html.parser")
412 logger.debug("doc[]='%s'", type(doc))
414 generator = doc.find("meta", {"name" : "generator"})
415 site_name = doc.find("meta", {"property": "og:site_name"})
417 logger.debug("generator[]='%s',site_name[]='%s'", type(generator), type(site_name))
418 if isinstance(generator, bs4.element.Tag) and isinstance(generator.get("content"), str):
419 logger.debug("Found generator meta tag:", domain)
420 software = tidyup.domain(generator.get("content"))
422 logger.debug("software[%s]='%s'", type(software), software)
423 if software is not None and software != "":
424 logger.info("domain='%s' is generated by '%s'", domain, software)
425 instances.set_detection_mode(domain, "GENERATOR")
426 elif isinstance(site_name, bs4.element.Tag) and isinstance(site_name.get("content"), str):
427 logger.debug("Found property=og:site_name:", domain)
428 software = tidyup.domain(site_name.get("content"))
430 logger.debug("software[%s]='%s'", type(software), software)
431 if software is not None and software != "":
432 logger.info("domain='%s' has og:site_name='%s'", domain, software)
433 instances.set_detection_mode(domain, "SITE_NAME")
435 logger.debug("software[]='%s'", type(software))
436 if isinstance(software, str) and software == "":
437 logger.debug("Corrected empty string to None for software of domain='%s'", domain)
439 elif isinstance(software, str) and ("." in software or " " in software):
440 logger.debug(f"software='{software}' may contain a version number, domain='{domain}', removing it ...")
441 software = version.remove(software)
443 logger.debug("software[]='%s'", type(software))
444 if isinstance(software, str) and "powered by " in software:
445 logger.debug(f"software='{software}' has 'powered by' in it")
446 software = version.remove(version.strip_powered_by(software))
447 elif isinstance(software, str) and " hosted on " in software:
448 logger.debug(f"software='{software}' has 'hosted on' in it")
449 software = version.remove(version.strip_hosted_on(software))
450 elif isinstance(software, str) and " by " in software:
451 logger.debug(f"software='{software}' has ' by ' in it")
452 software = version.strip_until(software, " by ")
453 elif isinstance(software, str) and " see " in software:
454 logger.debug(f"software='{software}' has ' see ' in it")
455 software = version.strip_until(software, " see ")
457 logger.debug("software='%s' - EXIT!", software)
460 def determine_software(domain: str, path: str = None) -> str:
461 logger.debug("domain(%d)='%s',path='%s' - CALLED!", len(domain), domain, path)
462 if not isinstance(domain, str):
463 raise ValueError(f"Parameter domain[]='{type(domain)}' is not 'str'")
465 raise ValueError("Parameter 'domain' is empty")
466 elif domain.lower() != domain:
467 raise ValueError(f"Parameter domain='{domain}' must be all lower-case")
468 elif not validators.domain(domain.split("/")[0]):
469 raise ValueError(f"domain='{domain}' is not a valid domain")
470 elif domain.endswith(".arpa"):
471 raise ValueError(f"domain='{domain}' is a domain for reversed IP addresses, please don't crawl them!")
472 elif domain.endswith(".tld"):
473 raise ValueError(f"domain='{domain}' is a fake domain, please don't crawl them!")
474 elif not isinstance(path, str) and path is not None:
475 raise ValueError(f"Parameter path[]='{type(path)}' is not 'str'")
477 logger.debug("Determining software for domain,path:", domain, path)
480 logger.debug(f"Fetching nodeinfo from '{domain}' ...")
481 data = fetch_nodeinfo(domain, path)
483 logger.debug(f"data[{type(data)}]='{data}'")
484 if "exception" in data:
485 # Continue raising it
486 raise data["exception"]
487 elif "error_message" in data:
488 logger.debug(f"Returned error_message during fetching nodeinfo: '{data['error_message']}',status_code='{data['status_code']}'")
489 return fetch_generator_from_path(domain)
490 elif "status" in data and data["status"] == "error" and "message" in data:
491 logger.warning("JSON response is an error:", data["message"])
492 instances.set_last_error(domain, data["message"])
493 return fetch_generator_from_path(domain)
494 elif "message" in data:
495 logger.warning("JSON response contains only a message:", data["message"])
496 instances.set_last_error(domain, data["message"])
497 return fetch_generator_from_path(domain)
498 elif "software" not in data or "name" not in data["software"]:
499 logger.debug(f"JSON response from domain='{domain}' does not include [software][name], fetching / ...")
500 software = fetch_generator_from_path(domain)
501 logger.debug(f"Generator for domain='{domain}' is: '{software}'")
502 elif "software" in data and "name" in data["software"]:
503 logger.debug("Found data[software][name] in JSON response")
504 software = data["software"]["name"]
507 logger.debug("Returning None - EXIT!")
510 software = tidyup.domain(software)
511 logger.debug("sofware after tidyup.domain():", software)
513 if software in ["akkoma", "rebased", "akkounfucked", "ched"]:
514 logger.debug("Setting pleroma:", domain, software)
516 elif software in ["hometown", "ecko"]:
517 logger.debug("Setting mastodon:", domain, software)
518 software = "mastodon"
519 elif software in ["slipfox calckey", "calckey", "groundpolis", "foundkey", "cherrypick", "meisskey", "magnetar", "keybump"]:
520 logger.debug("Setting misskey:", domain, software)
522 elif software == "runtube.re":
523 logger.debug("Setting peertube:", domain, software)
524 software = "peertube"
525 elif software == "nextcloud social":
526 logger.debug("Setting nextcloud:", domain, software)
527 software = "nextcloud"
528 elif software.find("/") > 0:
529 logger.warning("Spliting of slash:", software)
530 software = tidyup.domain(software.split("/")[-1])
531 elif software.find("|") > 0:
532 logger.warning("Spliting of pipe:", software)
533 software = tidyup.domain(software.split("|")[0])
534 elif "powered by" in software:
535 logger.debug(f"software='{software}' has 'powered by' in it")
536 software = version.strip_powered_by(software)
537 elif isinstance(software, str) and " by " in software:
538 logger.debug(f"software='{software}' has ' by ' in it")
539 software = version.strip_until(software, " by ")
540 elif isinstance(software, str) and " see " in software:
541 logger.debug(f"software='{software}' has ' see ' in it")
542 software = version.strip_until(software, " see ")
544 logger.debug("software[]='%s'", type(software))
546 logger.warning("tidyup.domain() left no software name behind:", domain)
549 logger.debug("software[]='%s'", type(software))
550 if str(software) == "":
551 logger.debug(f"software for '{domain}' was not detected, trying generator ...")
552 software = fetch_generator_from_path(domain)
553 elif len(str(software)) > 0 and ("." in software or " " in software):
554 logger.debug(f"software='{software}' may contain a version number, domain='{domain}', removing it ...")
555 software = version.remove(software)
557 logger.debug("software[]='%s'", type(software))
558 if isinstance(software, str) and "powered by" in software:
559 logger.debug(f"software='{software}' has 'powered by' in it")
560 software = version.remove(version.strip_powered_by(software))
562 logger.debug("Returning domain,software:", domain, software)
565 def find_domains(tag: bs4.element.Tag) -> list:
566 logger.debug(f"tag[]='{type(tag)}' - CALLED!")
567 if not isinstance(tag, bs4.element.Tag):
568 raise ValueError(f"Parameter tag[]='{type(tag)}' is not type of bs4.element.Tag")
569 elif len(tag.select("tr")) == 0:
570 raise KeyError("No table rows found in table!")
573 for element in tag.select("tr"):
574 logger.debug(f"element[]='{type(element)}'")
575 if not element.find("td"):
576 logger.debug("Skipping element, no <td> found")
579 domain = tidyup.domain(element.find("td").text)
580 reason = tidyup.reason(element.findAll("td")[1].text)
582 logger.debug("domain='%s',reason='%s'", domain, reason)
584 if not utils.is_domain_wanted(domain):
585 logger.debug("domain='%s' is blacklisted - SKIPPED!", domain)
587 elif domain == "gab.com/.ai, develop.gab.com":
588 logger.debug("Multiple domains detected in one row")
598 "domain": "develop.gab.com",
602 elif not validators.domain(domain.split("/")[0]):
603 logger.warning("domain='%s' is not a valid domain - SKIPPED!", domain)
606 logger.debug(f"Adding domain='{domain}',reason='{reason}' ...")
612 logger.debug(f"domains()={len(domains)} - EXIT!")
615 def add_peers(rows: dict) -> list:
616 logger.debug(f"rows[]={type(rows)} - CALLED!")
617 if not isinstance(rows, dict):
618 raise ValueError(f"Parameter rows[]='{type(rows)}' is not 'dict'")
621 for key in ["linked", "allowed", "blocked"]:
622 logger.debug(f"Checking key='{key}'")
623 if key not in rows or rows[key] is None:
624 logger.debug(f"Cannot find key='{key}' or it is NoneType - SKIPPED!")
627 logger.debug(f"Adding {len(rows[key])} peer(s) to peers list ...")
628 for peer in rows[key]:
629 logger.debug(f"peer='{peer}' - BEFORE!")
630 if isinstance(peer, dict) and "domain" in peer:
631 logger.debug(f"peer[domain]='{peer['domain']}'")
632 peer = tidyup.domain(peer["domain"])
633 elif isinstance(peer, str):
634 logger.debug(f"peer='{peer}'")
635 peer = tidyup.domain(peer)
637 raise ValueError(f"peer[]='{type(peer)}' is not supported,key='{key}'")
639 logger.debug(f"peer='{peer}' - AFTER!")
640 if not utils.is_domain_wanted(peer):
641 logger.debug("peer='%s' is not wanted - SKIPPED!", peer)
644 logger.debug(f"Adding peer='{peer}' ...")
647 logger.debug(f"peers()={len(peers)} - EXIT!")