]> git.mxchange.org Git - fba.git/blob - fba/instances.py
Continued:
[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 from fba import network
29
30 # Found info from node, such as nodeinfo URL, detection mode that needs to be
31 # written to database. Both arrays must be filled at the same time or else
32 # update_data() will fail
33 _pending = {
34     # Detection mode: 'AUTO_DISCOVERY', 'STATIC_CHECKS' or 'GENERATOR'
35     # NULL means all detection methods have failed (maybe still reachable instance)
36     "detection_mode"     : {},
37     # Found nodeinfo URL
38     "nodeinfo_url"       : {},
39     # Found total peers
40     "total_peers"        : {},
41     # Last fetched instances
42     "last_instance_fetch": {},
43     # Last updated
44     "last_updated"       : {},
45     # Last blocked
46     "last_blocked"       : {},
47     # Last nodeinfo (fetched)
48     "last_nodeinfo"      : {},
49     # Last status code
50     "last_status_code"   : {},
51     # Last error details
52     "last_error_details" : {},
53 }
54
55 def set_data(key: str, domain: str, value: any):
56     # DEBUG: print(f"DEBUG: key='{key}',domain='{domain}',value[]='{type(value)}' - CALLED!")
57     if not isinstance(key, str):
58         raise ValueError("Parameter key[]='{type(key)}' is not 'str'")
59     elif key == "":
60         raise ValueError("Parameter 'key' is empty")
61     elif not isinstance(domain, str):
62         raise ValueError(f"Parameter domain[]='{type(domain)}' is not 'str'")
63     elif domain == "":
64         raise ValueError("Parameter 'domain' is empty")
65     elif not key in _pending:
66         raise ValueError(f"key='{key}' not found in _pending")
67     elif not fba.is_primitive(value):
68         raise ValueError(f"value[]='{type(value)}' is not a primitive type")
69
70     # Set it
71     _pending[key][domain] = value
72
73     # DEBUG: print("DEBUG: EXIT!")
74
75 def has_pending_instance_data(domain: str) -> bool:
76     # DEBUG: print(f"DEBUG: domain='{domain}' - CALLED!")
77     if not isinstance(domain, str):
78         raise ValueError(f"Parameter domain[]='{type(domain)}' is not 'str'")
79     elif domain == "":
80         raise ValueError("Parameter 'domain' is empty")
81
82     has_pending = False
83     for key in _pending:
84         # DEBUG: print(f"DEBUG: key='{key}',domain='{domain}',_pending[key]()='{len(_pending[key])}'")
85         if domain in _pending[key]:
86             has_pending = True
87             break
88
89     # DEBUG: print(f"DEBUG: has_pending='{has_pending}' - EXIT!")
90     return has_pending
91
92 def update_data(domain: str):
93     # DEBUG: print(f"DEBUG: domain='{domain}' - CALLED!")
94     if not isinstance(domain, str):
95         raise ValueError(f"Parameter domain[]='{type(domain)}' is not 'str'")
96     elif domain == "":
97         raise ValueError("Parameter 'domain' is empty")
98     elif not has_pending_instance_data(domain):
99         raise Exception(f"Domain '{domain}' has no pending instance data, but function invoked")
100
101     # DEBUG: print(f"DEBUG: Updating instance data for domain='{domain}' ...")
102     sql_string = ""
103     fields = list()
104     for key in _pending:
105         # DEBUG: print("DEBUG: key:", key)
106         if domain in _pending[key]:
107             # DEBUG: print(f"DEBUG: Adding '{_pending[key][domain]}' for key='{key}' ...")
108             fields.append(_pending[key][domain])
109             sql_string += f" {key} = ?,"
110
111     fields.append(time.time())
112     fields.append(domain)
113
114     if sql_string == "":
115         raise ValueError(f"No fields have been set, but method invoked, domain='{domain}'")
116
117     # DEBUG: print(f"DEBUG: sql_string='{sql_string}',fields()={len(fields)}")
118     sql_string = "UPDATE instances SET" + sql_string + " last_updated = ? WHERE domain = ? LIMIT 1"
119     # DEBUG: print("DEBUG: sql_string:", sql_string)
120
121     try:
122         # DEBUG: print("DEBUG: Executing SQL:", sql_string)
123         fba.cursor.execute(sql_string, fields)
124
125         # DEBUG: print(f"DEBUG: Success! (rowcount={fba.cursor.rowcount })")
126         if fba.cursor.rowcount == 0:
127             # DEBUG: print(f"DEBUG: Did not update any rows: domain='{domain}',fields()={len(fields)} - EXIT!")
128             return
129
130         # DEBUG: print("DEBUG: Committing changes ...")
131         fba.connection.commit()
132
133         # DEBUG: print(f"DEBUG: Deleting _pending for domain='{domain}'")
134         for key in _pending:
135             # DEBUG: print(f"DEBUG: domain='{domain}',key='{key}'")
136             if domain in _pending[key]:
137                 del _pending[key][domain]
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]) or domain.split(".")[-1] == "arpa":
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     software = None
200     try:
201         # DEBUG: print("DEBUG: domain,origin,command,path:", domain, origin, command, path)
202         software = federation.determine_software(domain, path)
203     except network.exceptions as exception:
204         print(f"WARNING Exception '{type(exception)}' during determining software type")
205
206     # DEBUG: print("DEBUG: Determined software:", software)
207     if software == "lemmy" and domain.find("/c/") > 0:
208         domain = domain.split("/c/")[0]
209         if is_registered(domain):
210             print(f"WARNING: domain='{domain}' already registered after cutting off user part. - EXIT!")
211             return
212
213     print(f"INFO: Adding instance domain='{domain}' (origin='{origin}',software='{software}')")
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     # DEBUG: print(f"DEBUG: Updating nodeinfo for domain='{domain}'")
235     update_last_nodeinfo(domain)
236
237     # DEBUG: print("DEBUG: EXIT!")
238
239 def update_last_nodeinfo(domain: str):
240     # DEBUG: print(f"DEBUG: domain='{domain}' - CALLED!")
241     if not isinstance(domain, str):
242         raise ValueError(f"Parameter domain[]='{type(domain)}' is not 'str'")
243     elif domain == "":
244         raise ValueError("Parameter 'domain' is empty")
245
246     # DEBUG: print("DEBUG: Updating last_nodeinfo for domain:", domain)
247     set_data("last_nodeinfo", domain, time.time())
248     set_data("last_updated" , domain, time.time())
249
250     # Running pending updated
251     # DEBUG: print(f"DEBUG: Invoking update_data({domain}) ...")
252     update_data(domain)
253
254     # DEBUG: print("DEBUG: EXIT!")
255
256 def update_last_error(domain: str, error: dict):
257     # DEBUG: print("DEBUG: domain,error[]:", domain, type(error))
258     if not isinstance(domain, str):
259         raise ValueError(f"Parameter domain[]='{type(domain)}' is not 'str'")
260     elif domain == "":
261         raise ValueError("Parameter 'domain' is empty")
262
263     # DEBUG: print("DEBUG: BEFORE error[]:", type(error))
264     if isinstance(error, BaseException) or isinstance(error, json.decoder.JSONDecodeError):
265         error = f"error[{type(error)}]='{str(error)}'"
266     # DEBUG: print("DEBUG: AFTER error[]:", type(error))
267
268     if isinstance(error, str):
269         # DEBUG: print(f"DEBUG: Setting last_error_details='{error}'")
270         set_data("last_status_code"  , domain, 999)
271         set_data("last_error_details", domain, error)
272     elif isinstance(error, requests.models.Response):
273         # DEBUG: print(f"DEBUG: Setting last_error_details='{error.reason}'")
274         set_data("last_status_code"  , domain, error.status_code)
275         set_data("last_error_details", domain, error.reason)
276     elif "status_code" in error and "error_message" in error:
277         # DEBUG: print(f"DEBUG: Setting last_error_details='{error['error_message']}'")
278         set_data("last_status_code"  , domain, error["status_code"])
279         set_data("last_error_details", domain, error["error_message"])
280     elif "json" in error and "error" in error["json"]:
281         set_data("last_status_code"  , domain, error["status_code"])
282         set_data("last_error_details", domain, error["json"]["error"])
283     else:
284         raise KeyError(f"Cannot handle keys in error[{type(error)}]='{error}'")
285
286     # Running pending updated
287     # DEBUG: print(f"DEBUG: Invoking update_data({domain}) ...")
288     update_data(domain)
289
290     fba.log_error(domain, error)
291
292     # DEBUG: print("DEBUG: EXIT!")
293
294 def is_registered(domain: str) -> bool:
295     # DEBUG: print(f"DEBUG: domain='{domain}' - CALLED!")
296     if not isinstance(domain, str):
297         raise ValueError(f"Parameter domain[]='{type(domain)}' is not 'str'")
298     elif domain == "":
299         raise ValueError("Parameter 'domain' is empty")
300
301     # DEBUG: print(f"DEBUG: domain='{domain}' - CALLED!")
302     if not cache.key_exists("is_registered"):
303         # DEBUG: print("DEBUG: Cache for 'is_registered' not initialized, fetching all rows ...")
304         fba.cursor.execute("SELECT domain FROM instances")
305
306         # Check Set all
307         cache.set_all("is_registered", fba.cursor.fetchall(), True)
308
309     # Is cache found?
310     registered = cache.sub_key_exists("is_registered", domain)
311
312     # DEBUG: print(f"DEBUG: registered='{registered}' - EXIT!")
313     return registered