]> 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 config
26 from fba import fba
27 from fba import federation
28 from fba import network
29
30 from fba.helpers import cache
31
32 # Found info from node, such as nodeinfo URL, detection mode that needs to be
33 # written to database. Both arrays must be filled at the same time or else
34 # update_data() will fail
35 _pending = {
36     # Detection mode: 'AUTO_DISCOVERY', 'STATIC_CHECKS' or 'GENERATOR'
37     # NULL means all detection methods have failed (maybe still reachable instance)
38     "detection_mode"     : {},
39     # Found nodeinfo URL
40     "nodeinfo_url"       : {},
41     # Found total peers
42     "total_peers"        : {},
43     # Last fetched instances
44     "last_instance_fetch": {},
45     # Last updated
46     "last_updated"       : {},
47     # Last blocked
48     "last_blocked"       : {},
49     # Last nodeinfo (fetched)
50     "last_nodeinfo"      : {},
51     # Last status code
52     "last_status_code"   : {},
53     # Last error details
54     "last_error_details" : {},
55 }
56
57 def set_data(key: str, domain: str, value: any):
58     # DEBUG: print(f"DEBUG: key='{key}',domain='{domain}',value[]='{type(value)}' - CALLED!")
59     if not isinstance(key, str):
60         raise ValueError("Parameter key[]='{type(key)}' is not 'str'")
61     elif key == "":
62         raise ValueError("Parameter 'key' is empty")
63     elif not isinstance(domain, str):
64         raise ValueError(f"Parameter domain[]='{type(domain)}' is not 'str'")
65     elif domain == "":
66         raise ValueError("Parameter 'domain' is empty")
67     elif not key in _pending:
68         raise ValueError(f"key='{key}' not found in _pending")
69     elif not fba.is_primitive(value):
70         raise ValueError(f"value[]='{type(value)}' is not a primitive type")
71
72     # Set it
73     _pending[key][domain] = value
74
75     # DEBUG: print("DEBUG: EXIT!")
76
77 def has_pending(domain: str) -> bool:
78     # DEBUG: print(f"DEBUG: domain='{domain}' - CALLED!")
79     if not isinstance(domain, str):
80         raise ValueError(f"Parameter domain[]='{type(domain)}' is not 'str'")
81     elif domain == "":
82         raise ValueError("Parameter 'domain' is empty")
83
84     has = False
85     for key in _pending:
86         # DEBUG: print(f"DEBUG: key='{key}',domain='{domain}',_pending[key]()='{len(_pending[key])}'")
87         if domain in _pending[key]:
88             has = True
89             break
90
91     # DEBUG: print(f"DEBUG: has='{has}' - EXIT!")
92     return has
93
94 def update_data(domain: str):
95     # DEBUG: print(f"DEBUG: domain='{domain}' - CALLED!")
96     if not isinstance(domain, str):
97         raise ValueError(f"Parameter domain[]='{type(domain)}' is not 'str'")
98     elif domain == "":
99         raise ValueError("Parameter 'domain' is empty")
100     elif not has_pending(domain):
101         raise Exception(f"Domain '{domain}' has no pending instance data, but function invoked")
102
103     # DEBUG: print(f"DEBUG: Updating instance data for domain='{domain}' ...")
104     sql_string = ""
105     fields = list()
106     for key in _pending:
107         # DEBUG: print("DEBUG: key:", key)
108         if domain in _pending[key]:
109             # DEBUG: print(f"DEBUG: Adding '{_pending[key][domain]}' for key='{key}' ...")
110             fields.append(_pending[key][domain])
111             sql_string += f" {key} = ?,"
112
113     fields.append(time.time())
114     fields.append(domain)
115
116     if sql_string == "":
117         raise ValueError(f"No fields have been set, but method invoked, domain='{domain}'")
118
119     # DEBUG: print(f"DEBUG: sql_string='{sql_string}',fields()={len(fields)}")
120     sql_string = "UPDATE instances SET" + sql_string + " last_updated = ? WHERE domain = ? LIMIT 1"
121     # DEBUG: print("DEBUG: sql_string:", sql_string)
122
123     try:
124         # DEBUG: print("DEBUG: Executing SQL:", sql_string)
125         fba.cursor.execute(sql_string, fields)
126
127         # DEBUG: print(f"DEBUG: Success! (rowcount={fba.cursor.rowcount })")
128         if fba.cursor.rowcount == 0:
129             # DEBUG: print(f"DEBUG: Did not update any rows: domain='{domain}',fields()={len(fields)} - EXIT!")
130             return
131
132         # DEBUG: print("DEBUG: Committing changes ...")
133         fba.connection.commit()
134
135         # DEBUG: print(f"DEBUG: Deleting _pending for domain='{domain}'")
136         for key in _pending:
137             # DEBUG: print(f"DEBUG: domain='{domain}',key='{key}'")
138             if domain in _pending[key]:
139                 del _pending[key][domain]
140
141     except BaseException as exception:
142         print(f"ERROR: failed SQL query: domain='{domain}',sql_string='{sql_string}',exception[{type(exception)}]:'{str(exception)}'")
143         sys.exit(255)
144
145     # DEBUG: print("DEBUG: EXIT!")
146
147 def update_last_instance_fetch(domain: str):
148     # DEBUG: print(f"DEBUG: domain='{domain}' - CALLED!")
149     if not isinstance(domain, str):
150         raise ValueError(f"Parameter domain[]='{type(domain)}' is not 'str'")
151     elif domain == "":
152         raise ValueError("Parameter 'domain' is empty")
153
154     # DEBUG: print("DEBUG: Updating last_instance_fetch for domain:", domain)
155     set_data("last_instance_fetch", domain, time.time())
156
157     # Running pending updated
158     # DEBUG: print(f"DEBUG: Invoking update_data({domain}) ...")
159     update_data(domain)
160
161     # DEBUG: print("DEBUG: EXIT!")
162
163 def update_last_blocked(domain: str):
164     # DEBUG: print(f"DEBUG: domain='{domain}' - CALLED!")
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]) or domain.split(".")[-1] == "arpa":
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     software = None
203     try:
204         # DEBUG: print("DEBUG: domain,origin,command,path:", domain, origin, command, path)
205         software = federation.determine_software(domain, path)
206     except network.exceptions as exception:
207         print(f"WARNING Exception '{type(exception)}' during determining software type")
208
209     # DEBUG: print("DEBUG: Determined software:", software)
210     if software == "lemmy" and domain.find("/c/") > 0:
211         domain = domain.split("/c/")[0]
212         if is_registered(domain):
213             print(f"WARNING: domain='{domain}' already registered after cutting off user part. - EXIT!")
214             return
215
216     print(f"INFO: Adding instance domain='{domain}' (origin='{origin}',software='{software}')")
217     fba.cursor.execute(
218         "INSERT INTO instances (domain, origin, command, hash, software, first_seen) VALUES (?, ?, ?, ?, ?, ?)",
219         (
220            domain,
221            origin,
222            command,
223            fba.get_hash(domain),
224            software,
225            time.time()
226         ),
227     )
228
229     cache.set_sub_key("is_registered", domain, True)
230
231     if has_pending(domain):
232         # DEBUG: print(f"DEBUG: domain='{domain}' has pending nodeinfo being updated ...")
233         set_data("last_status_code"  , domain, None)
234         set_data("last_error_details", domain, None)
235         update_data(domain)
236
237     # DEBUG: print(f"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 not isinstance(domain, str):
245         raise ValueError(f"Parameter domain[]='{type(domain)}' is not 'str'")
246     elif domain == "":
247         raise ValueError("Parameter 'domain' is empty")
248
249     # DEBUG: print("DEBUG: Updating last_nodeinfo for domain:", domain)
250     set_data("last_nodeinfo", domain, time.time())
251     set_data("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, error: dict):
260     # DEBUG: print("DEBUG: domain,error[]:", domain, type(error))
261     if not isinstance(domain, str):
262         raise ValueError(f"Parameter domain[]='{type(domain)}' is not 'str'")
263     elif domain == "":
264         raise ValueError("Parameter 'domain' is empty")
265
266     # DEBUG: print("DEBUG: BEFORE error[]:", type(error))
267     if isinstance(error, (BaseException, json.decoder.JSONDecodeError)):
268         error = f"error[{type(error)}]='{str(error)}'"
269     # DEBUG: print("DEBUG: AFTER error[]:", type(error))
270
271     if isinstance(error, str):
272         # DEBUG: print(f"DEBUG: Setting last_error_details='{error}'")
273         set_data("last_status_code"  , domain, 999)
274         set_data("last_error_details", domain, error if error != "" else None)
275     elif isinstance(error, requests.models.Response):
276         # DEBUG: print(f"DEBUG: Setting last_error_details='{error.reason}'")
277         set_data("last_status_code"  , domain, error.status_code)
278         set_data("last_error_details", domain, error.reason if error.reason != "" else None)
279     elif not isinstance(error, dict):
280         raise KeyError(f"Cannot handle keys in error[{type(error)}]='{error}'")
281     elif "status_code" in error and "error_message" in error:
282         # DEBUG: print(f"DEBUG: Setting last_error_details='{error['error_message']}'")
283         set_data("last_status_code"  , domain, error["status_code"])
284         set_data("last_error_details", domain, error["error_message"] if error["error_message"] != "" else None)
285     elif "json" in error and "error" in error["json"]:
286         set_data("last_status_code"  , domain, error["status_code"])
287         set_data("last_error_details", domain, error["json"]["error"] if error["json"]["error"] != "" else None)
288
289     # Running pending updated
290     # DEBUG: print(f"DEBUG: Invoking update_data({domain}) ...")
291     update_data(domain)
292
293     fba.log_error(domain, error)
294
295     # DEBUG: print("DEBUG: EXIT!")
296
297 def is_registered(domain: str) -> bool:
298     # DEBUG: print(f"DEBUG: domain='{domain}' - CALLED!")
299     if not isinstance(domain, str):
300         raise ValueError(f"Parameter domain[]='{type(domain)}' is not 'str'")
301     elif domain == "":
302         raise ValueError("Parameter 'domain' is empty")
303
304     # DEBUG: print(f"DEBUG: domain='{domain}' - CALLED!")
305     if not cache.key_exists("is_registered"):
306         # DEBUG: print("DEBUG: Cache for 'is_registered' not initialized, fetching all rows ...")
307         fba.cursor.execute("SELECT domain FROM instances")
308
309         # Check Set all
310         cache.set_all("is_registered", fba.cursor.fetchall(), True)
311
312     # Is cache found?
313     registered = cache.sub_key_exists("is_registered", domain)
314
315     # DEBUG: print(f"DEBUG: registered='{registered}' - EXIT!")
316     return registered
317
318 def is_recent(domain: str) -> bool:
319     # DEBUG: print(f"DEBUG: domain='{domain}' - CALLED!")
320     if not isinstance(domain, str):
321         raise ValueError(f"Parameter domain[]='{type(domain)}' is not 'str'")
322     elif domain == "":
323         raise ValueError("Parameter 'domain' is empty")
324     elif not is_registered(domain):
325         # DEBUG: print(f"DEBUG: domain='{domain}' is not registered, returning False - EXIT!")
326         return False
327
328     # Query database
329     fba.cursor.execute("SELECT last_instance_fetch FROM instances WHERE domain = ? LIMIT 1", [domain])
330
331     # Fetch row
332     fetched = fba.cursor.fetchone()[0]
333
334     # DEBUG: print(f"DEBUG: fetched[{type(fetched)}]='{fetched}'")
335     recently = isinstance(fetched, float) and time.time() - fetched <= config.get("recheck_instance")
336
337     # DEBUG: print(f"DEBUG: recently='{recently}' - EXIT!")
338     return recently
339
340 def deobscure(char: str, domain: str, blocked_hash: str = None) -> tuple:
341     #print(f"DEBUG: char='{char}',domain='{domain}',blocked_hash='{blocked_hash}' - CALLED!")
342     if not isinstance(char, str):
343         raise ValueError(f"Parameter char[]='{type(char)}' is not 'str'")
344     elif char == "":
345         raise ValueError("Parameter 'char' is empty")
346     elif not isinstance(domain, str):
347         raise ValueError(f"Parameter domain[]='{type(domain)}' is not 'str'")
348     elif domain == "":
349         raise ValueError("Parameter 'domain' is empty")
350     elif not isinstance(blocked_hash, str) and blocked_hash is not None:
351         raise ValueError(f"Parameter blocked_hash[]='{type(blocked_hash)}' is not 'str'")
352
353     if isinstance(blocked_hash, str):
354         fba.cursor.execute(
355             "SELECT domain, origin, nodeinfo_url FROM instances WHERE hash = ? LIMIT 1", [blocked_hash]
356         )
357
358         if fba.cursor.fetchone() is None:
359             #print(f"DEBUG: blocked_hash='{blocked_hash}' not found, trying domain='{domain}' ...")
360             return deobscure(char, domain)
361     else:
362         fba.cursor.execute(
363             "SELECT domain, origin, nodeinfo_url FROM instances WHERE domain LIKE ? ORDER BY rowid LIMIT 1", [domain.replace(char, "_")]
364         )
365
366     row = fba.cursor.fetchone()
367
368     #print(f"DEBUG: row[]='{type(row)}' - EXIT!")
369     return row