feat(maintenance): add API endpoint to clear LXMF icons and enhance backup configuration options

This commit is contained in:
2026-01-05 20:21:31 -06:00
parent 2bef49de81
commit 8bb38d3e51
5 changed files with 162 additions and 21 deletions

View File

@@ -4038,6 +4038,12 @@ class ReticulumMeshChat:
self.database.misc.delete_archived_pages()
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
@routes.get("/api/v1/maintenance/messages/export")
async def maintenance_export_messages(request):
@@ -8519,6 +8525,14 @@ class ReticulumMeshChat:
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
if "crawler_enabled" in data:
self.config.crawler_enabled.set(self._parse_bool(data["crawler_enabled"]))
@@ -8591,6 +8605,14 @@ class ReticulumMeshChat:
if "message_font_size" in data:
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
if "desktop_open_calls_in_separate_window" in data:
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_max_versions": ctx.config.page_archiver_max_versions.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_max_retries": ctx.config.crawler_max_retries.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_color": ctx.config.banished_color.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(),
"libretranslate_url": ctx.config.libretranslate_url.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]
foreground_colour = "#" + icon_appearance[1].hex()
background_colour = "#" + icon_appearance[2].hex()
self.update_lxmf_user_icon(
lxmf_message.source_hash.hex(),
icon_name,
foreground_colour,
background_colour,
context=ctx,
local_hash = (
ctx.local_lxmf_destination.hexhash
if ctx.local_lxmf_destination
else None
)
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:
print("failed to update lxmf user icon from lxmf message")
print(e)

View File

@@ -80,30 +80,66 @@ class BotHandler:
logger.warning("Failed to restore bot %s: %s", entry.get("id"), exc)
def get_status(self):
bots = []
for bot_id, bot_info in self.running_bots.items():
instance = bot_info["instance"]
bots: list[dict] = []
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(
{
"id": bot_id,
"template": bot_info["template"],
"name": instance.bot.config.name
if instance and instance.bot
else "Unknown",
"address": RNS.prettyhexrep(instance.bot.local.hash)
if instance and instance.bot and instance.bot.local
else "Unknown",
"full_address": RNS.hexrep(instance.bot.local.hash, delimit=False)
if instance and instance.bot and instance.bot.local
else None,
"template": template,
"template_id": template,
"name": name,
"address": address_pretty or "Unknown",
"full_address": address_full,
"running": running,
"pid": pid,
"storage_dir": entry.get("storage_dir"),
},
)
return {
"has_lxmfy": True,
"detection_error": None,
"running_bots": bots,
"bots": self.bots_state,
"running_bots": [b for b in bots if b["running"]],
"bots": bots,
}
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):
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
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):
for bot_id in list(self.running_bots.keys()):
self.stop_bot(bot_id)

View File

@@ -270,6 +270,7 @@ class ConfigManager:
"#dc2626",
)
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
self.blackhole_integration_enabled = self.BoolConfig(

View File

@@ -99,6 +99,15 @@ class MiscDAO:
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
def get_forwarding_rules(self, identity_hash=None, active_only=False):
query = "SELECT * FROM lxmf_forwarding_rules WHERE 1=1"

View File

@@ -206,6 +206,16 @@ class IdentityContext:
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
self.rncp_handler = RNCPHandler(
reticulum_instance=getattr(self.app, "reticulum", None),