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