]> git.mxchange.org Git - fba.git/blob - fba/models/instances.py
ab9995f83523b3613d145665077ca80e174c8d0e
[fba.git] / fba / models / 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 logging
18 import json
19 import time
20
21 import requests
22 import validators
23
24 from fba import database
25 from fba import utils
26
27 from fba.helpers import blacklist
28 from fba.helpers import cache
29 from fba.helpers import config
30 from fba.helpers import domain as domain_helper
31 from fba.helpers import tidyup
32
33 from fba.http import federation
34 from fba.http import network
35
36 from fba.models import error_log
37
38 logging.basicConfig(level=logging.INFO)
39 logger = logging.getLogger(__name__)
40 #logger.setLevel(logging.DEBUG)
41
42 # Found info from node, such as nodeinfo URL, detection mode that needs to be
43 # written to database. Both arrays must be filled at the same time or else
44 # update() will fail
45 _pending = {
46     # Detection mode
47     # NULL means all detection methods have failed (maybe still reachable instance)
48     "detection_mode"     : {},
49     # Found nodeinfo URL
50     "nodeinfo_url"       : {},
51     # Found total peers
52     "total_peers"        : {},
53     # Found total blocks
54     "total_blocks"       : {},
55     # Obfuscated domains
56     "obfuscated_blocks"  : {},
57     # Last fetched instances
58     "last_instance_fetch": {},
59     # Last updated
60     "last_updated"       : {},
61     # Last blocked
62     "last_blocked"       : {},
63     # Last nodeinfo (fetched)
64     "last_nodeinfo"      : {},
65     # Last response time
66     "last_response_time" : {},
67     # Last status code
68     "last_status_code"   : {},
69     # Last error details
70     "last_error_details" : {},
71     # Wether obfuscation has been used
72     "has_obfuscation"    : {},
73     # Original software
74     "original_software"  : {},
75     # Aliased software
76     "software"           : {},
77 }
78
79 def _set_data(key: str, domain: str, value: any):
80     logger.debug("key='%s',domain='%s',value[]='%s' - CALLED!", key, domain, type(value))
81     domain_helper.raise_on(domain)
82
83     if not isinstance(key, str):
84         raise ValueError(f"Parameter key[]='{type(key)}' is not of type 'str'")
85     elif key == "":
86         raise ValueError("Parameter 'key' is empty")
87     elif not key in _pending:
88         raise ValueError(f"key='{key}' not found in _pending")
89     elif blacklist.is_blacklisted(domain):
90         raise Exception(f"domain='{domain}' is blacklisted but function has been invoked")
91     elif not utils.is_primitive(value):
92         raise ValueError(f"value[]='{type(value)}' is not a primitive type")
93
94     # Set it
95     _pending[key][domain] = value
96
97     logger.debug("EXIT!")
98
99 def has_pending(domain: str) -> bool:
100     logger.debug("domain='%s' - CALLED!", domain)
101     domain_helper.raise_on(domain)
102
103     if not is_registered(domain):
104         raise ValueError(f"domain='{domain}' is not registered but function was invoked.")
105     elif blacklist.is_blacklisted(domain):
106         raise Exception(f"domain='{domain}' is blacklisted but function has been invoked")
107
108     has = False
109     logger.debug("Checking %d _pending array elements ...", len(_pending))
110     for key in _pending:
111         logger.debug("domain='%s',_pending[%s]()=%d", domain, key, len(_pending[key]))
112         if domain in _pending[key]:
113             logger.debug("domain='%s' at key='%s' has pending data ...", domain, key)
114             has = True
115             break
116
117     logger.debug("has='%s' - EXIT!", has)
118     return has
119
120 def update(domain: str):
121     logger.debug("domain='%s' - CALLED!", domain)
122     domain_helper.raise_on(domain)
123
124     if not is_registered(domain):
125         raise Exception(f"domain='{domain}' cannot be updated while not being registered")
126     elif not has_pending(domain):
127         raise Exception(f"domain='{domain}' has no pending instance data, but function invoked")
128     elif blacklist.is_blacklisted(domain):
129         raise Exception(f"domain='{domain}' is blacklisted but function has been invoked")
130
131     sql_string = ""
132     fields = list()
133
134     logger.debug("Checking %d _pending array elements ...", len(_pending))
135     for key in _pending:
136         logger.debug("Checking key='%s',domain='%s'", key, domain)
137         if domain in _pending[key]:
138             logger.debug("Adding '%s' for key='%s' ...", _pending[key][domain], key)
139             fields.append(_pending[key][domain])
140             sql_string += f" {key} = ?,"
141
142     logger.debug("sql_string(%d)='%s'", len(sql_string), sql_string)
143     if sql_string == "":
144         raise ValueError(f"No fields have been set, but function invoked, domain='{domain}'")
145
146     # Set last_updated to current timestamp
147     fields.append(time.time())
148
149     # For WHERE statement
150     logger.debug("Setting domain='%s' for WHERE statement ...", domain)
151     fields.append(domain)
152
153     logger.debug("sql_string='%s',fields()=%d", sql_string, len(fields))
154     sql_string = "UPDATE instances SET" + sql_string + " last_updated = ? WHERE domain = ? LIMIT 1"
155
156     logger.debug("Executing SQL: sql_string='%s',fields()=%d", sql_string, len(fields))
157     database.cursor.execute(sql_string, fields)
158
159     logger.debug("rowcount=%d", database.cursor.rowcount)
160     if database.cursor.rowcount == 0:
161         raise Exception(f"Did not update any rows: domain='{domain}',fields()={len(fields)}")
162
163     logger.debug("Invoking commit() ...")
164     database.connection.commit()
165
166     logger.debug("Deleting _pending for domain='%s'", domain)
167     for key in _pending:
168         logger.debug("domain='%s',key='%s'", domain, key)
169         if domain in _pending[key]:
170             logger.debug("Deleting key='%s',domain='%s' ...", key, domain)
171             del _pending[key][domain]
172
173     logger.debug("EXIT!")
174
175 def add(domain: str, origin: str, command: str, path: str = None, software: str = None):
176     logger.debug("domain='%s',origin='%s',command='%s',path='%s',software='%s' - CALLED!", domain, origin, command, path, software)
177     domain_helper.raise_on(domain)
178
179     if not isinstance(origin, str) and origin is not None:
180         raise ValueError(f"origin[]='{type(origin)}' is not of type 'str'")
181     elif origin == "":
182         raise ValueError("Parameter 'origin' is empty")
183     elif not isinstance(command, str):
184         raise ValueError(f"command[]='{type(command)}' is not of type 'str'")
185     elif command == "":
186         raise ValueError("Parameter 'command' is empty")
187     elif not isinstance(path, str) and path is not None:
188         raise ValueError(f"path[]='{type(path)}' is not of type 'str'")
189     elif path == "":
190         raise ValueError("Parameter 'path' is empty")
191     elif path is not None and not path.startswith("/"):
192         raise ValueError(f"path='{path}' does not start with / but should")
193     elif not isinstance(software, str) and software is not None:
194         raise ValueError(f"software[]='{type(software)}' is not of type 'str'")
195     elif software == "":
196         raise ValueError("Parameter 'software' is empty")
197     elif origin is not None and not validators.domain(origin.split("/")[0]):
198         raise ValueError(f"Bad origin name='{origin}'")
199     elif blacklist.is_blacklisted(domain):
200         raise Exception(f"domain='{domain}' is blacklisted, but function invoked")
201     elif domain.find("/profile/") > 0 or domain.find("/users/") > 0 or (is_registered(domain.split("/")[0]) and domain.find("/c/") > 0):
202         raise Exception(f"domain='{domain}' is a single user")
203     elif domain.find("/tag/") > 0:
204         raise Exception(f"domain='{domain}' is a tag")
205
206     if software is None:
207         try:
208             logger.debug("domain='%s',origin='%s',command='%s',path='%s'", domain, origin, command, path)
209             software = federation.determine_software(domain, path)
210         except network.exceptions as exception:
211             logger.warning("Exception '%s' during determining software type, domain='%s'", type(exception), domain)
212             set_last_error(domain, exception)
213
214     logger.debug("Determined software='%s'", software)
215     if software == "lemmy" and domain.find("/c/") > 0:
216         domain = domain.split("/c/")[0]
217
218         logger.debug("domain='%s' - LEMMY /c/ !", domain)
219         if is_registered(domain):
220             logger.warning("domain='%s' already registered after cutting off user part. - EXIT!", domain)
221             return
222
223     logger.info("Adding instance domain='%s',origin='%s',software='%s',command='%s' ...", domain, origin, software, command)
224     database.cursor.execute(
225         "INSERT INTO instances (domain, origin, command, hash, software, original_software, first_seen) VALUES (?, ?, ?, ?, ?, ?, ?)",
226         (
227            domain,
228            origin,
229            command,
230            utils.get_hash(domain),
231            software,
232            software,
233            time.time()
234         ),
235     )
236
237     logger.debug("Marking domain='%s' as registered.", domain)
238     cache.set_sub_key("is_registered", domain, True)
239
240     logger.debug("Checking if domain='%s' has pending updates ...", domain)
241     if has_pending(domain):
242         logger.debug("Flushing updates for domain='%s' ...", domain)
243         update(domain)
244
245     logger.debug("EXIT!")
246
247 def set_last_nodeinfo(domain: str):
248     logger.debug("domain='%s' - CALLED!", domain)
249     domain_helper.raise_on(domain)
250
251     logger.debug("Updating last_nodeinfo for domain='%s'", domain)
252     _set_data("last_nodeinfo", domain, time.time())
253
254     logger.debug("EXIT!")
255
256 def set_last_error(domain: str, error: dict):
257     logger.debug("domain='%s',error[]='%s' - CALLED!", domain, type(error))
258     domain_helper.raise_on(domain)
259
260     logger.debug("error[]='%s' - BEFORE!", type(error))
261     if isinstance(error, (BaseException, json.decoder.JSONDecodeError)):
262         error = f"error[{type(error)}]='{str(error)}'"
263     logger.debug("error[]='%s' - AFTER!", type(error))
264
265     if isinstance(error, str):
266         logger.debug("Setting last_error_details='%s' (str)", error)
267         _set_data("last_status_code"  , domain, 999)
268         _set_data("last_error_details", domain, error if error != "" else None)
269     elif isinstance(error, requests.models.Response):
270         logger.debug("Setting last_error_details='%s' (Response)", error.reason)
271         _set_data("last_status_code"  , domain, error.status_code)
272         _set_data("last_error_details", domain, error.reason if error.reason != "" else None)
273     elif not isinstance(error, dict):
274         raise KeyError(f"Cannot handle keys in error[{type(error)}]='{error}'")
275     elif "status_code" in error and "error_message" in error:
276         logger.debug("Setting last_error_details='%s' (error_message)", error['error_message'])
277         _set_data("last_status_code"  , domain, error["status_code"])
278         _set_data("last_error_details", domain, error["error_message"] if error["error_message"] != "" else None)
279     elif "json" in error and "error" in error["json"] and "msg" in error["json"]:
280         logger.debug("Setting last_error_details='%s' (json,error)", error["json"]["msg"])
281         _set_data("last_status_code"  , domain, error["status_code"])
282         _set_data("last_error_details", domain, error["json"]["msg"] if error["json"]["msg"] != "" else None)
283     elif "json" in error and "error" in error["json"] and "message" in error["json"]["error"]:
284         logger.debug("Setting last_error_details='%s' (json,error)", error["json"]["error"]["message"])
285         _set_data("last_status_code"  , domain, error["status_code"])
286         _set_data("last_error_details", domain, error["json"]["error"]["message"] if error["json"]["error"]["message"] != "" else None)
287     elif "json" in error and "error" in error["json"]:
288         logger.debug("Setting last_error_details='%s' (json,error)", error["json"]["error"])
289         _set_data("last_status_code"  , domain, error["status_code"])
290         _set_data("last_error_details", domain, error["json"]["error"] if error["json"]["error"] != "" else None)
291
292     logger.debug("Invoking error_log.add(domain='%s',error[]='%s'", domain, type(error))
293     error_log.add(domain, error)
294
295     logger.debug("EXIT!")
296
297 def set_success(domain: str):
298     logger.debug("domain='%s' - CALLED!", domain)
299     domain_helper.raise_on(domain)
300
301     # Set both to success
302     _set_data("last_status_code"  , domain, 200)
303     _set_data("last_error_details", domain, None)
304
305     logger.debug("EXIT!")
306
307 def is_registered(domain: str, skip_raise = False) -> bool:
308     logger.debug("domain='%s',skip_raise='%s' - CALLED!", domain, skip_raise)
309     domain_helper.raise_on(domain)
310
311     if blacklist.is_blacklisted(domain):
312         raise Exception(f"domain='{domain}' is blacklisted but function has been invoked")
313     elif not isinstance(skip_raise, bool):
314         raise ValueError(f"skip_raise[]='{type(skip_raise)}' is not type of 'bool'")
315
316     if not skip_raise:
317         domain_helper.raise_on(domain)
318
319     logger.debug("domain='%s' - CALLED!", domain)
320     if not cache.key_exists("is_registered"):
321         logger.debug("Cache for 'is_registered' not initialized, fetching all rows ...")
322         database.cursor.execute("SELECT domain FROM instances")
323
324         # Check Set all
325         cache.set_all("is_registered", database.cursor.fetchall(), True)
326
327     # Is cache found?
328     registered = cache.sub_key_exists("is_registered", domain)
329
330     logger.debug("registered='%s' - EXIT!", registered)
331     return registered
332
333 def is_recent(domain: str, column: str = "last_instance_fetch") -> bool:
334     logger.debug("domain='%s',column='%s' - CALLED!", domain, column)
335     domain_helper.raise_on(domain)
336
337     if not isinstance(column, str):
338         raise ValueError(f"Parameter column[]='{type(column)}' is not of type 'str'")
339     elif not column.startswith("last_"):
340         raise ValueError(f"Parameter column='{column}' is not expected")
341     elif blacklist.is_blacklisted(domain):
342         raise ValueError(f"domain='{domain}' is blacklisted but function was invoked")
343     elif not is_registered(domain):
344         logger.debug("domain='%s' is not registered, returning False - EXIT!", domain)
345         return False
346
347     key = "recheck_instance"
348     if column == "last_blocked":
349         key = "recheck_block"
350
351     # Query database
352     database.cursor.execute(f"SELECT {column} FROM instances WHERE domain = ? LIMIT 1", [domain])
353
354     # Fetch row
355     row = database.cursor.fetchone()
356
357     fetched = float(row[column]) if row[column] is not None else 0.0
358
359     diff = (time.time() - fetched)
360
361     logger.debug("fetched[%s]='%s',key='%s',diff=%f", type(fetched), fetched, key, diff)
362     recently = bool(diff < config.get(key))
363
364     logger.debug("recently='%s' - EXIT!", recently)
365     return recently
366
367 def deobfuscate(char: str, domain: str, blocked_hash: str = None) -> tuple:
368     logger.debug("char='%s',domain='%s',blocked_hash='%s' - CALLED!", char, domain, blocked_hash)
369
370     if not isinstance(char, str):
371         raise ValueError(f"Parameter char[]='{type(char)}' is not of type 'str'")
372     elif char == "":
373         raise ValueError("Parameter 'char' is empty")
374     elif not char in domain:
375         raise ValueError(f"char='{char}' not found in domain='{domain}' but function invoked")
376     elif not isinstance(domain, str):
377         raise ValueError(f"Parameter domain[]='{type(domain)}'")
378     elif not isinstance(blocked_hash, str) and blocked_hash is not None:
379         raise ValueError(f"Parameter blocked_hash[]='{type(blocked_hash)}' is not of type 'str'")
380
381     # Init row
382     row = None
383
384     logger.debug("blocked_hash[]='%s'", type(blocked_hash))
385     if isinstance(blocked_hash, str):
386         logger.debug("Looking up blocked_hash='%s',domain='%s' ...", blocked_hash, domain)
387         database.cursor.execute(
388             "SELECT domain, origin, nodeinfo_url FROM instances WHERE hash = ? OR domain LIKE ? LIMIT 1", [blocked_hash, domain.replace(char, "_")]
389         )
390
391         row = database.cursor.fetchone()
392         logger.debug("row[]='%s'", type(row))
393
394         if row is None:
395             logger.debug("blocked_hash='%s' not found, trying domain='%s' ...", blocked_hash, domain)
396             return deobfuscate(char, domain)
397     elif not domain.startswith("*."):
398         logger.debug("domain='%s' - BEFORE!", domain)
399         domain = tidyup.domain(domain)
400         logger.debug("domain='%s' - AFTER!", domain)
401
402         if domain == "":
403             logger.warning("domain is empty after tidyup - EXIT!")
404             return None
405
406         search = domain.replace(char, "_")
407
408         logger.debug("Looking up domain='%s',search='%s' ...", domain, search)
409         database.cursor.execute(
410             "SELECT domain, origin, nodeinfo_url FROM instances WHERE domain LIKE ? OR 'https://' || domain LIKE ? ORDER BY rowid LIMIT 1", [search, search]
411         )
412
413         row = database.cursor.fetchone()
414         logger.debug("row[]='%s'", type(row))
415
416     logger.debug("row[]='%s' - EXIT!", type(row))
417     return row
418
419 def set_last_blocked(domain: str):
420     logger.debug("domain='%s' - CALLED!", domain)
421     domain_helper.raise_on(domain)
422
423     # Set timestamp
424     _set_data("last_blocked", domain, time.time())
425     logger.debug("EXIT!")
426
427 def set_last_instance_fetch(domain: str):
428     logger.debug("domain='%s' - CALLED!", domain)
429     domain_helper.raise_on(domain)
430
431     # Set timestamp
432     _set_data("last_instance_fetch", domain, time.time())
433     logger.debug("EXIT!")
434
435 def set_last_response_time(domain: str, response_time: float):
436     logger.debug("domain='%s',response_time=%d - CALLED!", domain, response_time)
437     domain_helper.raise_on(domain)
438
439     if not isinstance(response_time, float):
440         raise ValueError(f"response_time[]='{type(response_time)}' is not of type 'float'")
441     elif response_time < 0:
442         raise ValueError(f"response_time={response_time} is below zero")
443
444     # Set timestamp
445     _set_data("last_response_time", domain, response_time)
446     logger.debug("EXIT!")
447
448 def set_total_peers(domain: str, peers: list):
449     logger.debug("domain='%s',peers()=%d - CALLED!", domain, len(peers))
450     domain_helper.raise_on(domain)
451
452     if not isinstance(peers, list):
453         raise ValueError(f"Parameter peers[]='{type(peers)}' is not of type 'list'")
454
455     # Set timestamp
456     _set_data("total_peers", domain, len(peers))
457     logger.debug("EXIT!")
458
459 def set_total_blocks(domain: str, blocks: list):
460     logger.debug("domain='%s',blocks()=%d - CALLED!", domain, len(blocks))
461     domain_helper.raise_on(domain)
462
463     if not isinstance(blocks, list):
464         raise ValueError(f"Parameter blocks[]='{type(blocks)}' is not of type 'list'")
465
466     # Set timestamp
467     _set_data("total_blocks", domain, len(blocks))
468     logger.debug("EXIT!")
469
470 def set_obfuscated_blocks(domain: str, obfuscated: int):
471     logger.debug("domain='%s',obfuscated=%d - CALLED!", domain, obfuscated)
472     domain_helper.raise_on(domain)
473
474     if not isinstance(obfuscated, int):
475         raise ValueError(f"Parameter obfuscated[]='{type(obfuscated)}' is not of type 'int'")
476     elif obfuscated < 0:
477         raise ValueError(f"Parameter obfuscated={obfuscated} is not valid")
478
479     # Set timestamp
480     _set_data("obfuscated_blocks", domain, obfuscated)
481     logger.debug("EXIT!")
482
483 def set_nodeinfo_url(domain: str, url: str):
484     logger.debug("domain='%s',url='%s' - CALLED!", domain, url)
485     domain_helper.raise_on(domain)
486
487     if not isinstance(url, str) and url is not None:
488         raise ValueError(f"Parameter url[]='{type(url)}' is not of type 'str'")
489     elif url == "":
490         raise ValueError("Parameter 'url' is empty")
491     elif url is not None and not validators.url(url):
492         raise ValueError(f"Parameter url='{url}' is not a valid URL")
493
494     # Set timestamp
495     _set_data("nodeinfo_url", domain, url)
496     logger.debug("EXIT!")
497
498 def set_detection_mode(domain: str, mode: str):
499     logger.debug("domain='%s',mode='%s' - CALLED!", domain, mode)
500     domain_helper.raise_on(domain)
501
502     if not isinstance(mode, str) and mode is not None:
503         raise ValueError(f"Parameter mode[]='{type(mode)}' is not of type 'str'")
504     elif mode == "":
505         raise ValueError("Parameter 'mode' is empty")
506
507     # Set timestamp
508     _set_data("detection_mode", domain, mode)
509     logger.debug("EXIT!")
510
511 def set_has_obfuscation(domain: str, status: bool):
512     logger.debug("domain='%s',status='%s' - CALLED!", domain, status)
513     domain_helper.raise_on(domain)
514
515     if not isinstance(status, bool):
516         raise ValueError(f"Parameter status[]='{type(status)}' is not of type 'bool'")
517
518     # Set timestamp
519     _set_data("has_obfuscation", domain, status)
520     logger.debug("EXIT!")
521
522 def set_original_software(domain: str, software: str):
523     logger.debug("domain='%s',software='%s' - CALLED!", domain, software)
524     domain_helper.raise_on(domain)
525
526     if not isinstance(software, str) and software is not None:
527         raise ValueError(f"Parameter software[]='{type(software)}' is not of type 'str'")
528     elif software == "":
529         raise ValueError("Parameter 'software' is empty")
530
531     # Set original software
532     _set_data("original_software", domain, software)
533     logger.debug("EXIT!")
534
535
536 def set_software(domain: str, software: str):
537     logger.debug("domain='%s',software='%s' - CALLED!", domain, software)
538     domain_helper.raise_on(domain)
539
540     if not isinstance(software, str) and software is not None:
541         raise ValueError(f"Parameter software[]='{type(software)}' is not of type 'str'")
542     elif software == "":
543         raise ValueError("Parameter 'software' is empty")
544
545     # Set software (maybe aliased to generic name)
546     _set_data("software", domain, software)
547     logger.debug("EXIT!")
548
549 def valid(value: str, column: str) -> bool:
550     logger.debug("value='%s' - CALLED!", value)
551     if not isinstance(value, str):
552         raise ValueError(f"Parameter value[]='{type(value)}' is not of type 'str'")
553     elif value == "":
554         raise ValueError("Parameter 'value' is empty")
555     elif not isinstance(column, str):
556         raise ValueError(f"Parameter column[]='{type(column)}' is not of type 'str'")
557     elif column == "":
558         raise ValueError("Parameter 'column' is empty")
559
560     # Query database
561     database.cursor.execute(
562         f"SELECT {column} FROM instances WHERE {column} = ? LIMIT 1", [value]
563     )
564
565     is_valid = database.cursor.fetchone() is not None
566
567     logger.debug("is_valid='%s' - EXIT!", is_valid)
568     return is_valid
569
570 def translate_idnas(rows: list, column: str):
571     logger.debug("rows[]='%s' - CALLED!", type(rows))
572
573     if not isinstance(rows, list):
574         raise ValueError("rows[]='{type(rows)}' is not of type 'list'")
575     elif len(rows) == 0:
576         raise ValueError("Parameter 'rows' is an empty list")
577     elif not isinstance(column, str):
578         raise ValueError(f"column='{type(column)}' is not of type 'str'")
579     elif column == "":
580         raise ValueError("Parameter 'column' is empty")
581     elif column not in ["domain", "origin"]:
582         raise ValueError(f"column='{column}' is not supported")
583
584     logger.info("Checking/converting %d domain names ...", len(rows))
585     for row in rows:
586         logger.debug("row[]='%s'", type(row))
587
588         translated = row[column].encode("idna").decode("utf-8")
589         logger.debug("translated='%s',row[%s]='%s'", translated, column, row[column])
590
591         if translated != row[column]:
592             logger.info("Translated row[%s]='%s' to '%s'", column, row[column], translated)
593             if is_registered(translated, True):
594                 logger.warning("Deleting row[%s]='%s' as translated='%s' already exist", column, row[column], translated)
595                 database.cursor.execute(f"DELETE FROM instances WHERE {column} = ? LIMIT 1", [row[column]])
596             else:
597                 logger.debug("Updating row[%s]='%s' to translated='%s' ...", column, row[column], translated)
598                 database.cursor.execute(f"UPDATE instances SET {column} = ? WHERE {column} = ? LIMIT 1", [translated, row[column]])
599
600             logger.debug("Invoking commit() ...")
601             database.connection.commit()
602
603     logger.debug("EXIT!")