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()
|
||||
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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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),
|
||||
|
||||
Reference in New Issue
Block a user