Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 36 additions & 0 deletions alembic.ini
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
[alembic]
script_location = alembic
prepend_sys_path = .

[loggers]
keys = root,sqlalchemy,alembic

[handlers]
keys = console

[formatters]
keys = generic

[logger_root]
level = WARN
handlers = console

[logger_sqlalchemy]
level = WARN
handlers =
qualname = sqlalchemy.engine

[logger_alembic]
level = INFO
handlers =
qualname = alembic

[handler_console]
class = StreamHandler
args = (sys.stderr,)
level = NOTSET
formatter = generic

[formatter_generic]
format = %(levelname)-5.5s [%(name)s] %(message)s
datefmt = %H:%M:%S
62 changes: 62 additions & 0 deletions alembic/env.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
from __future__ import annotations

import sys
from logging.config import fileConfig
from pathlib import Path

from alembic import context
from sqlalchemy import engine_from_config, pool

PROJECT_ROOT = Path(__file__).resolve().parents[1]
SRC_DIR = PROJECT_ROOT / "src"
if str(SRC_DIR) not in sys.path:
sys.path.insert(0, str(SRC_DIR))

config = context.config

if config.config_file_name is not None:
fileConfig(config.config_file_name)

target_metadata = None


def _get_database_url() -> str:
from core.db import get_database_url

return get_database_url()


def run_migrations_offline() -> None:
url = _get_database_url()
context.configure(
url=url,
target_metadata=target_metadata,
literal_binds=True,
dialect_opts={"paramstyle": "named"},
)

with context.begin_transaction():
context.run_migrations()


def run_migrations_online() -> None:
configuration = config.get_section(config.config_ini_section) or {}
configuration["sqlalchemy.url"] = _get_database_url()

connectable = engine_from_config(
configuration,
prefix="sqlalchemy.",
poolclass=pool.NullPool,
)

with connectable.connect() as connection:
context.configure(connection=connection, target_metadata=target_metadata)

with context.begin_transaction():
context.run_migrations()


if context.is_offline_mode():
run_migrations_offline()
else:
run_migrations_online()
25 changes: 25 additions & 0 deletions alembic/script.py.mako
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
"""${message}

Revision ID: ${up_revision}
Revises: ${down_revision | comma,n}
Create Date: ${create_date}
"""

from __future__ import annotations

from alembic import op

${imports if imports else ""}

revision = ${repr(up_revision)}
down_revision = ${repr(down_revision)}
branch_labels = ${repr(branch_labels)}
depends_on = ${repr(depends_on)}


def upgrade() -> None:
${upgrades if upgrades else "pass"}


def downgrade() -> None:
${downgrades if downgrades else "pass"}
109 changes: 109 additions & 0 deletions alembic/versions/0001_runtime_baseline.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
"""runtime baseline

Revision ID: 0001_runtime_baseline
Revises:
Create Date: 2026-03-16 00:00:00
"""

from __future__ import annotations

from alembic import op

revision = "0001_runtime_baseline"
down_revision = None
branch_labels = None
depends_on = None


def upgrade() -> None:
op.execute(
"""
CREATE TABLE IF NOT EXISTS kv_store (
scope TEXT NOT NULL,
key TEXT NOT NULL,
value TEXT NOT NULL,
updated_at TEXT NOT NULL,
PRIMARY KEY (scope, key)
)
"""
)

op.execute(
"""
CREATE TABLE IF NOT EXISTS webhooks (
id INTEGER PRIMARY KEY,
name TEXT NOT NULL,
url TEXT NOT NULL,
events TEXT NOT NULL,
secret TEXT NOT NULL,
enabled INTEGER NOT NULL DEFAULT 1,
failure_count INTEGER NOT NULL DEFAULT 0,
max_failures INTEGER NOT NULL DEFAULT 10,
last_success_at TEXT,
last_error TEXT,
disabled_reason TEXT,
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL
)
"""
)

op.execute(
"""
CREATE TABLE IF NOT EXISTS webhook_deliveries (
id INTEGER PRIMARY KEY,
webhook_id INTEGER NOT NULL,
event_type TEXT NOT NULL,
payload TEXT NOT NULL,
success INTEGER NOT NULL,
status_code INTEGER,
error TEXT,
attempt INTEGER NOT NULL,
response_body TEXT,
request_headers TEXT,
created_at TEXT NOT NULL
)
"""
)

