Files

241 lines
8.0 KiB
Python
Raw Permalink Normal View History

2026-09-09 21:44:05 -05:00
#!/usr/bin/env python3
"""
oko user management script.
Adds/removes end users and provisions private Matrix rooms on ayrc.online
so each user has a space to view their surveillance clips.
Usage:
python3 manage_users.py add --user alice --matrix-id @alice:ayrc.online
python3 manage_users.py remove --user alice
python3 manage_users.py list
python3 manage_users.py info --user alice
Reads the bot token from matrix.token (same file the coordinator uses).
Stores user data in users.json at the project root.
"""
import argparse
import json
import os
import sys
import time
import urllib.error
import urllib.request
SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
USERS_FILE = os.path.join(SCRIPT_DIR, "users.json")
TOKEN_FILE = os.path.join(SCRIPT_DIR, "matrix.token")
MATRIX_HOMESERVER = "https://ayrc.online"
MATRIX_BOT_USER = "@okobot:ayrc.online"
# ── Matrix helpers (raw Client-Server API, no dependencies) ──────────────
def matrix_request(method, path, token, body=None):
"""Send a request to the Matrix Client-Server API and return the parsed
response. Raises on HTTP errors."""
url = f"{MATRIX_HOMESERVER}{path}"
data = json.dumps(body).encode() if body is not None else None
req = urllib.request.Request(url, data=data, method=method)
req.add_header("Authorization", f"Bearer {token}")
req.add_header("Content-Type", "application/json")
try:
with urllib.request.urlopen(req, timeout=30) as resp:
return json.loads(resp.read())
except urllib.error.HTTPError as exc:
err_body = exc.read().decode(errors="replace")
raise SystemExit(f"Matrix API error {exc.code}: {err_body}") from exc
def matrix_create_room(token, name, topic, invite_user):
"""Create an invite-only private room, set its name and topic, then
invite *invite_user*. Returns the room ID."""
resp = matrix_request("POST", "/_matrix/client/v3/createRoom", token, {
"name": name,
"topic": topic,
"room_version": "10",
"is_direct": False,
"visibility": "private",
"invite": [invite_user],
"preset": "private_chat",
"initial_state": [
{
"type": "m.room.join_rules",
"content": {"join_rule": "invite"},
},
{
"type": "m.room.history_visibility",
"content": {"history_visibility": "shared"},
},
],
})
return resp["room_id"]
def matrix_send_notice(token, room_id, text):
"""Send a human-readable notice into a room."""
matrix_request("POST", f"/_matrix/client/v3/rooms/{room_id}/send/m.room.message", token, {
"msgtype": "m.text",
"body": text,
})
# ── User store ───────────────────────────────────────────────────────────
def load_users():
if not os.path.exists(USERS_FILE):
return {}
with open(USERS_FILE) as f:
return json.load(f)
def save_users(users):
with open(USERS_FILE, "w") as f:
json.dump(users, f, indent=2, sort_keys=True)
f.write("\n")
# ── Commands ─────────────────────────────────────────────────────────────
def cmd_add(args):
users = load_users()
name = args.user.lower()
if name in users and users[name].get("active", True):
print(f"User '{name}' already exists and is active.")
sys.exit(1)
matrix_id = args.matrix_id
if not matrix_id.startswith("@"):
print("Matrix user ID must start with '@' (e.g. @alice:ayrc.online)")
sys.exit(1)
bot_token = open(TOKEN_FILE).read().strip()
if not bot_token:
print(f"Cannot read bot token from {TOKEN_FILE}")
sys.exit(1)
display_name = args.display_name or name
room_name = f"oko - {display_name}"
room_topic = f"Private surveillance clips for {display_name}"
print(f"Creating private Matrix room for {display_name}...")
room_id = matrix_create_room(bot_token, room_name, room_topic, matrix_id)
print(f" Room created: {room_id}")
greeting = (
f"Welcome, {display_name}! This is your private oko surveillance room.\n"
f"Clips from your cameras will appear here."
)
matrix_send_notice(bot_token, room_id, greeting)
print(f" Greeting posted to room")
users[name] = {
"display_name": display_name,
"matrix_id": matrix_id,
"room_id": room_id,
"created_at": int(time.time()),
"active": True,
}
save_users(users)
print(f"User '{name}' saved to {USERS_FILE}")
print(f"\nDone. Invite the user to log in to Matrix and accept the room invite.")
def cmd_remove(args):
users = load_users()
name = args.user.lower()
if name not in users:
print(f"User '{name}' not found.")
sys.exit(1)
if not users[name].get("active", True):
print(f"User '{name}' is already inactive.")
sys.exit(0)
users[name]["active"] = False
users[name]["removed_at"] = int(time.time())
save_users(users)
print(f"User '{name}' marked as inactive.")
print(f" Matrix room {users[name]['room_id']} was left intact.")
def cmd_list(args):
users = load_users()
if not users:
print("No users registered.")
return
active = [(n, u) for n, u in users.items() if u.get("active", True)]
inactive = [(n, u) for n, u in users.items() if not u.get("active", True)]
if active:
print(f"Active users ({len(active)}):")
for name, u in active:
print(f" {name:20s} {u['matrix_id']:30s} room {u['room_id']}")
if inactive:
print(f"Inactive users ({len(inactive)}):")
for name, u in inactive:
print(f" {name:20s} {u['matrix_id']:30s} (removed)")
if not active and not inactive:
print("No users registered.")
def cmd_info(args):
users = load_users()
name = args.user.lower()
if name not in users:
print(f"User '{name}' not found.")
sys.exit(1)
u = users[name]
status = "active" if u.get("active", True) else "inactive"
created = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(u.get("created_at", 0)))
print(f"User: {name}")
print(f"Status: {status}")
print(f"Display: {u.get('display_name', '')}")
print(f"Matrix ID: {u.get('matrix_id', '')}")
print(f"Room ID: {u.get('room_id', '')}")
print(f"Created: {created}")
# ── CLI ──────────────────────────────────────────────────────────────────
def main():
parser = argparse.ArgumentParser(
description="oko user management add, remove, and list surveillance users",
)
sub = parser.add_subparsers(dest="command", required=True)
p_add = sub.add_parser("add", help="add a new user and create their Matrix room")
p_add.add_argument("--user", required=True, help="username (lowercase, no spaces)")
p_add.add_argument("--matrix-id", required=True, help="Matrix user ID, e.g. @alice:ayrc.online")
p_add.add_argument("--display-name", default=None, help="human-readable display name (defaults to username)")
p_rm = sub.add_parser("remove", help="deactivate a user")
p_rm.add_argument("--user", required=True, help="username to remove")
p_ls = sub.add_parser("list", help="list all users")
p_info = sub.add_parser("info", help="show details for a user")
p_info.add_argument("--user", required=True, help="username to inspect")
args = parser.parse_args()
if args.command == "add":
cmd_add(args)
elif args.command == "remove":
cmd_remove(args)
elif args.command == "list":
cmd_list(args)
elif args.command == "info":
cmd_info(args)
if __name__ == "__main__":
main()