# db_utils/actions.py # Action definition and management operations import json import sqlite3 from functools import lru_cache from typing import Any, Dict, List, Optional import logging from logger import Logger logger = Logger(name="db_utils.actions", level=logging.DEBUG) class ActionOps: """Action definition and configuration operations""" def __init__(self, base): self.base = base def create_tables(self): """Create actions table""" self.base.execute(""" CREATE TABLE IF NOT EXISTS actions ( b_class TEXT PRIMARY KEY, b_module TEXT NOT NULL, b_port INTEGER, b_status TEXT, b_parent TEXT, b_args TEXT, b_description TEXT, b_name TEXT, b_author TEXT, b_version TEXT, b_icon TEXT, b_docs_url TEXT, b_examples TEXT, b_action TEXT DEFAULT 'normal', b_service TEXT, b_trigger TEXT, b_requires TEXT, b_priority INTEGER DEFAULT 50, b_tags TEXT, b_timeout INTEGER DEFAULT 300, b_max_retries INTEGER DEFAULT 3, b_cooldown INTEGER DEFAULT 0, b_rate_limit TEXT, b_stealth_level INTEGER DEFAULT 5, b_risk_level TEXT DEFAULT 'medium', b_enabled INTEGER DEFAULT 1 ); """) logger.debug("Actions table created/verified") # ========================================================================= # ACTION CRUD OPERATIONS # ========================================================================= def sync_actions(self, actions): """Sync action definitions to database""" if not actions: return def _as_int(x, default=None): if x is None: return default if isinstance(x, (list, tuple)): x = x[0] if x else default try: return int(x) except Exception: return default def _as_str(x, default=None): if x is None: return default if isinstance(x, (list, tuple, set, dict)): try: return json.dumps(list(x) if not isinstance(x, dict) else x, ensure_ascii=False) except Exception: return default return str(x) def _as_json(x): if x is None: return None if isinstance(x, str): xs = x.strip() if (xs.startswith("{") and xs.endswith("}")) or (xs.startswith("[") and xs.endswith("]")): return xs return json.dumps(x, ensure_ascii=False) try: return json.dumps(x, ensure_ascii=False) except Exception: return None with self.base.transaction(): for a in actions: # Normalize fields b_service = a.get("b_service") if isinstance(b_service, (list, tuple, set, dict)): b_service = json.dumps(list(b_service) if not isinstance(b_service, dict) else b_service, ensure_ascii=False) b_tags = a.get("b_tags") if isinstance(b_tags, (list, tuple, set, dict)): b_tags = json.dumps(list(b_tags) if not isinstance(b_tags, dict) else b_tags, ensure_ascii=False) b_trigger = a.get("b_trigger") if isinstance(b_trigger, (list, tuple, set, dict)): b_trigger = json.dumps(b_trigger, ensure_ascii=False) b_requires = a.get("b_requires") if isinstance(b_requires, (list, tuple, set, dict)): b_requires = json.dumps(b_requires, ensure_ascii=False) b_args_json = _as_json(a.get("b_args")) # Enriched metadata b_name = _as_str(a.get("b_name")) b_description = _as_str(a.get("b_description")) b_author = _as_str(a.get("b_author")) b_version = _as_str(a.get("b_version")) b_icon = _as_str(a.get("b_icon")) b_docs_url = _as_str(a.get("b_docs_url")) b_examples = _as_json(a.get("b_examples")) # Typed fields b_port = _as_int(a.get("b_port")) b_priority = _as_int(a.get("b_priority"), 50) b_timeout = _as_int(a.get("b_timeout"), 300) b_max_retries = _as_int(a.get("b_max_retries"), 3) b_cooldown = _as_int(a.get("b_cooldown"), 0) b_stealth_level = _as_int(a.get("b_stealth_level"), 5) b_enabled = _as_int(a.get("b_enabled"), 1) b_rate_limit = _as_str(a.get("b_rate_limit")) b_risk_level = _as_str(a.get("b_risk_level"), "medium") self.base.execute(""" INSERT INTO actions ( b_class,b_module,b_port,b_status,b_parent, b_action,b_service,b_trigger,b_requires,b_priority, b_tags,b_timeout,b_max_retries,b_cooldown,b_rate_limit, b_stealth_level,b_risk_level,b_enabled, b_args, b_name, b_description, b_author, b_version, b_icon, b_docs_url, b_examples ) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?, ?,?,?,?,?,?,?) ON CONFLICT(b_class) DO UPDATE SET b_module = excluded.b_module, b_port = COALESCE(excluded.b_port, actions.b_port), b_status = COALESCE(excluded.b_status, actions.b_status), b_parent = COALESCE(excluded.b_parent, actions.b_parent), b_action = COALESCE(excluded.b_action, actions.b_action), b_service = COALESCE(excluded.b_service, actions.b_service), b_trigger = COALESCE(excluded.b_trigger, actions.b_trigger), b_requires = COALESCE(excluded.b_requires, actions.b_requires), b_priority = COALESCE(excluded.b_priority, actions.b_priority), b_tags = COALESCE(excluded.b_tags, actions.b_tags), b_timeout = COALESCE(excluded.b_timeout, actions.b_timeout), b_max_retries = COALESCE(excluded.b_max_retries, actions.b_max_retries), b_cooldown = COALESCE(excluded.b_cooldown, actions.b_cooldown), b_rate_limit = COALESCE(excluded.b_rate_limit, actions.b_rate_limit), b_stealth_level = COALESCE(excluded.b_stealth_level, actions.b_stealth_level), b_risk_level = COALESCE(excluded.b_risk_level, actions.b_risk_level), b_enabled = COALESCE(excluded.b_enabled, actions.b_enabled), b_args = COALESCE(excluded.b_args, actions.b_args), b_name = COALESCE(excluded.b_name, actions.b_name), b_description = COALESCE(excluded.b_description, actions.b_description), b_author = COALESCE(excluded.b_author, actions.b_author), b_version = COALESCE(excluded.b_version, actions.b_version), b_icon = COALESCE(excluded.b_icon, actions.b_icon), b_docs_url = COALESCE(excluded.b_docs_url, actions.b_docs_url), b_examples = COALESCE(excluded.b_examples, actions.b_examples) """, ( a.get("b_class"), a.get("b_module"), b_port, a.get("b_status"), a.get("b_parent"), a.get("b_action", "normal"), b_service, b_trigger, b_requires, b_priority, b_tags, b_timeout, b_max_retries, b_cooldown, b_rate_limit, b_stealth_level, b_risk_level, b_enabled, b_args_json, b_name, b_description, b_author, b_version, b_icon, b_docs_url, b_examples )) # Update action counter in stats action_count_row = self.base.query_one("SELECT COUNT(*) as cnt FROM actions WHERE b_enabled = 1") if action_count_row: try: self.base.execute(""" UPDATE stats SET actions_count = ? WHERE id = 1 """, (action_count_row['cnt'],)) except sqlite3.OperationalError: # Column doesn't exist yet, add it self.base.execute("ALTER TABLE stats ADD COLUMN actions_count INTEGER DEFAULT 0") self.base.execute(""" UPDATE stats SET actions_count = ? WHERE id = 1 """, (action_count_row['cnt'],)) logger.info(f"Synchronized {len(actions)} actions") def list_actions(self): """List all action definitions ordered by class name""" return self.base.query("SELECT * FROM actions ORDER BY b_class;") def list_studio_actions(self): """List all studio action definitions""" return self.base.query("SELECT * FROM actions_studio ORDER BY b_class;") def get_action_by_class(self, b_class: str) -> dict | None: """Get action by class name""" rows = self.base.query("SELECT * FROM actions WHERE b_class=? LIMIT 1;", (b_class,)) return rows[0] if rows else None def delete_action(self, b_class: str) -> None: """Delete action by class name""" self.base.execute("DELETE FROM actions WHERE b_class=?;", (b_class,)) def upsert_simple_action(self, *, b_class: str, b_module: str, **kw) -> None: """Minimal upsert of an action by reusing sync_actions""" rec = {"b_class": b_class, "b_module": b_module} rec.update(kw) self.sync_actions([rec]) def list_action_cards(self) -> list[dict]: """Lightweight descriptor of actions for card-based UIs""" rows = self.base.query(""" SELECT b_class, COALESCE(b_enabled, 0) AS b_enabled FROM actions ORDER BY b_class; """) out = [] for r in rows: cls = r["b_class"] enabled = int(r["b_enabled"]) # 0 reste 0 out.append({ "name": cls, "image": f"/actions/actions_icons/{cls}.png", "enabled": enabled, }) return out # def list_action_cards(self) -> list[dict]: # """Lightweight descriptor of actions for card-based UIs""" # rows = self.base.query(""" # SELECT b_class, b_enabled # FROM actions # ORDER BY b_class; # """) # out = [] # for r in rows: # cls = r["b_class"] # out.append({ # "name": cls, # "image": f"/actions/actions_icons/{cls}.png", # "enabled": int(r.get("b_enabled", 1) or 1), # }) # return out @lru_cache(maxsize=32) def get_action_definition(self, b_class: str) -> Optional[Dict[str, Any]]: """Cached lookup of an action definition by class name""" row = self.base.query("SELECT * FROM actions WHERE b_class=? LIMIT 1;", (b_class,)) if not row: return None r = row[0] if r.get("b_args"): try: r["b_args"] = json.loads(r["b_args"]) except Exception: pass return r