feat(maintenance): add API endpoint to clear LXMF icons and enhance backup configuration options
This commit is contained in:
@@ -4038,6 +4038,12 @@ class ReticulumMeshChat:
|
|||||||
self.database.misc.delete_archived_pages()
|
self.database.misc.delete_archived_pages()
|
||||||
return web.json_response({"message": "All archived pages cleared"})
|
return web.json_response({"message": "All archived pages cleared"})
|
||||||
|
|
||||||
|
# maintenance - clear LXMF icons
|
||||||
|
@routes.delete("/api/v1/maintenance/lxmf-icons")
|
||||||
|
async def maintenance_clear_lxmf_icons(request):
|
||||||
|
self.database.misc.delete_all_user_icons()
|
||||||
|
return web.json_response({"message": "All LXMF icons cleared"})
|
||||||
|
|
||||||
# maintenance - export messages
|
# maintenance - export messages
|
||||||
@routes.get("/api/v1/maintenance/messages/export")
|
@routes.get("/api/v1/maintenance/messages/export")
|
||||||
async def maintenance_export_messages(request):
|
async def maintenance_export_messages(request):
|
||||||
@@ -8519,6 +8525,14 @@ class ReticulumMeshChat:
|
|||||||
int(data["archives_max_storage_gb"]),
|
int(data["archives_max_storage_gb"]),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if "backup_max_count" in data:
|
||||||
|
try:
|
||||||
|
value = int(data["backup_max_count"])
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
value = self.config.backup_max_count.default_value
|
||||||
|
value = max(1, min(value, 50))
|
||||||
|
self.config.backup_max_count.set(value)
|
||||||
|
|
||||||
# update crawler settings
|
# update crawler settings
|
||||||
if "crawler_enabled" in data:
|
if "crawler_enabled" in data:
|
||||||
self.config.crawler_enabled.set(self._parse_bool(data["crawler_enabled"]))
|
self.config.crawler_enabled.set(self._parse_bool(data["crawler_enabled"]))
|
||||||
@@ -8591,6 +8605,14 @@ class ReticulumMeshChat:
|
|||||||
if "message_font_size" in data:
|
if "message_font_size" in data:
|
||||||
self.config.message_font_size.set(int(data["message_font_size"]))
|
self.config.message_font_size.set(int(data["message_font_size"]))
|
||||||
|
|
||||||
|
if "message_icon_size" in data:
|
||||||
|
try:
|
||||||
|
value = int(data["message_icon_size"])
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
value = self.config.message_icon_size.default_value
|
||||||
|
value = max(12, min(value, 96))
|
||||||
|
self.config.message_icon_size.set(value)
|
||||||
|
|
||||||
# update desktop settings
|
# update desktop settings
|
||||||
if "desktop_open_calls_in_separate_window" in data:
|
if "desktop_open_calls_in_separate_window" in data:
|
||||||
self.config.desktop_open_calls_in_separate_window.set(
|
self.config.desktop_open_calls_in_separate_window.set(
|
||||||
@@ -9534,6 +9556,7 @@ class ReticulumMeshChat:
|
|||||||
"page_archiver_enabled": ctx.config.page_archiver_enabled.get(),
|
"page_archiver_enabled": ctx.config.page_archiver_enabled.get(),
|
||||||
"page_archiver_max_versions": ctx.config.page_archiver_max_versions.get(),
|
"page_archiver_max_versions": ctx.config.page_archiver_max_versions.get(),
|
||||||
"archives_max_storage_gb": ctx.config.archives_max_storage_gb.get(),
|
"archives_max_storage_gb": ctx.config.archives_max_storage_gb.get(),
|
||||||
|
"backup_max_count": ctx.config.backup_max_count.get(),
|
||||||
"crawler_enabled": ctx.config.crawler_enabled.get(),
|
"crawler_enabled": ctx.config.crawler_enabled.get(),
|
||||||
"crawler_max_retries": ctx.config.crawler_max_retries.get(),
|
"crawler_max_retries": ctx.config.crawler_max_retries.get(),
|
||||||
"crawler_retry_delay_seconds": ctx.config.crawler_retry_delay_seconds.get(),
|
"crawler_retry_delay_seconds": ctx.config.crawler_retry_delay_seconds.get(),
|
||||||
@@ -9569,6 +9592,7 @@ class ReticulumMeshChat:
|
|||||||
"banished_text": ctx.config.banished_text.get(),
|
"banished_text": ctx.config.banished_text.get(),
|
||||||
"banished_color": ctx.config.banished_color.get(),
|
"banished_color": ctx.config.banished_color.get(),
|
||||||
"message_font_size": ctx.config.message_font_size.get(),
|
"message_font_size": ctx.config.message_font_size.get(),
|
||||||
|
"message_icon_size": ctx.config.message_icon_size.get(),
|
||||||
"translator_enabled": ctx.config.translator_enabled.get(),
|
"translator_enabled": ctx.config.translator_enabled.get(),
|
||||||
"libretranslate_url": ctx.config.libretranslate_url.get(),
|
"libretranslate_url": ctx.config.libretranslate_url.get(),
|
||||||
"desktop_open_calls_in_separate_window": ctx.config.desktop_open_calls_in_separate_window.get(),
|
"desktop_open_calls_in_separate_window": ctx.config.desktop_open_calls_in_separate_window.get(),
|
||||||
@@ -10011,13 +10035,50 @@ class ReticulumMeshChat:
|
|||||||
icon_name = icon_appearance[0]
|
icon_name = icon_appearance[0]
|
||||||
foreground_colour = "#" + icon_appearance[1].hex()
|
foreground_colour = "#" + icon_appearance[1].hex()
|
||||||
background_colour = "#" + icon_appearance[2].hex()
|
background_colour = "#" + icon_appearance[2].hex()
|
||||||
self.update_lxmf_user_icon(
|
|
||||||
lxmf_message.source_hash.hex(),
|
local_hash = (
|
||||||
icon_name,
|
ctx.local_lxmf_destination.hexhash
|
||||||
foreground_colour,
|
if ctx.local_lxmf_destination
|
||||||
background_colour,
|
else None
|
||||||
context=ctx,
|
|
||||||
)
|
)
|
||||||
|
source_hash = lxmf_message.source_hash.hex()
|
||||||
|
|
||||||
|
# ignore our own icon and empty payloads to avoid overwriting peers with our appearance
|
||||||
|
if source_hash and local_hash and source_hash == local_hash:
|
||||||
|
pass
|
||||||
|
elif (
|
||||||
|
not icon_name or not foreground_colour or not background_colour
|
||||||
|
):
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
local_icon_name = ctx.config.lxmf_user_icon_name.get()
|
||||||
|
local_icon_fg = (
|
||||||
|
ctx.config.lxmf_user_icon_foreground_colour.get()
|
||||||
|
)
|
||||||
|
local_icon_bg = (
|
||||||
|
ctx.config.lxmf_user_icon_background_colour.get()
|
||||||
|
)
|
||||||
|
|
||||||
|
# if incoming icon matches our own, skip storing and clear any mistaken stored copy
|
||||||
|
# for now, but this will need to be updated later if two users do have the same icon
|
||||||
|
# FIXME
|
||||||
|
if (
|
||||||
|
local_icon_name
|
||||||
|
and local_icon_fg
|
||||||
|
and local_icon_bg
|
||||||
|
and icon_name == local_icon_name
|
||||||
|
and foreground_colour == local_icon_fg
|
||||||
|
and background_colour == local_icon_bg
|
||||||
|
):
|
||||||
|
ctx.database.misc.delete_user_icon(source_hash)
|
||||||
|
else:
|
||||||
|
self.update_lxmf_user_icon(
|
||||||
|
source_hash,
|
||||||
|
icon_name,
|
||||||
|
foreground_colour,
|
||||||
|
background_colour,
|
||||||
|
context=ctx,
|
||||||
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print("failed to update lxmf user icon from lxmf message")
|
print("failed to update lxmf user icon from lxmf message")
|
||||||
print(e)
|
print(e)
|
||||||
|
|||||||
@@ -80,30 +80,66 @@ class BotHandler:
|
|||||||
logger.warning("Failed to restore bot %s: %s", entry.get("id"), exc)
|
logger.warning("Failed to restore bot %s: %s", entry.get("id"), exc)
|
||||||
|
|
||||||
def get_status(self):
|
def get_status(self):
|
||||||
bots = []
|
bots: list[dict] = []
|
||||||
for bot_id, bot_info in self.running_bots.items():
|
|
||||||
instance = bot_info["instance"]
|
for entry in self.bots_state:
|
||||||
|
bot_id = entry.get("id")
|
||||||
|
template = entry.get("template_id") or entry.get("template")
|
||||||
|
name = entry.get("name") or "Unknown"
|
||||||
|
pid = entry.get("pid")
|
||||||
|
|
||||||
|
running = False
|
||||||
|
if bot_id in self.running_bots:
|
||||||
|
running = True
|
||||||
|
elif pid:
|
||||||
|
running = self._is_pid_alive(pid)
|
||||||
|
|
||||||
|
address_pretty = None
|
||||||
|
address_full = None
|
||||||
|
|
||||||
|
# Try running instance first
|
||||||
|
instance = self.running_bots.get(bot_id, {}).get("instance")
|
||||||
|
if (
|
||||||
|
instance
|
||||||
|
and getattr(instance, "bot", None)
|
||||||
|
and getattr(instance.bot, "local", None)
|
||||||
|
):
|
||||||
|
try:
|
||||||
|
address_pretty = RNS.prettyhexrep(instance.bot.local.hash)
|
||||||
|
address_full = RNS.hexrep(instance.bot.local.hash, delimit=False)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Fallback to identity file on disk
|
||||||
|
if address_full is None:
|
||||||
|
identity = self._load_identity_for_bot(bot_id)
|
||||||
|
if identity:
|
||||||
|
try:
|
||||||
|
destination = RNS.Destination(identity, "lxmf", "delivery")
|
||||||
|
address_full = destination.hash.hex()
|
||||||
|
address_pretty = RNS.prettyhexrep(destination.hash)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
bots.append(
|
bots.append(
|
||||||
{
|
{
|
||||||
"id": bot_id,
|
"id": bot_id,
|
||||||
"template": bot_info["template"],
|
"template": template,
|
||||||
"name": instance.bot.config.name
|
"template_id": template,
|
||||||
if instance and instance.bot
|
"name": name,
|
||||||
else "Unknown",
|
"address": address_pretty or "Unknown",
|
||||||
"address": RNS.prettyhexrep(instance.bot.local.hash)
|
"full_address": address_full,
|
||||||
if instance and instance.bot and instance.bot.local
|
"running": running,
|
||||||
else "Unknown",
|
"pid": pid,
|
||||||
"full_address": RNS.hexrep(instance.bot.local.hash, delimit=False)
|
"storage_dir": entry.get("storage_dir"),
|
||||||
if instance and instance.bot and instance.bot.local
|
|
||||||
else None,
|
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"has_lxmfy": True,
|
"has_lxmfy": True,
|
||||||
"detection_error": None,
|
"detection_error": None,
|
||||||
"running_bots": bots,
|
"running_bots": [b for b in bots if b["running"]],
|
||||||
"bots": self.bots_state,
|
"bots": bots,
|
||||||
}
|
}
|
||||||
|
|
||||||
def start_bot(self, template_id, name=None, bot_id=None, storage_dir=None):
|
def start_bot(self, template_id, name=None, bot_id=None, storage_dir=None):
|
||||||
@@ -274,8 +310,32 @@ class BotHandler:
|
|||||||
if os.path.exists(id_path_alt):
|
if os.path.exists(id_path_alt):
|
||||||
return id_path_alt
|
return id_path_alt
|
||||||
|
|
||||||
|
# LXMFy may nest inside config/lxmf
|
||||||
|
id_path_lxmf = os.path.join(storage_dir, "config", "lxmf", "identity")
|
||||||
|
if os.path.exists(id_path_lxmf):
|
||||||
|
return id_path_lxmf
|
||||||
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
def _load_identity_for_bot(self, bot_id):
|
||||||
|
identity_path = self.get_bot_identity_path(bot_id)
|
||||||
|
if not identity_path:
|
||||||
|
return None
|
||||||
|
try:
|
||||||
|
return RNS.Identity.from_file(identity_path)
|
||||||
|
except Exception:
|
||||||
|
return None
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _is_pid_alive(pid):
|
||||||
|
if not pid:
|
||||||
|
return False
|
||||||
|
try:
|
||||||
|
os.kill(pid, 0)
|
||||||
|
return True
|
||||||
|
except OSError:
|
||||||
|
return False
|
||||||
|
|
||||||
def stop_all(self):
|
def stop_all(self):
|
||||||
for bot_id in list(self.running_bots.keys()):
|
for bot_id in list(self.running_bots.keys()):
|
||||||
self.stop_bot(bot_id)
|
self.stop_bot(bot_id)
|
||||||
|
|||||||
@@ -270,6 +270,7 @@ class ConfigManager:
|
|||||||
"#dc2626",
|
"#dc2626",
|
||||||
)
|
)
|
||||||
self.message_font_size = self.IntConfig(self, "message_font_size", 14)
|
self.message_font_size = self.IntConfig(self, "message_font_size", 14)
|
||||||
|
self.message_icon_size = self.IntConfig(self, "message_icon_size", 28)
|
||||||
|
|
||||||
# blackhole integration config
|
# blackhole integration config
|
||||||
self.blackhole_integration_enabled = self.BoolConfig(
|
self.blackhole_integration_enabled = self.BoolConfig(
|
||||||
|
|||||||
@@ -99,6 +99,15 @@ class MiscDAO:
|
|||||||
tuple(destination_hashes),
|
tuple(destination_hashes),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def delete_user_icon(self, destination_hash):
|
||||||
|
self.provider.execute(
|
||||||
|
"DELETE FROM lxmf_user_icons WHERE destination_hash = ?",
|
||||||
|
(destination_hash,),
|
||||||
|
)
|
||||||
|
|
||||||
|
def delete_all_user_icons(self):
|
||||||
|
self.provider.execute("DELETE FROM lxmf_user_icons")
|
||||||
|
|
||||||
# Forwarding Rules
|
# Forwarding Rules
|
||||||
def get_forwarding_rules(self, identity_hash=None, active_only=False):
|
def get_forwarding_rules(self, identity_hash=None, active_only=False):
|
||||||
query = "SELECT * FROM lxmf_forwarding_rules WHERE 1=1"
|
query = "SELECT * FROM lxmf_forwarding_rules WHERE 1=1"
|
||||||
|
|||||||
@@ -206,6 +206,16 @@ class IdentityContext:
|
|||||||
lambda msg: self.app.on_lxmf_delivery(msg, context=self),
|
lambda msg: self.app.on_lxmf_delivery(msg, context=self),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Restore preferred propagation node on startup
|
||||||
|
try:
|
||||||
|
preferred_node = (
|
||||||
|
self.config.lxmf_preferred_propagation_node_destination_hash.get()
|
||||||
|
)
|
||||||
|
if preferred_node:
|
||||||
|
self.app.set_active_propagation_node(preferred_node, context=self)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
# 5. Initialize Handlers and Managers
|
# 5. Initialize Handlers and Managers
|
||||||
self.rncp_handler = RNCPHandler(
|
self.rncp_handler = RNCPHandler(
|
||||||
reticulum_instance=getattr(self.app, "reticulum", None),
|
reticulum_instance=getattr(self.app, "reticulum", None),
|
||||||
|
|||||||
Reference in New Issue
Block a user