]> git.mxchange.org Git - fba.git/blob - fba/models/instances.py
e86128181f8dd487a299d40412f6a4502e9c039b
[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 not is_registered(domain):
342         logger.debug("domain='%s' is not registered, returning False - EXIT!", domain)
343         return False
344
345     key = "recheck_instance"
346     if column == "last_blocked":
347         key = "recheck_block"
348
349     # Query database
350     database.cursor.execute(f"SELECT {column} FROM instances WHERE domain = ? LIMIT 1", [domain])
351
352     # Fetch row
353     row = database.cursor.fetchone()
354
355     fetched = float(row[column]) if row[column] is not None else 0.0
356
357     diff = (time.time() - fetched)
358
359     logger.debug("fetched[%s]='%s',key='%s',diff=%f", type(fetched), fetched, key, diff)
360     recently = bool(diff < config.get(key))
361
362     logger.debug("recently='%s' - EXIT!", recently)
363     return recently
364
365 def deobfuscate(char: str, domain: str, blocked_hash: str = None) -> tuple:
366     logger.debug("char='%s',domain='%s',blocked_hash='%s' - CALLED!", char, domain, blocked_hash)
367
368     if not isinstance(char, str):
369         raise ValueError(f"Parameter char[]='{type(char)}' is not of type 'str'")
370     elif char == "":
371         raise ValueError("Parameter 'char' is empty")
372     elif not char in domain:
373         raise ValueError(f"char='{char}' not found in domain='{domain}' but function invoked")
374     elif not isinstance(domain, str):
375         raise ValueError(f"Parameter domain[]='{type(domain)}'")
376     elif not isinstance(blocked_hash, str) and blocked_hash is not None:
377         raise ValueError(f"Parameter blocked_hash[]='{type(blocked_hash)}' is not of type 'str'")
378
379     # Init row
380     row = None
381
382     logger.debug("blocked_hash[]='%s'", type(blocked_hash))
383     if isinstance(blocked_hash, str):
384         logger.debug("Looking up blocked_hash='%s',domain='%s' ...", blocked_hash, domain)
385         database.cursor.execute(
386             "SELECT domain, origin, nodeinfo_url FROM instances WHERE hash = ? OR domain LIKE ? LIMIT 1", [blocked_hash, domain.replace(char, "_")]
387         )
388
389         row = database.cursor.fetchone()
390         logger.debug("row[]='%s'", type(row))
391
392         if row is None:
393             logger.debug("blocked_hash='%s' not found, trying domain='%s' ...", blocked_hash, domain)
394             return deobfuscate(char, domain)
395     elif not domain.startswith("*."):
396         logger.debug("domain='%s' - BEFORE!", domain)
397         domain = tidyup.domain(domain)
398         logger.debug("domain='%s' - AFTER!", domain)
399
400         if domain == "":
401             logger.warning("domain is empty after tidyup - EXIT!")
402             return None
403
404         search = domain.replace(char, "_")
405
406         logger.debug("Looking up domain='%s',search='%s' ...", domain, search)
407         database.cursor.execute(
408             "SELECT domain, origin, nodeinfo_url FROM instances WHERE domain LIKE ? OR 'https://' || domain LIKE ? ORDER BY rowid LIMIT 1", [search, search]
409         )
410
411         row = database.cursor.fetchone()
412         logger.debug("row[]='%s'", type(row))
413
414     logger.debug("row[]='%s' - EXIT!", type(row))
415     return row
416
417 def set_last_blocked(domain: str):
418     logger.debug("domain='%s' - CALLED!", domain)
419     domain_helper.raise_on(domain)
420
421     # Set timestamp
422     _set_data("last_blocked", domain, time.time())
423     logger.debug("EXIT!")
424
425 def set_last_instance_fetch(domain: str):
426     logger.debug("domain='%s' - CALLED!", domain)
427     domain_helper.raise_on(domain)
428
429     # Set timestamp
430     _set_data("last_instance_fetch", domain, time.time())
431     logger.debug("EXIT!")
432
433 def set_last_response_time(domain: str, response_time: float):
434     logger.debug("domain='%s',response_time=%d - CALLED!", domain, response_time)
435     domain_helper.raise_on(domain)
436
437     if not isinstance(response_time, float):
438         raise ValueError(f"response_time[]='{type(response_time)}' is not of type 'float'")
439     elif response_time < 0:
440         raise ValueError(f"response_time={response_time} is below zero")
441
442     # Set timestamp
443     _set_data("last_response_time", domain, response_time)
444     logger.debug("EXIT!")
445
446 def set_total_peers(domain: str, peers: list):
447     logger.debug("domain='%s',peers()=%d - CALLED!", domain, len(peers))
448     domain_helper.raise_on(domain)
449
450     if not isinstance(peers, list):
451         raise ValueError(f"Parameter peers[]='{type(peers)}' is not of type 'list'")
452
453     # Set timestamp
454     _set_data("total_peers", domain, len(peers))
455     logger.debug("EXIT!")
456
457 def set_total_blocks(domain: str, blocks: list):
458     logger.debug("domain='%s',blocks()=%d - CALLED!", domain, len(blocks))
459     domain_helper.raise_on(domain)
460
461     if not isinstance(blocks, list):
462         raise ValueError(f"Parameter blocks[]='{type(blocks)}' is not of type 'list'")
463
464     # Set timestamp
465     _set_data("total_blocks", domain, len(blocks))
466     logger.debug("EXIT!")
467
468 def set_obfuscated_blocks(domain: str, obfuscated: int):
469     logger.debug("domain='%s',obfuscated=%d - CALLED!", domain, obfuscated)
470     domain_helper.raise_on(domain)
471
472     if not isinstance(obfuscated, int):
473         raise ValueError(f"Parameter obfuscated[]='{type(obfuscated)}' is not of type 'int'")
474     elif obfuscated < 0:
475         raise ValueError(f"Parameter obfuscated={obfuscated} is not valid")
476
477     # Set timestamp
478     _set_data("obfuscated_blocks", domain, obfuscated)
479     logger.debug("EXIT!")
480
481 def set_nodeinfo_url(domain: str, url: str):
482     logger.debug("domain='%s',url='%s' - CALLED!", domain, url)
483     domain_helper.raise_on(domain)
484
485     if not isinstance(url, str) and url is not None:
486         raise ValueError(f"Parameter url[]='{type(url)}' is not of type 'str'")
487     elif url == "":
488         raise ValueError("Parameter 'url' is empty")
489     elif url is not None and not validators.url(url):
490         raise ValueError(f"Parameter url='{url}' is not a valid URL")
491
492     # Set timestamp
493     _set_data("nodeinfo_url", domain, url)
494     logger.debug("EXIT!")
495
496 def set_detection_mode(domain: str, mode: str):
497     logger.debug("domain='%s',mode='%s' - CALLED!", domain, mode)
498     domain_helper.raise_on(domain)
499
500     if not isinstance(mode, str) and mode is not None:
501         raise ValueError(f"Parameter mode[]='{type(mode)}' is not of type 'str'")
502     elif mode == "":
503         raise ValueError("Parameter 'mode' is empty")
504
505     # Set timestamp
506     _set_data("detection_mode", domain, mode)
507     logger.debug("EXIT!")
508
509 def set_has_obfuscation(domain: str, status: bool):
510     logger.debug("domain='%s',status='%s' - CALLED!", domain, status)
511     domain_helper.raise_on(domain)
512
513     if not isinstance(status, bool):
514         raise ValueError(f"Parameter status[]='{type(status)}' is not of type 'bool'")
515
516     # Set timestamp
517     _set_data("has_obfuscation", domain, status)
518     logger.debug("EXIT!")
519
520 def set_original_software(domain: str, software: str):
521     logger.debug("domain='%s',software='%s' - CALLED!", domain, software)
522     domain_helper.raise_on(domain)
523
524     if not isinstance(software, str) and software is not None:
525         raise ValueError(f"Parameter software[]='{type(software)}' is not of type 'str'")
526     elif software == "":
527         raise ValueError("Parameter 'software' is empty")
528
529     # Set original software
530     _set_data("original_software", domain, software)
531     logger.debug("EXIT!")
532
533
534 def set_software(domain: str, software: str):
535     logger.debug("domain='%s',software='%s' - CALLED!", domain, software)
536     domain_helper.raise_on(domain)
537
538     if not isinstance(software, str) and software is not None:
539         raise ValueError(f"Parameter software[]='{type(software)}' is not of type 'str'")
540     elif software == "":
541         raise ValueError("Parameter 'software' is empty")
542
543     # Set software (maybe aliased to generic name)
544     _set_data("software", domain, software)
545     logger.debug("EXIT!")
546
547 def valid(value: str, column: str) -> bool:
548     logger.debug("value='%s' - CALLED!", value)
549     if not isinstance(value, str):
550         raise ValueError(f"Parameter value[]='{type(value)}' is not of type 'str'")
551     elif value == "":
552         raise ValueError("Parameter 'value' is empty")
553     elif not isinstance(column, str):
554         raise ValueError(f"Parameter column[]='{type(column)}' is not of type 'str'")
555     elif column == "":
556         raise ValueError("Parameter 'column' is empty")
557
558     # Query database
559     database.cursor.execute(
560         f"SELECT {column} FROM instances WHERE {column} = ? LIMIT 1", [value]
561     )
562
563     is_valid = database.cursor.fetchone() is not None
564
565     logger.debug("is_valid='%s' - EXIT!", is_valid)
566     return is_valid
567
568 def translate_idnas(rows: list, column: str):
569     logger.debug("rows[]='%s' - CALLED!", type(rows))
570
571     if not isinstance(rows, list):
572         raise ValueError("rows[]='{type(rows)}' is not of type 'list'")
573     elif len(rows) == 0:
574         raise ValueError("Parameter 'rows' is an empty list")
575     elif not isinstance(column, str):
576         raise ValueError(f"column='{type(column)}' is not of type 'str'")
577     elif column == "":
578         raise ValueError("Parameter 'column' is empty")
579     elif column not in ["domain", "origin"]:
580         raise ValueError(f"column='{column}' is not supported")
581
582     logger.info("Checking/converting %d domain names ...", len(rows))
583     for row in rows:
584         logger.debug("row[]='%s'", type(row))
585
586         translated = row[column].encode("idna").decode("utf-8")
587         logger.debug("translated='%s',row[%s]='%s'", translated, column, row[column])
588
589         if translated != row[column]:
590             logger.info("Translated row[%s]='%s' to '%s'", column, row[column], translated)
591             if is_registered(translated, True):
592                 logger.warning("Deleting row[%s]='%s' as translated='%s' already exist", column, row[column], translated)
593                 database.cursor.execute(f"DELETE FROM instances WHERE {column} = ? LIMIT 1", [row[column]])
594             else:
595                 logger.debug("Updating row[%s]='%s' to translated='%s' ...", column, row[column], translated)
596                 database.cursor.execute(f"UPDATE instances SET {column} = ? WHERE {column} = ? LIMIT 1", [translated, row[column]])
597
598             logger.debug("Invoking commit() ...")
599             database.connection.commit()
600
601     logger.debug("EXIT!")