2026-01-19 16:57:18 -03:00
|
|
|
from __future__ import annotations
|
|
|
|
|
|
2026-01-20 23:03:04 -03:00
|
|
|
from dataclasses import dataclass
|
|
|
|
|
from datetime import datetime, timezone
|
|
|
|
|
import re
|
|
|
|
|
import time
|
2026-01-19 16:57:18 -03:00
|
|
|
from typing import Any
|
|
|
|
|
|
2026-01-20 23:03:04 -03:00
|
|
|
import httpx
|
|
|
|
|
import psycopg
|
|
|
|
|
|
|
|
|
|
from ..k8s.exec import ExecError, PodExecutor
|
|
|
|
|
from ..k8s.pods import PodSelectionError
|
2026-01-19 16:57:18 -03:00
|
|
|
from ..settings import settings
|
2026-01-20 23:03:04 -03:00
|
|
|
from ..utils.logging import get_logger
|
|
|
|
|
from .keycloak_admin import keycloak_admin
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
logger = get_logger(__name__)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _extract_attr(attrs: Any, key: str) -> str:
|
|
|
|
|
if not isinstance(attrs, dict):
|
|
|
|
|
return ""
|
|
|
|
|
raw = attrs.get(key)
|
|
|
|
|
if isinstance(raw, list):
|
|
|
|
|
for item in raw:
|
|
|
|
|
if isinstance(item, str) and item.strip():
|
|
|
|
|
return item.strip()
|
|
|
|
|
return ""
|
|
|
|
|
if isinstance(raw, str) and raw.strip():
|
|
|
|
|
return raw.strip()
|
|
|
|
|
return ""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _resolve_mailu_email(username: str, user: dict[str, Any]) -> str:
|
|
|
|
|
attrs = user.get("attributes")
|
|
|
|
|
mailu_email = _extract_attr(attrs, "mailu_email")
|
|
|
|
|
if mailu_email:
|
|
|
|
|
return mailu_email
|
|
|
|
|
email = user.get("email")
|
|
|
|
|
if isinstance(email, str) and email.strip():
|
|
|
|
|
email = email.strip()
|
|
|
|
|
if email.lower().endswith(f"@{settings.mailu_domain.lower()}"):
|
|
|
|
|
return email
|
|
|
|
|
return f"{username}@{settings.mailu_domain}"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _parse_mail_export(output: str) -> list[tuple[str, str]]:
|
|
|
|
|
accounts: list[tuple[str, str]] = []
|
|
|
|
|
account_id = ""
|
|
|
|
|
for line in output.splitlines():
|
|
|
|
|
line = line.strip()
|
|
|
|
|
if not line:
|
|
|
|
|
continue
|
|
|
|
|
match = re.match(r"^Account\s+(\d+):", line, flags=re.IGNORECASE)
|
|
|
|
|
if match:
|
|
|
|
|
account_id = match.group(1)
|
|
|
|
|
continue
|
|
|
|
|
match = re.match(r"^-\s*E-?mail:\s*(\S+)", line, flags=re.IGNORECASE)
|
|
|
|
|
if match and account_id:
|
|
|
|
|
accounts.append((account_id, match.group(1)))
|
|
|
|
|
return accounts
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
|
|
|
class NextcloudMailSyncSummary:
|
|
|
|
|
processed: int
|
|
|
|
|
created: int
|
|
|
|
|
updated: int
|
|
|
|
|
deleted: int
|
|
|
|
|
skipped: int
|
|
|
|
|
failures: int
|
|
|
|
|
detail: str = ""
|
2026-01-19 16:57:18 -03:00
|
|
|
|
|
|
|
|
|
|
|
|
|
class NextcloudService:
|
|
|
|
|
def __init__(self) -> None:
|
2026-01-20 23:03:04 -03:00
|
|
|
self._executor = PodExecutor(
|
2026-01-20 18:11:02 -03:00
|
|
|
settings.nextcloud_namespace,
|
2026-01-20 23:03:04 -03:00
|
|
|
settings.nextcloud_pod_label,
|
|
|
|
|
settings.nextcloud_container,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
def _occ(self, args: list[str]) -> str:
|
|
|
|
|
command = ["runuser", "-u", "www-data", "--", "php", "/var/www/html/occ", *args]
|
|
|
|
|
result = self._executor.exec(
|
|
|
|
|
command,
|
|
|
|
|
timeout_sec=settings.nextcloud_exec_timeout_sec,
|
|
|
|
|
check=True,
|
|
|
|
|
)
|
|
|
|
|
return result.stdout
|
|
|
|
|
|
|
|
|
|
def run_cron(self) -> dict[str, Any]:
|
|
|
|
|
if not settings.nextcloud_namespace:
|
|
|
|
|
raise RuntimeError("nextcloud cron not configured")
|
|
|
|
|
try:
|
|
|
|
|
self._executor.exec(
|
|
|
|
|
["runuser", "-u", "www-data", "--", "php", "-f", "/var/www/html/cron.php"],
|
|
|
|
|
timeout_sec=settings.nextcloud_exec_timeout_sec,
|
|
|
|
|
check=True,
|
|
|
|
|
)
|
|
|
|
|
except (ExecError, PodSelectionError, TimeoutError) as exc:
|
|
|
|
|
return {"status": "error", "detail": str(exc)}
|
|
|
|
|
return {"status": "ok"}
|
|
|
|
|
|
|
|
|
|
def _list_mail_accounts(self, username: str) -> list[tuple[str, str]]:
|
|
|
|
|
output = self._occ(["mail:account:export", username])
|
|
|
|
|
return _parse_mail_export(output)
|
|
|
|
|
|
|
|
|
|
def _set_editor_mode_richtext(self, account_ids: list[str]) -> None:
|
|
|
|
|
safe_ids = [item for item in account_ids if item.isdigit()]
|
|
|
|
|
if not safe_ids:
|
|
|
|
|
return
|
|
|
|
|
if not settings.nextcloud_db_host or not settings.nextcloud_db_password:
|
|
|
|
|
logger.info(
|
|
|
|
|
"nextcloud editor_mode skipped",
|
|
|
|
|
extra={"event": "nextcloud_mail_editor_mode", "status": "skip", "reason": "missing db config"},
|
|
|
|
|
)
|
|
|
|
|
return
|
|
|
|
|
ids_csv = ",".join(safe_ids)
|
|
|
|
|
query = (
|
|
|
|
|
"UPDATE oc_mail_accounts SET editor_mode='richtext' "
|
|
|
|
|
f"WHERE id IN ({ids_csv}) AND editor_mode <> 'richtext';"
|
2026-01-20 18:11:02 -03:00
|
|
|
)
|
2026-01-20 23:03:04 -03:00
|
|
|
try:
|
|
|
|
|
with psycopg.connect(
|
|
|
|
|
host=settings.nextcloud_db_host,
|
|
|
|
|
port=settings.nextcloud_db_port,
|
|
|
|
|
dbname=settings.nextcloud_db_name,
|
|
|
|
|
user=settings.nextcloud_db_user,
|
|
|
|
|
password=settings.nextcloud_db_password,
|
|
|
|
|
) as conn:
|
|
|
|
|
with conn.cursor() as cur:
|
|
|
|
|
cur.execute(query)
|
|
|
|
|
except Exception as exc:
|
|
|
|
|
logger.info(
|
|
|
|
|
"nextcloud editor_mode update failed",
|
|
|
|
|
extra={"event": "nextcloud_mail_editor_mode", "status": "error", "detail": str(exc)},
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
def _set_user_mail_meta(self, user_id: str, primary_email: str, account_count: int) -> None:
|
|
|
|
|
synced_at = time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime())
|
|
|
|
|
attrs = {
|
|
|
|
|
"nextcloud_mail_primary_email": [primary_email],
|
|
|
|
|
"nextcloud_mail_account_count": [str(account_count)],
|
|
|
|
|
"nextcloud_mail_synced_at": [synced_at],
|
|
|
|
|
}
|
|
|
|
|
try:
|
|
|
|
|
keycloak_admin.update_user_safe(user_id, {"attributes": attrs})
|
|
|
|
|
except Exception:
|
|
|
|
|
return
|
2026-01-19 16:57:18 -03:00
|
|
|
|
|
|
|
|
def sync_mail(self, username: str | None = None, wait: bool = True) -> dict[str, Any]:
|
2026-01-20 23:03:04 -03:00
|
|
|
if not settings.nextcloud_namespace:
|
2026-01-19 16:57:18 -03:00
|
|
|
raise RuntimeError("nextcloud mail sync not configured")
|
2026-01-20 23:03:04 -03:00
|
|
|
cleaned_username = None
|
|
|
|
|
if username is not None:
|
|
|
|
|
cleaned_username = username.strip()
|
|
|
|
|
if not cleaned_username:
|
2026-01-19 16:57:18 -03:00
|
|
|
raise RuntimeError("missing username")
|
2026-01-20 23:03:04 -03:00
|
|
|
if not keycloak_admin.ready():
|
|
|
|
|
return {"status": "error", "detail": "keycloak admin not configured"}
|
|
|
|
|
|
|
|
|
|
users: list[dict[str, Any]]
|
|
|
|
|
if cleaned_username is not None:
|
|
|
|
|
user = keycloak_admin.find_user(cleaned_username)
|
|
|
|
|
if not user:
|
|
|
|
|
return {"status": "ok", "detail": "no matching user"}
|
|
|
|
|
users = [user]
|
|
|
|
|
else:
|
|
|
|
|
users = keycloak_admin.iter_users(page_size=200, brief=False)
|
|
|
|
|
|
|
|
|
|
processed = created = updated = deleted = skipped = failures = 0
|
|
|
|
|
|
|
|
|
|
for user in users:
|
|
|
|
|
username_val = user.get("username") if isinstance(user.get("username"), str) else ""
|
|
|
|
|
username_val = username_val.strip()
|
|
|
|
|
if not username_val:
|
|
|
|
|
skipped += 1
|
|
|
|
|
continue
|
|
|
|
|
if user.get("enabled") is False:
|
|
|
|
|
skipped += 1
|
|
|
|
|
continue
|
|
|
|
|
if user.get("serviceAccountClientId") or username_val.startswith("service-account-"):
|
|
|
|
|
skipped += 1
|
|
|
|
|
continue
|
|
|
|
|
|
|
|
|
|
user_id = user.get("id") if isinstance(user.get("id"), str) else ""
|
|
|
|
|
full_user = user
|
|
|
|
|
if user_id:
|
|
|
|
|
try:
|
|
|
|
|
full_user = keycloak_admin.get_user(user_id)
|
|
|
|
|
except Exception:
|
|
|
|
|
full_user = user
|
|
|
|
|
|
|
|
|
|
attrs = full_user.get("attributes") if isinstance(full_user.get("attributes"), dict) else {}
|
|
|
|
|
mailu_email = _resolve_mailu_email(username_val, full_user)
|
|
|
|
|
app_pw = _extract_attr(attrs, "mailu_app_password")
|
|
|
|
|
if not mailu_email or not app_pw:
|
|
|
|
|
skipped += 1
|
|
|
|
|
continue
|
|
|
|
|
if mailu_email and not _extract_attr(attrs, "mailu_email"):
|
|
|
|
|
try:
|
|
|
|
|
keycloak_admin.set_user_attribute(username_val, "mailu_email", mailu_email)
|
|
|
|
|
except Exception:
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
accounts = self._list_mail_accounts(username_val)
|
|
|
|
|
except Exception as exc:
|
|
|
|
|
failures += 1
|
|
|
|
|
logger.info(
|
|
|
|
|
"nextcloud mail export failed",
|
|
|
|
|
extra={"event": "nextcloud_mail_export", "status": "error", "detail": str(exc)},
|
|
|
|
|
)
|
|
|
|
|
continue
|
|
|
|
|
|
|
|
|
|
processed += 1
|
|
|
|
|
mailu_accounts = [(aid, email) for aid, email in accounts if email.lower().endswith(f"@{settings.mailu_domain.lower()}")]
|
|
|
|
|
|
|
|
|
|
primary_id = ""
|
|
|
|
|
primary_email = ""
|
|
|
|
|
for account_id, account_email in mailu_accounts:
|
|
|
|
|
if not primary_id:
|
|
|
|
|
primary_id = account_id
|
|
|
|
|
primary_email = account_email
|
|
|
|
|
if account_email.lower() == mailu_email.lower():
|
|
|
|
|
primary_id = account_id
|
|
|
|
|
primary_email = account_email
|
|
|
|
|
break
|
|
|
|
|
|
|
|
|
|
if mailu_accounts:
|
|
|
|
|
try:
|
|
|
|
|
self._occ(
|
|
|
|
|
[
|
|
|
|
|
"mail:account:update",
|
|
|
|
|
"-q",
|
|
|
|
|
primary_id,
|
|
|
|
|
"--name",
|
|
|
|
|
username_val,
|
|
|
|
|
"--email",
|
|
|
|
|
mailu_email,
|
|
|
|
|
"--imap-host",
|
|
|
|
|
settings.mailu_host,
|
|
|
|
|
"--imap-port",
|
|
|
|
|
"993",
|
|
|
|
|
"--imap-ssl-mode",
|
|
|
|
|
"ssl",
|
|
|
|
|
"--imap-user",
|
|
|
|
|
mailu_email,
|
|
|
|
|
"--imap-password",
|
|
|
|
|
app_pw,
|
|
|
|
|
"--smtp-host",
|
|
|
|
|
settings.mailu_host,
|
|
|
|
|
"--smtp-port",
|
|
|
|
|
"587",
|
|
|
|
|
"--smtp-ssl-mode",
|
|
|
|
|
"tls",
|
|
|
|
|
"--smtp-user",
|
|
|
|
|
mailu_email,
|
|
|
|
|
"--smtp-password",
|
|
|
|
|
app_pw,
|
|
|
|
|
"--auth-method",
|
|
|
|
|
"password",
|
|
|
|
|
]
|
|
|
|
|
)
|
|
|
|
|
updated += 1
|
|
|
|
|
except Exception:
|
|
|
|
|
failures += 1
|
|
|
|
|
continue
|
|
|
|
|
|
|
|
|
|
for account_id, account_email in mailu_accounts:
|
|
|
|
|
if account_id == primary_id:
|
|
|
|
|
continue
|
|
|
|
|
try:
|
|
|
|
|
self._occ(["mail:account:delete", "-q", account_id])
|
|
|
|
|
deleted += 1
|
|
|
|
|
except Exception:
|
|
|
|
|
failures += 1
|
|
|
|
|
else:
|
|
|
|
|
try:
|
|
|
|
|
self._occ(
|
|
|
|
|
[
|
|
|
|
|
"mail:account:create",
|
|
|
|
|
"-q",
|
|
|
|
|
username_val,
|
|
|
|
|
username_val,
|
|
|
|
|
mailu_email,
|
|
|
|
|
settings.mailu_host,
|
|
|
|
|
"993",
|
|
|
|
|
"ssl",
|
|
|
|
|
mailu_email,
|
|
|
|
|
app_pw,
|
|
|
|
|
settings.mailu_host,
|
|
|
|
|
"587",
|
|
|
|
|
"tls",
|
|
|
|
|
mailu_email,
|
|
|
|
|
app_pw,
|
|
|
|
|
"password",
|
|
|
|
|
]
|
|
|
|
|
)
|
|
|
|
|
created += 1
|
|
|
|
|
except Exception:
|
|
|
|
|
failures += 1
|
|
|
|
|
continue
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
accounts_after = self._list_mail_accounts(username_val)
|
|
|
|
|
except Exception:
|
|
|
|
|
failures += 1
|
|
|
|
|
continue
|
|
|
|
|
|
|
|
|
|
mailu_accounts_after = [
|
|
|
|
|
(aid, email) for aid, email in accounts_after if email.lower().endswith(f"@{settings.mailu_domain.lower()}")
|
|
|
|
|
]
|
|
|
|
|
account_count = len(mailu_accounts_after)
|
|
|
|
|
primary_email_after = ""
|
|
|
|
|
editor_mode_ids = []
|
|
|
|
|
for account_id, account_email in mailu_accounts_after:
|
|
|
|
|
editor_mode_ids.append(account_id)
|
|
|
|
|
if account_email.lower() == mailu_email.lower():
|
|
|
|
|
primary_email_after = account_email
|
|
|
|
|
break
|
|
|
|
|
if not primary_email_after:
|
|
|
|
|
primary_email_after = account_email
|
|
|
|
|
|
|
|
|
|
self._set_editor_mode_richtext(editor_mode_ids)
|
|
|
|
|
if user_id:
|
|
|
|
|
self._set_user_mail_meta(user_id, primary_email_after, account_count)
|
|
|
|
|
|
|
|
|
|
summary = NextcloudMailSyncSummary(
|
|
|
|
|
processed=processed,
|
|
|
|
|
created=created,
|
|
|
|
|
updated=updated,
|
|
|
|
|
deleted=deleted,
|
|
|
|
|
skipped=skipped,
|
|
|
|
|
failures=failures,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
logger.info(
|
|
|
|
|
"nextcloud mail sync finished",
|
|
|
|
|
extra={
|
|
|
|
|
"event": "nextcloud_mail_sync",
|
|
|
|
|
"status": "ok" if failures == 0 else "error",
|
|
|
|
|
"processed_count": processed,
|
|
|
|
|
"created_count": created,
|
|
|
|
|
"updated_count": updated,
|
|
|
|
|
"deleted_count": deleted,
|
|
|
|
|
"skipped_count": skipped,
|
|
|
|
|
"failures_count": failures,
|
|
|
|
|
},
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
status = "ok" if failures == 0 else "error"
|
|
|
|
|
return {"status": status, "summary": summary}
|
|
|
|
|
|
|
|
|
|
def _run_shell(self, script: str, check: bool = True) -> None:
|
|
|
|
|
self._executor.exec(
|
|
|
|
|
script,
|
|
|
|
|
timeout_sec=settings.nextcloud_exec_timeout_sec,
|
|
|
|
|
check=check,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
def _external_api(self, method: str, path: str, data: dict[str, Any] | None = None) -> dict[str, Any]:
|
|
|
|
|
if not settings.nextcloud_url:
|
|
|
|
|
raise RuntimeError("nextcloud url not configured")
|
|
|
|
|
if not settings.nextcloud_admin_user or not settings.nextcloud_admin_password:
|
|
|
|
|
raise RuntimeError("nextcloud admin credentials missing")
|
|
|
|
|
url = f"{settings.nextcloud_url}/ocs/v2.php/apps/external/api/v1{path}"
|
|
|
|
|
headers = {"OCS-APIRequest": "true"}
|
|
|
|
|
with httpx.Client(timeout=settings.nextcloud_exec_timeout_sec) as client:
|
|
|
|
|
resp = client.request(
|
|
|
|
|
method,
|
|
|
|
|
url,
|
|
|
|
|
headers=headers,
|
|
|
|
|
auth=(settings.nextcloud_admin_user, settings.nextcloud_admin_password),
|
|
|
|
|
data=data,
|
|
|
|
|
)
|
|
|
|
|
resp.raise_for_status()
|
|
|
|
|
try:
|
|
|
|
|
return resp.json()
|
|
|
|
|
except Exception:
|
|
|
|
|
return {}
|
|
|
|
|
|
|
|
|
|
def run_maintenance(self) -> dict[str, Any]:
|
|
|
|
|
if not settings.nextcloud_namespace:
|
|
|
|
|
raise RuntimeError("nextcloud maintenance not configured")
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
self._run_shell(
|
|
|
|
|
"""
|
|
|
|
|
set -euo pipefail
|
|
|
|
|
if [ ! -d /var/www/html/lib ] && [ -d /usr/src/nextcloud/lib ]; then
|
|
|
|
|
if command -v rsync >/dev/null 2>&1; then
|
|
|
|
|
rsync -a --delete --exclude config --exclude data /usr/src/nextcloud/ /var/www/html/
|
|
|
|
|
else
|
|
|
|
|
cp -a /usr/src/nextcloud/. /var/www/html/
|
|
|
|
|
fi
|
|
|
|
|
fi
|
|
|
|
|
mkdir -p /var/www/html/data
|
|
|
|
|
chown 33:33 /var/www/html || true
|
|
|
|
|
chmod 775 /var/www/html || true
|
|
|
|
|
chown -R 33:33 /var/www/html/apps /var/www/html/custom_apps /var/www/html/data /var/www/html/config 2>/dev/null || true
|
|
|
|
|
""",
|
|
|
|
|
check=False,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
self._occ(["config:app:set", "theming", "name", "--value", "Atlas Cloud"])
|
|
|
|
|
self._occ(["config:app:set", "theming", "slogan", "--value", "Unified access to Atlas services"])
|
|
|
|
|
theming_url = settings.nextcloud_url or "https://cloud.bstein.dev"
|
|
|
|
|
self._occ(["config:app:set", "theming", "url", "--value", theming_url])
|
|
|
|
|
self._occ(["config:app:set", "theming", "color", "--value", "#0f172a"])
|
|
|
|
|
self._occ(["config:app:set", "theming", "disable-user-theming", "--value", "yes"])
|
|
|
|
|
|
|
|
|
|
self._executor.exec(
|
|
|
|
|
["runuser", "-u", "www-data", "--", "php", "/var/www/html/occ", "app:install", "customcss"],
|
|
|
|
|
timeout_sec=settings.nextcloud_exec_timeout_sec,
|
|
|
|
|
check=False,
|
|
|
|
|
)
|
|
|
|
|
self._executor.exec(
|
|
|
|
|
["runuser", "-u", "www-data", "--", "php", "/var/www/html/occ", "app:enable", "customcss"],
|
|
|
|
|
timeout_sec=settings.nextcloud_exec_timeout_sec,
|
|
|
|
|
check=False,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
mail_css = (
|
|
|
|
|
".mail-message-body, .mail-message-body pre, .mail-message-body code, .mail-message-body table {\n"
|
|
|
|
|
" font-family: \"Inter\", \"Source Sans 3\", \"Helvetica Neue\", Arial, sans-serif;\n"
|
|
|
|
|
" font-size: 14px;\n"
|
|
|
|
|
" line-height: 1.6;\n"
|
|
|
|
|
" color: var(--color-main-text);\n"
|
|
|
|
|
"}\n"
|
|
|
|
|
".mail-message-body pre {\n"
|
|
|
|
|
" background: rgba(15, 23, 42, 0.06);\n"
|
|
|
|
|
" padding: 12px;\n"
|
|
|
|
|
" border-radius: 8px;\n"
|
|
|
|
|
"}\n"
|
|
|
|
|
".mail-message-body blockquote {\n"
|
|
|
|
|
" border-left: 3px solid var(--color-border);\n"
|
|
|
|
|
" padding-left: 12px;\n"
|
|
|
|
|
" margin: 8px 0;\n"
|
|
|
|
|
" color: var(--color-text-lighter);\n"
|
|
|
|
|
"}\n"
|
|
|
|
|
".mail-message-body img {\n"
|
|
|
|
|
" max-width: 100%;\n"
|
|
|
|
|
" border-radius: 6px;\n"
|
|
|
|
|
"}\n"
|
|
|
|
|
)
|
|
|
|
|
self._occ(["config:app:set", "customcss", "css", "--value", mail_css])
|
|
|
|
|
self._occ(["config:app:set", "files", "default_quota", "--value", "250 GB"])
|
|
|
|
|
|
|
|
|
|
payload = self._external_api("GET", "?format=json")
|
|
|
|
|
links = payload.get("ocs", {}).get("data", []) if isinstance(payload, dict) else []
|
|
|
|
|
for link in links:
|
|
|
|
|
link_id = link.get("id") if isinstance(link, dict) else None
|
|
|
|
|
if link_id is not None:
|
|
|
|
|
self._external_api("DELETE", f"/sites/{link_id}?format=json")
|
|
|
|
|
|
|
|
|
|
sites = [
|
|
|
|
|
("Vaultwarden", "https://vault.bstein.dev"),
|
|
|
|
|
("Jellyfin", "https://stream.bstein.dev"),
|
|
|
|
|
("Gitea", "https://scm.bstein.dev"),
|
|
|
|
|
("Jenkins", "https://ci.bstein.dev"),
|
|
|
|
|
("Harbor", "https://registry.bstein.dev"),
|
|
|
|
|
("Vault", "https://secret.bstein.dev"),
|
|
|
|
|
("Jitsi", "https://meet.bstein.dev"),
|
|
|
|
|
("Grafana", "https://metrics.bstein.dev"),
|
|
|
|
|
("Chat LLM", "https://chat.ai.bstein.dev"),
|
|
|
|
|
("Vision", "https://draw.ai.bstein.dev"),
|
|
|
|
|
("STT/TTS", "https://talk.ai.bstein.dev"),
|
|
|
|
|
]
|
|
|
|
|
for name, url in sites:
|
|
|
|
|
self._external_api(
|
|
|
|
|
"POST",
|
|
|
|
|
"/sites?format=json",
|
|
|
|
|
data={
|
|
|
|
|
"name": name,
|
|
|
|
|
"url": url,
|
|
|
|
|
"lang": "",
|
|
|
|
|
"type": "link",
|
|
|
|
|
"device": "",
|
|
|
|
|
"icon": "",
|
|
|
|
|
"groups[]": "",
|
|
|
|
|
"redirect": "1",
|
|
|
|
|
},
|
|
|
|
|
)
|
|
|
|
|
except (ExecError, PodSelectionError, TimeoutError) as exc:
|
|
|
|
|
return {"status": "error", "detail": str(exc)}
|
|
|
|
|
except Exception as exc: # noqa: BLE001
|
|
|
|
|
return {"status": "error", "detail": str(exc)}
|
|
|
|
|
|
|
|
|
|
return {"status": "ok", "detail": "maintenance complete"}
|
2026-01-19 16:57:18 -03:00
|
|
|
|
|
|
|
|
|
|
|
|
|
nextcloud = NextcloudService()
|