ForcePilot/backend/package/yuxi/channel/extensions/tlon/approval.py

304 lines
10 KiB
Python
Raw Normal View History

import json
import logging
import time
import uuid
from dataclasses import asdict
from pathlib import Path
from yuxi.channel.extensions.tlon.types import ApprovalType, PendingApproval
from yuxi.channel.extensions.tlon.utils import normalize_ship, is_owner
from yuxi.channel.extensions.tlon.send import send_dm
from yuxi.channel.extensions.tlon.security import (
check_dm_allowlist, check_channel_authorization,
block_ship, unblock_ship, get_blocked_ships,
)
logger = logging.getLogger(__name__)
PENDING_APPROVALS_KEY = "pending_approvals"
def _approvals_file_path() -> Path:
return Path(__file__).parent / "pending_approvals.json"
def load_pending_approvals() -> list[PendingApproval]:
filepath = _approvals_file_path()
if not filepath.exists():
return []
try:
data = json.loads(filepath.read_text(encoding="utf-8"))
result = []
for item in data:
ap_type = ApprovalType.DM
raw_type = item.get("type", "dm")
if raw_type == "channel":
ap_type = ApprovalType.CHANNEL
elif raw_type == "group":
ap_type = ApprovalType.GROUP
result.append(PendingApproval(
id=item.get("id", ""),
type=ap_type,
requesting_ship=item.get("requesting_ship", ""),
account_id=item.get("account_id", "default"),
channel_nest=item.get("channel_nest"),
group_flag=item.get("group_flag"),
original_message=item.get("original_message"),
message_preview=item.get("message_preview"),
created_at=item.get("created_at", time.time()),
))
return result
except Exception as e:
logger.warning("[tlon] Failed to load pending approvals: %s", e)
return []
def save_pending_approvals(approvals: list[PendingApproval]) -> None:
filepath = _approvals_file_path()
try:
data = []
for a in approvals:
data.append({
"id": a.id,
"type": a.type.value,
"requesting_ship": a.requesting_ship,
"account_id": a.account_id,
"channel_nest": a.channel_nest,
"group_flag": a.group_flag,
"original_message": a.original_message,
"message_preview": a.message_preview,
"created_at": a.created_at,
})
filepath.write_text(json.dumps(data, ensure_ascii=False, indent=2), encoding="utf-8")
except Exception as e:
logger.warning("[tlon] Failed to save pending approvals: %s", e)
async def create_pending_approval(approval_type: str, requesting_ship: str, *,
account_id: str = "default",
channel_nest: str | None = None,
group_flag: str | None = None,
original_message: str | None = None,
message_preview: str | None = None,
config: dict | None = None,
approval_store: list[PendingApproval]) -> PendingApproval:
approval_id = f"{approval_type}-{int(time.time() * 1000)}-{uuid.uuid4().hex[:6]}"
existing = [a for a in approval_store
if a.requesting_ship == normalize_ship(requesting_ship)
and a.type.value == approval_type
and (not channel_nest or a.channel_nest == channel_nest)]
if existing:
return existing[0]
ap_type = ApprovalType.DM
if approval_type == "channel":
ap_type = ApprovalType.CHANNEL
elif approval_type == "group":
ap_type = ApprovalType.GROUP
approval = PendingApproval(
id=approval_id,
type=ap_type,
requesting_ship=normalize_ship(requesting_ship),
account_id=account_id,
channel_nest=channel_nest,
group_flag=group_flag,
original_message=original_message,
message_preview=message_preview,
)
approval_store.append(approval)
save_pending_approvals(approval_store)
return approval
async def send_owner_notification(ctx, client, account: dict,
approval_type: str, requesting_ship: str, *,
nest: str | None = None,
group_flag: str | None = None) -> None:
owner_ship = account.get("owner_ship")
if not owner_ship:
return
ship_display = account.get("ship", "bot")
if approval_type == "dm":
msg = (
f"**Tlon Approval Request**\n\n"
f"Type: DM Request\n"
f"From: {requesting_ship}\n\n"
f"Reply with:\n"
f"- `approve` — Allow this user to DM\n"
f"- `deny` — Reject this request\n"
f"- `block` — Block this user"
)
elif approval_type == "channel":
msg = (
f"**Tlon Approval Request**\n\n"
f"Type: Channel Access\n"
f"From: {requesting_ship}\n"
f"Channel: {nest or 'unknown'}\n\n"
f"Reply with:\n"
f"- `approve` — Grant channel access\n"
f"- `deny` — Reject request\n"
f"- `block` — Block this user"
)
elif approval_type == "group":
msg = (
f"**Tlon Approval Request**\n\n"
f"Type: Group Invite\n"
f"From: {requesting_ship}\n"
f"Group: {group_flag or 'unknown'}\n\n"
f"Reply with:\n"
f"- `approve` — Accept invite\n"
f"- `deny` — Reject invite\n"
f"- `block` — Block this user"
)
else:
msg = f"**Tlon Approval Request**\n\nFrom: {requesting_ship}"
try:
await send_dm(client, ship_display, owner_ship, msg)
except Exception as e:
logger.warning("[tlon] Failed to send approval notification: %s", e)
async def handle_approval_command(text: str, ctx, client, account: dict, *,
pending_approvals: list[PendingApproval],
dm_allowlist: set[str]) -> str | None:
parts = text.strip().split()
if not parts:
return None
cmd = parts[0].lower()
ship_display = account.get("ship", "bot")
if cmd == "approve":
target_id = parts[1] if len(parts) > 1 else None
return await _handle_approve(target_id, pending_approvals, dm_allowlist,
client, ship_display, account)
elif cmd == "deny":
target_id = parts[1] if len(parts) > 1 else None
return await _handle_deny(target_id, pending_approvals, account)
elif cmd == "block":
target_id = parts[1] if len(parts) > 1 else None
return await _handle_block(target_id, pending_approvals, client, account)
elif cmd == "blocked":
blocked = await get_blocked_ships(client)
if blocked:
return "**Blocked Ships:**\n" + "\n".join(f"- {s}" for s in blocked)
return "No blocked ships."
elif cmd == "pending":
if not pending_approvals:
return "No pending approval requests."
lines = ["**Pending Approvals:**"]
for a in pending_approvals:
lines.append(
f"- `{a.id[:8]}` | {a.type.value} | {a.requesting_ship} "
f"{'in ' + a.channel_nest if a.channel_nest else ''}"
)
return "\n".join(lines)
elif cmd == "unblock" and len(parts) > 1:
target_ship = normalize_ship(parts[1])
await unblock_ship(client, target_ship)
return f"Unblocked {target_ship}"
return None
async def _handle_approve(target_id: str | None,
approvals: list[PendingApproval],
dm_allowlist: set[str],
client, ship_display: str,
account: dict) -> str:
if target_id:
matching = [a for a in approvals if a.id.startswith(target_id)]
else:
matching = [approvals[-1]] if approvals else []
if not matching:
return "No matching approval found."
for a in matching:
if a.type == ApprovalType.DM:
dm_allowlist.add(a.requesting_ship)
elif a.type == ApprovalType.CHANNEL:
pass
elif a.type == ApprovalType.GROUP:
try:
await client.poke("groups", "group-join", {
"flag": a.group_flag,
"share-contact": True,
})
except Exception:
pass
approvals.remove(a)
save_pending_approvals(approvals)
approved_ships = [a.requesting_ship for a in matching]
return f"Approved: {', '.join(approved_ships)}"
async def _handle_deny(target_id: str | None,
approvals: list[PendingApproval],
account: dict) -> str:
if target_id:
matching = [a for a in approvals if a.id.startswith(target_id)]
else:
matching = [approvals[-1]] if approvals else []
if not matching:
return "No matching approval found."
denied_ships = []
for a in matching:
denied_ships.append(a.requesting_ship)
approvals.remove(a)
save_pending_approvals(approvals)
return f"Denied: {', '.join(denied_ships)}"
async def _handle_block(target_id: str | None,
approvals: list[PendingApproval],
client, account: dict) -> str:
if target_id:
matching = [a for a in approvals if a.id.startswith(target_id)]
else:
matching = [approvals[-1]] if approvals else []
if not matching:
return "No matching approval found."
blocked_ships = []
for a in matching:
await block_ship(client, a.requesting_ship)
blocked_ships.append(a.requesting_ship)
approvals.remove(a)
save_pending_approvals(approvals)
return f"Blocked: {', '.join(blocked_ships)}"
def get_approval_by_id(approval_id: str,
approvals: list[PendingApproval]) -> PendingApproval | None:
for a in approvals:
if a.id == approval_id:
return a
return None
def remove_approval(approval_id: str,
approvals: list[PendingApproval]) -> bool:
a = get_approval_by_id(approval_id, approvals)
if a:
approvals.remove(a)
save_pending_approvals(approvals)
return True
return False