op.execute(
"""
CREATE TABLE IF NOT EXISTS incoming_webhook_keys (
id INTEGER PRIMARY KEY,
name TEXT NOT NULL,
token_hash TEXT NOT NULL UNIQUE,
allowed_actions TEXT NOT NULL,
rate_limit_per_minute INTEGER NOT NULL DEFAULT 30,
enabled INTEGER NOT NULL DEFAULT 1,
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL,
last_used_at TEXT
)
"""
)

op.execute(
"""
CREATE TABLE IF NOT EXISTS audit_logs (
id INTEGER PRIMARY KEY,
actor TEXT NOT NULL,
action TEXT NOT NULL,
resource TEXT NOT NULL,
details TEXT,
created_at TEXT NOT NULL
)
"""
)

op.execute(
"CREATE INDEX IF NOT EXISTS idx_webhook_deliveries_webhook ON webhook_deliveries(webhook_id, id DESC)"
)
op.execute("CREATE INDEX IF NOT EXISTS idx_audit_logs_created ON audit_logs(id DESC)")


def downgrade() -> None:
op.execute("DROP TABLE IF EXISTS audit_logs")
op.execute("DROP TABLE IF EXISTS incoming_webhook_keys")
op.execute("DROP TABLE IF EXISTS webhook_deliveries")
op.execute("DROP TABLE IF EXISTS webhooks")
op.execute("DROP TABLE IF EXISTS kv_store")
109 changes: 109 additions & 0 deletions config.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -230,6 +230,11 @@
"type": "boolean",
"description": "Enable no-code automation rules",
"default": true
},
"anti_spam": {
"type": "boolean",
"description": "Enable anti-spam message flood protection",
"default": false
}
}
},
Expand Down Expand Up @@ -474,6 +479,110 @@
"description": "List of command names that are disabled",
"default": []
},
"command_permissions": {
"type": "object",
"description": "Role overrides for command access (global and per-group)",
"properties": {
"global": {
"type": "object",
"description": "Global role override per command name",
"additionalProperties": {
"type": "string",
"enum": [
"member",
"admin",
"owner"
]
},
"default": {}
},
"groups": {
"type": "object",
"description": "Per-group role override maps by group JID",
"additionalProperties": {
"type": "object",
"additionalProperties": {
"type": "string",
"enum": [
"member",
"admin",
"owner"
]
}
},
"default": {}
}
},
"default": {
"global": {},
"groups": {}
}
},
"privacy": {
"type": "object",
"description": "Privacy and data retention settings",
"properties": {
"analytics_retention_days": {
"type": "integer",
"minimum": 1,
"maximum": 365,
"description": "How many days of analytics history to keep",
"default": 30
},
"ai_memory_enabled": {
"type": "boolean",
"description": "Enable AI conversation memory globally",
"default": true
},
"ai_memory_ttl_hours": {
"type": "number",
"minimum": 1,
"maximum": 720,
"description": "AI memory time-to-live in hours before eviction",
"default": 24
}
},
"default": {
"analytics_retention_days": 30,
"ai_memory_enabled": true,
"ai_memory_ttl_hours": 24
}
},
"anti_spam": {
"type": "object",
"description": "Anti-spam flood protection settings",
"properties": {
"max_messages": {
"type": "integer",
"minimum": 2,
"maximum": 50,
"description": "Maximum messages allowed per user within the time window before action is taken",
"default": 5
},
"window_seconds": {
"type": "number",
"minimum": 1,
"maximum": 120,
"description": "Sliding time window in seconds for counting messages",
"default": 10
},
"action": {
"type": "string",
"enum": [
"warn",
"mute",
"kick"
],
"description": "Action to take when spam is detected",
"default": "warn"
},
"whitelist_admins": {
"type": "boolean",
"description": "Exempt group admins from spam detection",
"default": true
}
}
},
"dashboard": {
"type": "object",
"description": "Dashboard API and UI settings",
Expand Down
Loading
Loading