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