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