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