]> git.mxchange.org Git - fba.git/blob - fba/instances.py
c872c2b96832c47bf74ef85ac969ff27f0028983
[fba.git] / fba / instances.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 json
18 import sys
19 import time
20
21 import requests
22 import validators
23
24 from fba import blacklist
25 from fba import cache
26 from fba import fba
27 from fba import federation
28
29 # Found info from node, such as nodeinfo URL, detection mode that needs to be
30 # written to database. Both arrays must be filled at the same time or else
31 # update_data() will fail
32 _pending = {
33     # Detection mode: 'AUTO_DISCOVERY', 'STATIC_CHECKS' or 'GENERATOR'
34     # NULL means all detection methods have failed (maybe still reachable instance)
35     "detection_mode"     : {},
36     # Found nodeinfo URL
37     "nodeinfo_url"       : {},
38     # Found total peers
39     "total_peers"        : {},
40     # Last fetched instances
41     "last_instance_fetch": {},
42     # Last updated
43     "last_updated"       : {},
44     # Last blocked
45     "last_blocked"       : {},
46     # Last nodeinfo (fetched)
47     "last_nodeinfo"      : {},
48     # Last status code
49     "last_status_code"   : {},
50     # Last error details
51     "last_error_details" : {},
52     # Whether CSRF tokens are present
53     "has_csrf"           : {},
54 }
55
56 def set_data(key: str, domain: str, value: any):
57     # DEBUG: print(f"DEBUG: key='{key}',domain='{domain}',value[]='{type(value)}' - CALLED!")
58     if not isinstance(key, str):
59         raise ValueError("Parameter key[]='{type(key)}' is not 'str'")
60     elif key == "":
61         raise ValueError("Parameter 'key' is empty")
62     elif not isinstance(domain, str):
63         raise ValueError(f"Parameter domain[]='{type(domain)}' is not 'str'")
64     elif domain == "":
65         raise ValueError("Parameter 'domain' is empty")
66     elif not key in _pending:
67         raise ValueError(f"key='{key}' not found in _pending")
68     elif not fba.is_primitive(value):
69         raise ValueError(f"value[]='{type(value)}' is not a primitive type")
70
71     # Set it
72     _pending[key][domain] = value
73
74     # DEBUG: print("DEBUG: EXIT!")
75
76 def has_pending_instance_data(domain: str) -> bool:
77     # DEBUG: print(f"DEBUG: domain='{domain}' - CALLED!")
78     if not isinstance(domain, str):
79         raise ValueError(f"Parameter domain[]={type(domain)} is not 'str'")
80     elif domain == "":
81         raise ValueError("Parameter 'domain' is empty")
82
83     has_pending = False
84     for key in _pending:
85         # DEBUG: print(f"DEBUG: key='{key}',domain='{domain}',_pending[key]()='{len(_pending[key])}'")
86         if domain in _pending[key]:
87             has_pending = True
88             break
89
90     # DEBUG: print(f"DEBUG: has_pending='{has_pending}' - EXIT!")
91     return has_pending
92
93 def update_data(domain: str):
94     # DEBUG: print(f"DEBUG: domain='{domain}' - CALLED!")
95     if not isinstance(domain, str):
96         raise ValueError(f"Parameter domain[]={type(domain)} is not 'str'")
97     elif domain == "":
98         raise ValueError("Parameter 'domain' is empty")
99     elif not has_pending_instance_data(domain):
100         raise Exception(f"Domain '{domain}' has no pending instance data, but function invoked")
101
102     # DEBUG: print(f"DEBUG: Updating instance data for domain='{domain}' ...")
103     sql_string = ""
104     fields = list()
105     for key in _pending:
106         # DEBUG: print("DEBUG: key:", key)
107         if domain in _pending[key]:
108             # DEBUG: print(f"DEBUG: Adding '{_pending[key][domain]}' for key='{key}' ...")
109             fields.append(_pending[key][domain])
110             sql_string += f" {key} = ?,"
111
112     fields.append(time.time())
113     fields.append(domain)
114
115     if sql_string == "":
116         raise ValueError(f"No fields have been set, but method invoked, domain='{domain}'")
117
118     # DEBUG: print(f"DEBUG: sql_string='{sql_string}',fields()={len(fields)}")
119     sql_string = "UPDATE instances SET" + sql_string + " last_updated = ? WHERE domain = ? LIMIT 1"
120     # DEBUG: print("DEBUG: sql_string:", sql_string)
121
122     try:
123         # DEBUG: print("DEBUG: Executing SQL:", sql_string)
124         fba.cursor.execute(sql_string, fields)
125
126         # DEBUG: print(f"DEBUG: Success! (rowcount={fba.cursor.rowcount })")
127         if fba.cursor.rowcount == 0:
128             # DEBUG: print(f"DEBUG: Did not update any rows: domain='{domain}',fields()={len(fields)} - EXIT!")
129             return
130
131         # DEBUG: print("DEBUG: Committing changes ...")
132         fba.connection.commit()
133
134         # DEBUG: print("DEBUG: Deleting _pending for domain:", domain)
135         for key in _pending:
136             try:
137                 # DEBUG: print("DEBUG: Deleting key:", key)
138                 del _pending[key][domain]
139             except:
140                 pass
141
142     except BaseException as exception:
143         print(f"ERROR: failed SQL query: domain='{domain}',sql_string='{sql_string}',exception[{type(exception)}]:'{str(exception)}'")
144         sys.exit(255)
145
146     # DEBUG: print("DEBUG: EXIT!")
147
148 def update_last_instance_fetch(domain: str):
149     # DEBUG: print(f"DEBUG: domain='{domain}' - CALLED!")
150     if not isinstance(domain, str):
151         raise ValueError(f"Parameter domain[]={type(domain)} is not 'str'")
152     elif domain == "":
153         raise ValueError("Parameter 'domain' is empty")
154
155     # DEBUG: print("DEBUG: Updating last_instance_fetch for domain:", domain)
156     set_data("last_instance_fetch", domain, time.time())
157
158     # Running pending updated
159     # DEBUG: print(f"DEBUG: Invoking update_data({domain}) ...")
160     update_data(domain)
161
162     # DEBUG: print("DEBUG: EXIT!")
163
164 def update_last_blocked(domain: str):
165     if not isinstance(domain, str):
166         raise ValueError(f"Parameter domain[]={type(domain)} is not 'str'")
167     elif domain == "":
168         raise ValueError("Parameter 'domain' is empty")
169
170     # DEBUG: print("DEBUG: Updating last_blocked for domain", domain)
171     set_data("last_blocked", domain, time.time())
172
173     # Running pending updated
174     # DEBUG: print(f"DEBUG: Invoking update_data({domain}) ...")
175     update_data(domain)
176
177     # DEBUG: print("DEBUG: EXIT!")
178
179 def add(domain: str, origin: str, command: str, path: str = None):
180     # DEBUG: print(f"DEBUG: domain='{domain}',origin='{origin}',command='{command}',path='{path}' - CALLED!")
181     if not isinstance(domain, str):
182         raise ValueError(f"Parameter domain[]={type(domain)} is not 'str'")
183     elif domain == "":
184         raise ValueError("Parameter 'domain' is empty")
185     elif not isinstance(origin, str) and origin is not None:
186         raise ValueError(f"origin[]={type(origin)} is not 'str'")
187     elif origin == "":
188         raise ValueError("Parameter 'origin' is empty")
189     elif not isinstance(command, str):
190         raise ValueError(f"command[]={type(command)} is not 'str'")
191     elif command == "":
192         raise ValueError("Parameter 'command' is empty")
193     elif not validators.domain(domain.split("/")[0]):
194         raise ValueError(f"Bad domain name='{domain}'")
195     elif origin is not None and not validators.domain(origin.split("/")[0]):
196         raise ValueError(f"Bad origin name='{origin}'")
197     elif blacklist.is_blacklisted(domain):
198         raise Exception(f"domain='{domain}' is blacklisted, but method invoked")
199     elif domain.find("/profile/") > 0 or domain.find("/users/") > 0:
200         raise Exception(f"domain='{domain}' is a single user")
201
202     # DEBUG: print("DEBUG: domain,origin,command,path:", domain, origin, command, path)
203     software = federation.determine_software(domain, path)
204
205     # DEBUG: print("DEBUG: Determined software:", software)
206     if software == "lemmy" and domain.find("/c/") > 0:
207         domain = domain.split("/c/")[0]
208         if is_registered(domain):
209             print(f"WARNING: domain='{domain}' already registered after cutting off user part. - EXIT!")
210             return
211
212     print(f"INFO: Adding instance domain='{domain}' (origin='{origin}',software='{software}')")
213     try:
214         fba.cursor.execute(
215             "INSERT INTO instances (domain, origin, command, hash, software, first_seen) VALUES (?, ?, ?, ?, ?, ?)",
216             (
217                domain,
218                origin,
219                command,
220                fba.get_hash(domain),
221                software,
222                time.time()
223             ),
224         )
225
226         cache.set_sub_key("is_registered", domain, True)
227
228         if has_pending_instance_data(domain):
229             # DEBUG: print(f"DEBUG: domain='{domain}' has pending nodeinfo being updated ...")
230             set_data("last_status_code"  , domain, None)
231             set_data("last_error_details", domain, None)
232             update_data(domain)
233
234     except BaseException as exception:
235         update_last_error(domain, exception)
236         raise Exception(f"ERROR: failed SQL query: domain='{domain}',exception[{type(exception)}]:'{str(exception)}'") from exception
237     else:
238         # DEBUG: print("DEBUG: Updating nodeinfo for domain:", domain)
239         update_last_nodeinfo(domain)
240
241     # DEBUG: print("DEBUG: EXIT!")
242
243 def update_last_nodeinfo(domain: str):
244     # DEBUG: print(f"DEBUG: domain='{domain}' - CALLED!")
245     if not isinstance(domain, str):
246         raise ValueError(f"Parameter domain[]={type(domain)} is not 'str'")
247     elif domain == "":
248         raise ValueError("Parameter 'domain' is empty")
249
250     # DEBUG: print("DEBUG: Updating last_nodeinfo for domain:", domain)
251     set_data("last_nodeinfo", domain, time.time())
252     set_data("last_updated" , domain, time.time())
253
254     # Running pending updated
255     # DEBUG: print(f"DEBUG: Invoking update_data({domain}) ...")
256     update_data(domain)
257
258     # DEBUG: print("DEBUG: EXIT!")
259
260 def update_last_error(domain: str, response: requests.models.Response):
261     print("DEBUG: domain,response[]:", domain, type(response))
262     if not isinstance(domain, str):
263         raise ValueError(f"Parameter domain[]={type(domain)} is not 'str'")
264     elif domain == "":
265         raise ValueError("Parameter 'domain' is empty")
266
267     print("DEBUG: BEFORE response[]:", type(response))
268     if isinstance(response, BaseException) or isinstance(response, json.decoder.JSONDecodeError):
269         response = f"response[{type(response)}]='{str(response)}'"
270
271     print("DEBUG: AFTER response[]:", type(response))
272     if isinstance(response, str):
273         print(f"DEBUG: Setting last_error_details='{response}'")
274         set_data("last_status_code"  , domain, 999)
275         set_data("last_error_details", domain, response)
276     else:
277         print(f"DEBUG: Setting last_error_details='{response.reason}'")
278         set_data("last_status_code"  , domain, response.status_code)
279         set_data("last_error_details", domain, response.reason)
280
281     # Running pending updated
282     print(f"DEBUG: Invoking update_data({domain}) ...")
283     update_data(domain)
284
285     fba.log_error(domain, response)
286
287     print("DEBUG: EXIT!")
288
289 def is_registered(domain: str) -> bool:
290     # DEBUG: print(f"DEBUG: domain='{domain}' - CALLED!")
291     if not isinstance(domain, str):
292         raise ValueError(f"Parameter domain[]={type(domain)} is not 'str'")
293     elif domain == "":
294         raise ValueError("Parameter 'domain' is empty")
295
296     # DEBUG: print(f"DEBUG: domain='{domain}' - CALLED!")
297     if not cache.key_exists("is_registered"):
298         # DEBUG: print("DEBUG: Cache for 'is_registered' not initialized, fetching all rows ...")
299         try:
300             fba.cursor.execute("SELECT domain FROM instances")
301
302             # Check Set all
303             cache.set_all("is_registered", fba.cursor.fetchall(), True)
304         except BaseException as exception:
305             update_last_error(domain, exception)
306             raise Exception(f"ERROR: failed SQL query: domain='{domain}',exception[{type(exception)}]:'{str(exception)}'") from exception
307
308     # Is cache found?
309     registered = cache.sub_key_exists("is_registered", domain)
310
311     # DEBUG: print(f"DEBUG: registered='{registered}' - EXIT!")
312     return registered