]> git.mxchange.org Git - fba.git/blob - fba/networks/lemmy.py
5c891748347b4ff2c77c6774a8db3f0dae15bd9e
[fba.git] / fba / networks / lemmy.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 inspect
18 import logging
19
20 import bs4
21 import validators
22
23 from fba import csrf
24 from fba import fba
25
26 from fba.helpers import blacklist
27 from fba.helpers import config
28 from fba.helpers import tidyup
29
30 from fba.http import federation
31 from fba.http import network
32
33 from fba.models import blocks
34 from fba.models import instances
35
36 logging.basicConfig(level=logging.INFO)
37 logger = logging.getLogger(__name__)
38
39 def fetch_peers(domain: str) -> list:
40     logger.debug("domain(%d)='%s' - CALLED!", len(domain), domain)
41     if not isinstance(domain, str):
42         raise ValueError(f"Parameter domain[]='{type(domain)}' is not 'str'")
43     elif domain == "":
44         raise ValueError("Parameter 'domain' is empty")
45     elif domain.lower() != domain:
46         raise ValueError(f"Parameter domain='{domain}' must be all lower-case")
47     elif not validators.domain(domain.split("/")[0]):
48         raise ValueError(f"domain='{domain}' is not a valid domain")
49     elif domain.endswith(".arpa"):
50         raise ValueError(f"domain='{domain}' is a domain for reversed IP addresses, please don't crawl them!")
51     elif domain.endswith(".tld"):
52         raise ValueError(f"domain='{domain}' is a fake domain, please don't crawl them!")
53
54     peers = list()
55
56     # No CSRF by default, you don't have to add network.api_headers by yourself here
57     headers = tuple()
58
59     try:
60         logger.debug("Checking CSRF for domain='%s'", domain)
61         headers = csrf.determine(domain, dict())
62     except network.exceptions as exception:
63         logger.warning(f"Exception '{type(exception)}' during checking CSRF (fetch_peers,{__name__}) - EXIT!")
64         instances.set_last_error(domain, exception)
65         return peers
66
67     try:
68         logger.debug(f"domain='{domain}' is Lemmy, fetching JSON ...")
69         data = network.get_json_api(
70             domain,
71             "/api/v3/site",
72             headers,
73             (config.get("connection_timeout"), config.get("read_timeout"))
74         )
75
76         logger.debug("data[]='%s'", type(data))
77         if "error_message" in data:
78             logger.warning("Could not reach any JSON API:", domain)
79             instances.set_last_error(domain, data)
80         elif "federated_instances" in data["json"] and isinstance(data["json"]["federated_instances"], dict):
81             logger.debug(f"Found federated_instances for domain='{domain}'")
82             peers = peers + federation.add_peers(data["json"]["federated_instances"])
83             logger.debug("Added instance(s) to peers")
84         else:
85             logger.warning("JSON response does not contain 'federated_instances', domain='%s'", domain)
86             instances.set_last_error(domain, data)
87
88     except network.exceptions as exception:
89         logger.warning(f"Exception during fetching JSON: domain='{domain}',exception[{type(exception)}]:'{str(exception)}'")
90         instances.set_last_error(domain, exception)
91
92     logger.debug(f"Adding '{len(peers)}' for domain='{domain}'")
93     instances.set_total_peers(domain, peers)
94
95     logger.debug("Returning peers[]:", type(peers))
96     return peers
97
98 def fetch_blocks(domain: str, origin: str, nodeinfo_url: str):
99     logger.debug(f"domain='{domain}',origin='{origin}',nodeinfo_url='{nodeinfo_url}' - CALLED!")
100     if not isinstance(domain, str):
101         raise ValueError(f"Parameter domain[]='{type(domain)}' is not 'str'")
102     elif domain == "":
103         raise ValueError("Parameter 'domain' is empty")
104     elif domain.lower() != domain:
105         raise ValueError(f"Parameter domain='{domain}' must be all lower-case")
106     elif not validators.domain(domain.split("/")[0]):
107         raise ValueError(f"domain='{domain}' is not a valid domain")
108     elif domain.endswith(".arpa"):
109         raise ValueError(f"domain='{domain}' is a domain for reversed IP addresses, please don't crawl them!")
110     elif domain.endswith(".tld"):
111         raise ValueError(f"domain='{domain}' is a fake domain, please don't crawl them!")
112     elif not isinstance(origin, str) and origin is not None:
113         raise ValueError(f"Parameter origin[]='{type(origin)}' is not 'str'")
114     elif origin == "":
115         raise ValueError("Parameter 'origin' is empty")
116     elif not isinstance(nodeinfo_url, str):
117         raise ValueError(f"Parameter nodeinfo_url[]='{type(nodeinfo_url)}' is not 'str'")
118     elif nodeinfo_url == "":
119         raise ValueError("Parameter 'nodeinfo_url' is empty")
120
121     translations = [
122         "Blocked Instances",
123         "Instàncies bloquejades",
124         "Blocáilte Ásc",
125         "封锁实例",
126         "Blokované instance",
127         "Geblokkeerde instanties",
128         "Blockerade instanser",
129         "Instàncias blocadas",
130         "Istanze bloccate",
131         "Instances bloquées",
132         "Letiltott példányok",
133         "Instancias bloqueadas",
134         "Blokeatuta dauden instantziak",
135         "차단된 인스턴스",
136         "Peladen Yang Diblokir",
137         "Blokerede servere",
138         "Blokitaj nodoj",
139         "Блокирани Инстанции",
140         "Blockierte Instanzen",
141         "Estetyt instanssit",
142         "Instâncias bloqueadas",
143         "Zablokowane instancje",
144         "Blokované inštancie",
145         "المثلاء المحجوبون",
146         "Užblokuoti serveriai",
147         "ブロックしたインスタンス",
148         "Блокированные Инстансы",
149         "Αποκλεισμένοι διακομιστές",
150         "封鎖站台",
151         "Instâncias bloqueadas",
152     ]
153
154     try:
155         # json endpoint for newer mastodongs
156         found_blocks = list()
157         blocklist = list()
158
159         rows = {
160             "reject"        : [],
161             "media_removal" : [],
162             "followers_only": [],
163             "report_removal": [],
164         }
165
166         logger.debug(f"Fetching /instances from domain='{domain}'")
167         response = network.fetch_response(
168             domain,
169             "/instances",
170             network.web_headers,
171             (config.get("connection_timeout"), config.get("read_timeout"))
172         )
173
174         logger.debug(f"response.ok='{response.ok}',response.status_code={response.status_code},response.text()={len(response.text)}")
175         if response.ok and response.status_code < 300 and response.text != "":
176             logger.debug(f"Parsing {len(response.text)} Bytes ...")
177
178             doc = bs4.BeautifulSoup(response.text, "html.parser")
179             logger.debug(f"doc[]={type(doc)}")
180
181             headers = doc.findAll("h5")
182             found = None
183             logger.debug(f"Search in {len(headers)} header(s) ...")
184             for header in headers:
185                 logger.debug(f"header[]={type(header)}")
186                 content = header.contents[0]
187
188                 logger.debug(f"content='{content}'")
189                 if content in translations:
190                     logger.debug("Found header with blocked instances - BREAK!")
191                     found = header
192                     break
193
194             logger.debug(f"found[]='{type(found)}'")
195             if found is None:
196                 logger.debug(f"domain='{domain}' is not blocking any instances - EXIT!")
197                 return
198
199             blocking = found.find_next("ul").findAll("a")
200             logger.debug(f"Found {len(blocking)} blocked instance(s) ...")
201             for tag in blocking:
202                 logger.debug(f"tag[]='{type(tag)}'")
203                 blocked = tidyup.domain(tag.contents[0])
204
205                 logger.debug(f"blocked='{blocked}'")
206                 if not validators.domain(blocked):
207                     logger.warning(f"blocked='{blocked}' is not a valid domain - SKIPPED!")
208                     continue
209                 elif blocked.endswith(".arpa"):
210                     logger.warning(f"blocked='{blocked}' is a reversed .arpa domain and should not be used generally.")
211                     continue
212                 elif blocked.endswith(".tld"):
213                     logger.warning(f"blocked='{blocked}' is a fake domain, please don't crawl them!")
214                     continue
215                 elif blacklist.is_blacklisted(blocked):
216                     logger.debug("blocked='%s' is blacklisted - SKIPPED!", blocked)
217                     continue
218                 elif not instances.is_registered(blocked):
219                     logger.debug("Hash wasn't found, adding:", blocked, domain)
220                     instances.add(blocked, domain, inspect.currentframe().f_code.co_name, nodeinfo_url)
221
222                 if not blocks.is_instance_blocked(domain, blocked, "reject"):
223                     logger.debug("Blocking:", domain, blocked)
224                     blocks.add_instance(domain, blocked, None, "reject")
225
226                     found_blocks.append({
227                         "blocked": blocked,
228                         "reason" : None
229                     })
230                 else:
231                     logger.debug(f"Updating block last seen for domain='{domain}',blocked='{blocked}' ...")
232                     blocks.update_last_seen(domain, blocked, "reject")
233
234         logger.debug("Committing changes ...")
235         fba.connection.commit()
236     except network.exceptions as exception:
237         logger.warning(f"domain='{domain}',exception[{type(exception)}]:'{str(exception)}'")
238         instances.set_last_error(domain, exception)
239
240     logger.debug("EXIT!")