1 # Fedi API Block - An aggregator for fetching blocking data from fediverse nodes
2 # Copyright (C) 2023 Free Software Foundation
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.
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.
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/>.
23 from fba import blacklist
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
31 # Detection mode: 'AUTO_DISCOVERY', 'STATIC_CHECKS' or 'GENERATOR'
32 # NULL means all detection methods have failed (maybe still reachable instance)
33 "detection_mode" : {},
38 # Last fetched instances
39 "last_instance_fetch": {},
44 # Last nodeinfo (fetched)
47 "last_status_code" : {},
49 "last_error_details" : {},
52 def set(key: str, domain: str, value: any):
53 # DEBUG: print(f"DEBUG: key='{key}',domain='{domain}',value[]='{type(value)}' - CALLED!")
55 raise ValueError("Parameter key[]='{type(key)}' is not 'str'")
57 raise ValueError(f"Parameter 'key' cannot be empty")
58 elif type(domain) != str:
59 raise ValueError("Parameter domain[]='{type(domain)}' is not 'str'")
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")
68 _pending[key][domain] = value
70 # DEBUG: print("DEBUG: EXIT!")
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'")
77 raise ValueError(f"Parameter 'domain' is empty")
81 # DEBUG: print(f"DEBUG: key='{key}',domain='{domain}',_pending[key]()='{len(_pending[key])}'")
82 if domain in _pending[key]:
86 # DEBUG: print(f"DEBUG: has_pending='{has_pending}' - EXIT!")
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'")
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")
98 # DEBUG: print(f"DEBUG: Updating instance data for domain='{domain}' ...")
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} = ?,"
108 fields.append(time.time())
109 fields.append(domain)
112 raise ValueError(f"No fields have been set, but method invoked, domain='{domain}'")
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)
119 # DEBUG: print("DEBUG: Executing SQL:", sql_string)
120 fba.cursor.execute(sql_string, fields)
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!")
127 # DEBUG: print("DEBUG: Committing changes ...")
128 fba.connection.commit()
130 # DEBUG: print("DEBUG: Deleting _pending for domain:", domain)
133 # DEBUG: print("DEBUG: Deleting key:", key)
134 del _pending[key][domain]
138 except BaseException as e:
139 print(f"ERROR: failed SQL query: domain='{domain}',sql_string='{sql_string}',exception[{type(e)}]:'{str(e)}'")
142 # DEBUG: print("DEBUG: EXIT!")
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'")
149 raise ValueError(f"Parameter 'domain' is empty")
151 # DEBUG: print("DEBUG: Updating last_instance_fetch for domain:", domain)
152 set("last_instance_fetch", domain, time.time())
154 # Running pending updated
155 # DEBUG: print(f"DEBUG: Invoking update_data({domain}) ...")
158 # DEBUG: print("DEBUG: EXIT!")
160 def update_last_blocked(domain: str):
161 if type(domain) != str:
162 raise ValueError(f"Parameter domain[]={type(domain)} is not 'str'")
164 raise ValueError(f"Parameter 'domain' is empty")
166 # DEBUG: print("DEBUG: Updating last_blocked for domain", domain)
167 set("last_blocked", domain, time.time())
169 # Running pending updated
170 # DEBUG: print(f"DEBUG: Invoking update_data({domain}) ...")
173 # DEBUG: print("DEBUG: EXIT!")
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'")
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")
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!")
205 print(f"INFO: Adding instance domain='{domain}' (origin='{origin}',software='{software}')")
208 "INSERT INTO instances (domain, origin, originator, hash, software, first_seen) VALUES (?, ?, ?, ?, ?, ?)",
213 fba.get_hash(domain),
219 cache.set_sub_key("is_registered", domain, True)
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)
226 fba.remove_pending_error(domain)
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)
233 except BaseException as e:
234 print(f"ERROR: failed SQL query: domain='{domain}',exception[{type(e)}]:'{str(e)}'")
237 # DEBUG: print("DEBUG: Updating nodeinfo for domain:", domain)
238 update_last_nodeinfo(domain)
240 # DEBUG: print("DEBUG: EXIT!")
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'")
247 raise ValueError(f"Parameter 'domain' is empty")
249 # DEBUG: print("DEBUG: Updating last_nodeinfo for domain:", domain)
250 set("last_nodeinfo", domain, time.time())
251 set("last_updated" , domain, time.time())
253 # Running pending updated
254 # DEBUG: print(f"DEBUG: Invoking update_data({domain}) ...")
257 # DEBUG: print("DEBUG: EXIT!")
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'")
264 raise ValueError(f"Parameter 'domain' is empty")
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)"
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)
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)
280 # Running pending updated
281 # DEBUG: print(f"DEBUG: Invoking update_data({domain}) ...")
284 fba.log_error(domain, response)
286 # DEBUG: print("DEBUG: EXIT!")
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'")
293 raise ValueError(f"Parameter 'domain' is empty")
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 ...")
299 fba.cursor.execute("SELECT domain FROM instances")
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)}'")
308 registered = cache.sub_key_exists("is_registered", domain)
310 # DEBUG: print(f"DEBUG: registered='{registered}' - EXIT!")