mirror of
https://github.com/infinition/Bjorn.git
synced 2025-12-12 07:35:00 +00:00
294 lines
13 KiB
Python
294 lines
13 KiB
Python
# 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
|