2026-01-08 01:55:58 -03:00
|
|
|
# services/comms/guest-name-job.yaml
|
2025-12-31 12:00:12 -03:00
|
|
|
apiVersion: batch/v1
|
|
|
|
|
kind: CronJob
|
|
|
|
|
metadata:
|
|
|
|
|
name: guest-name-randomizer
|
2026-01-01 16:29:11 -03:00
|
|
|
namespace: comms
|
2026-01-18 02:50:07 -03:00
|
|
|
labels:
|
|
|
|
|
atlas.bstein.dev/glue: "true"
|
2025-12-31 12:00:12 -03:00
|
|
|
spec:
|
|
|
|
|
schedule: "*/1 * * * *"
|
2026-01-08 00:13:40 -03:00
|
|
|
suspend: false
|
2026-01-08 12:15:59 -03:00
|
|
|
concurrencyPolicy: Forbid
|
2026-01-08 05:34:03 -03:00
|
|
|
successfulJobsHistoryLimit: 1
|
|
|
|
|
failedJobsHistoryLimit: 1
|
2025-12-31 12:00:12 -03:00
|
|
|
jobTemplate:
|
|
|
|
|
spec:
|
2025-12-31 17:33:20 -03:00
|
|
|
backoffLimit: 0
|
2025-12-31 12:00:12 -03:00
|
|
|
template:
|
2026-01-14 14:17:26 -03:00
|
|
|
metadata:
|
|
|
|
|
annotations:
|
|
|
|
|
vault.hashicorp.com/agent-inject: "true"
|
2026-01-14 14:29:29 -03:00
|
|
|
vault.hashicorp.com/agent-pre-populate-only: "true"
|
2026-01-14 14:17:26 -03:00
|
|
|
vault.hashicorp.com/role: "comms"
|
2026-01-14 14:21:58 -03:00
|
|
|
vault.hashicorp.com/agent-inject-secret-turn-secret: "kv/data/atlas/comms/turn-shared-secret"
|
|
|
|
|
vault.hashicorp.com/agent-inject-template-turn-secret: |
|
2026-01-14 14:17:26 -03:00
|
|
|
{{- with secret "kv/data/atlas/comms/turn-shared-secret" -}}{{ .Data.data.TURN_STATIC_AUTH_SECRET }}{{- end -}}
|
2026-01-14 14:21:58 -03:00
|
|
|
vault.hashicorp.com/agent-inject-secret-livekit-primary: "kv/data/atlas/comms/livekit-api"
|
|
|
|
|
vault.hashicorp.com/agent-inject-template-livekit-primary: |
|
2026-01-14 14:17:26 -03:00
|
|
|
{{- with secret "kv/data/atlas/comms/livekit-api" -}}{{ .Data.data.primary }}{{- end -}}
|
2026-01-14 14:21:58 -03:00
|
|
|
vault.hashicorp.com/agent-inject-secret-bot-pass: "kv/data/atlas/comms/atlasbot-credentials-runtime"
|
|
|
|
|
vault.hashicorp.com/agent-inject-template-bot-pass: |
|
2026-01-14 14:17:26 -03:00
|
|
|
{{- with secret "kv/data/atlas/comms/atlasbot-credentials-runtime" -}}{{ index .Data.data "bot-password" }}{{- end -}}
|
2026-01-14 14:21:58 -03:00
|
|
|
vault.hashicorp.com/agent-inject-secret-seeder-pass: "kv/data/atlas/comms/atlasbot-credentials-runtime"
|
|
|
|
|
vault.hashicorp.com/agent-inject-template-seeder-pass: |
|
2026-01-14 14:17:26 -03:00
|
|
|
{{- with secret "kv/data/atlas/comms/atlasbot-credentials-runtime" -}}{{ index .Data.data "seeder-password" }}{{- end -}}
|
2026-01-14 14:21:58 -03:00
|
|
|
vault.hashicorp.com/agent-inject-secret-chat-matrix: "kv/data/atlas/shared/chat-ai-keys-runtime"
|
|
|
|
|
vault.hashicorp.com/agent-inject-template-chat-matrix: |
|
2026-01-14 14:17:26 -03:00
|
|
|
{{- with secret "kv/data/atlas/shared/chat-ai-keys-runtime" -}}{{ .Data.data.matrix }}{{- end -}}
|
2026-01-14 14:21:58 -03:00
|
|
|
vault.hashicorp.com/agent-inject-secret-chat-homepage: "kv/data/atlas/shared/chat-ai-keys-runtime"
|
|
|
|
|
vault.hashicorp.com/agent-inject-template-chat-homepage: |
|
2026-01-14 14:17:26 -03:00
|
|
|
{{- with secret "kv/data/atlas/shared/chat-ai-keys-runtime" -}}{{ .Data.data.homepage }}{{- end -}}
|
2026-01-14 14:21:58 -03:00
|
|
|
vault.hashicorp.com/agent-inject-secret-mas-admin-secret: "kv/data/atlas/comms/mas-admin-client-runtime"
|
|
|
|
|
vault.hashicorp.com/agent-inject-template-mas-admin-secret: |
|
2026-01-14 14:17:26 -03:00
|
|
|
{{- with secret "kv/data/atlas/comms/mas-admin-client-runtime" -}}{{ .Data.data.client_secret }}{{- end -}}
|
2026-01-14 14:21:58 -03:00
|
|
|
vault.hashicorp.com/agent-inject-secret-synapse-db-pass: "kv/data/atlas/comms/synapse-db"
|
|
|
|
|
vault.hashicorp.com/agent-inject-template-synapse-db-pass: |
|
2026-01-14 14:17:26 -03:00
|
|
|
{{- with secret "kv/data/atlas/comms/synapse-db" -}}{{ .Data.data.POSTGRES_PASSWORD }}{{- end -}}
|
2026-01-14 14:21:58 -03:00
|
|
|
vault.hashicorp.com/agent-inject-secret-mas-db-pass: "kv/data/atlas/comms/mas-db"
|
|
|
|
|
vault.hashicorp.com/agent-inject-template-mas-db-pass: |
|
2026-01-14 14:17:26 -03:00
|
|
|
{{- with secret "kv/data/atlas/comms/mas-db" -}}{{ .Data.data.password }}{{- end -}}
|
2026-01-14 14:21:58 -03:00
|
|
|
vault.hashicorp.com/agent-inject-secret-mas-matrix-shared: "kv/data/atlas/comms/mas-secrets-runtime"
|
|
|
|
|
vault.hashicorp.com/agent-inject-template-mas-matrix-shared: |
|
2026-01-14 14:17:26 -03:00
|
|
|
{{- with secret "kv/data/atlas/comms/mas-secrets-runtime" -}}{{ .Data.data.matrix_shared_secret }}{{- end -}}
|
2026-01-14 14:21:58 -03:00
|
|
|
vault.hashicorp.com/agent-inject-secret-mas-kc-secret: "kv/data/atlas/comms/mas-secrets-runtime"
|
|
|
|
|
vault.hashicorp.com/agent-inject-template-mas-kc-secret: |
|
2026-01-14 14:17:26 -03:00
|
|
|
{{- with secret "kv/data/atlas/comms/mas-secrets-runtime" -}}{{ .Data.data.keycloak_client_secret }}{{- end -}}
|
2025-12-31 12:00:12 -03:00
|
|
|
spec:
|
2025-12-31 17:33:20 -03:00
|
|
|
restartPolicy: Never
|
2026-01-14 05:07:23 -03:00
|
|
|
serviceAccountName: comms-vault
|
2026-01-17 18:04:53 -03:00
|
|
|
nodeSelector:
|
|
|
|
|
hardware: rpi5
|
2026-01-08 00:13:40 -03:00
|
|
|
volumes:
|
2026-01-14 05:07:23 -03:00
|
|
|
- name: vault-scripts
|
|
|
|
|
configMap:
|
|
|
|
|
name: comms-vault-env
|
|
|
|
|
defaultMode: 0555
|
2025-12-31 12:00:12 -03:00
|
|
|
containers:
|
|
|
|
|
- name: rename
|
2026-01-17 17:51:21 -03:00
|
|
|
image: registry.bstein.dev/bstein/comms-guest-tools:0.1.0
|
2026-01-08 00:13:40 -03:00
|
|
|
volumeMounts:
|
2026-01-14 05:07:23 -03:00
|
|
|
- name: vault-scripts
|
|
|
|
|
mountPath: /vault/scripts
|
2026-01-08 00:13:40 -03:00
|
|
|
readOnly: true
|
2025-12-31 12:00:12 -03:00
|
|
|
env:
|
|
|
|
|
- name: SYNAPSE_BASE
|
|
|
|
|
value: http://othrys-synapse-matrix-synapse:8008
|
2026-01-08 00:13:40 -03:00
|
|
|
- name: MAS_ADMIN_CLIENT_ID
|
|
|
|
|
value: 01KDXMVQBQ5JNY6SEJPZW6Z8BM
|
|
|
|
|
- name: MAS_ADMIN_CLIENT_SECRET_FILE
|
2026-01-14 14:21:58 -03:00
|
|
|
value: /vault/secrets/mas-admin-secret
|
2026-01-08 00:13:40 -03:00
|
|
|
- name: MAS_ADMIN_API_BASE
|
|
|
|
|
value: http://matrix-authentication-service:8081/api/admin/v1
|
|
|
|
|
- name: MAS_TOKEN_URL
|
|
|
|
|
value: http://matrix-authentication-service:8080/oauth2/token
|
2025-12-31 12:00:12 -03:00
|
|
|
- name: SEEDER_USER
|
|
|
|
|
value: othrys-seeder
|
2026-01-08 11:59:51 -03:00
|
|
|
- name: PGHOST
|
|
|
|
|
value: postgres-service.postgres.svc.cluster.local
|
|
|
|
|
- name: PGPORT
|
|
|
|
|
value: "5432"
|
|
|
|
|
- name: PGDATABASE
|
|
|
|
|
value: synapse
|
|
|
|
|
- name: PGUSER
|
|
|
|
|
value: synapse
|
2025-12-31 12:00:12 -03:00
|
|
|
command:
|
|
|
|
|
- /bin/sh
|
|
|
|
|
- -c
|
|
|
|
|
- |
|
|
|
|
|
set -euo pipefail
|
2026-01-14 05:07:23 -03:00
|
|
|
. /vault/scripts/comms_vault_env.sh
|
2025-12-31 12:00:12 -03:00
|
|
|
python - <<'PY'
|
2026-01-08 00:13:40 -03:00
|
|
|
import base64
|
|
|
|
|
import os
|
|
|
|
|
import random
|
|
|
|
|
import requests
|
2026-01-08 02:00:52 -03:00
|
|
|
import time
|
2026-01-08 00:13:40 -03:00
|
|
|
import urllib.parse
|
2026-01-08 11:59:51 -03:00
|
|
|
import psycopg2
|
2025-12-31 12:00:12 -03:00
|
|
|
|
2026-01-08 00:13:40 -03:00
|
|
|
ADJ = [
|
|
|
|
|
"brisk","calm","eager","gentle","merry","nifty","rapid","sunny","witty","zesty",
|
|
|
|
|
"amber","bold","bright","crisp","daring","frosty","glad","jolly","lively","mellow",
|
|
|
|
|
"quiet","ripe","serene","spry","tidy","vivid","warm","wild","clever","kind",
|
|
|
|
|
]
|
|
|
|
|
NOUN = [
|
|
|
|
|
"otter","falcon","comet","ember","grove","harbor","meadow","raven","river","summit",
|
|
|
|
|
"breeze","cedar","cinder","cove","delta","forest","glade","lark","marsh","peak",
|
|
|
|
|
"pine","quartz","reef","ridge","sable","sage","shore","thunder","vale","zephyr",
|
|
|
|
|
]
|
2025-12-31 12:00:12 -03:00
|
|
|
|
|
|
|
|
BASE = os.environ["SYNAPSE_BASE"]
|
2026-01-08 00:13:40 -03:00
|
|
|
MAS_ADMIN_CLIENT_ID = os.environ["MAS_ADMIN_CLIENT_ID"]
|
|
|
|
|
MAS_ADMIN_CLIENT_SECRET_FILE = os.environ["MAS_ADMIN_CLIENT_SECRET_FILE"]
|
|
|
|
|
MAS_ADMIN_API_BASE = os.environ["MAS_ADMIN_API_BASE"].rstrip("/")
|
|
|
|
|
MAS_TOKEN_URL = os.environ["MAS_TOKEN_URL"]
|
|
|
|
|
SEEDER_USER = os.environ["SEEDER_USER"]
|
2026-01-01 16:29:11 -03:00
|
|
|
ROOM_ALIAS = "#othrys:live.bstein.dev"
|
2026-01-08 11:59:51 -03:00
|
|
|
SERVER_NAME = "live.bstein.dev"
|
2026-01-17 17:30:07 -03:00
|
|
|
STALE_GUEST_MS = 14 * 24 * 60 * 60 * 1000
|
2025-12-31 12:00:12 -03:00
|
|
|
|
2026-01-08 00:13:40 -03:00
|
|
|
def mas_admin_token():
|
|
|
|
|
with open(MAS_ADMIN_CLIENT_SECRET_FILE, "r", encoding="utf-8") as f:
|
|
|
|
|
secret = f.read().strip()
|
|
|
|
|
basic = base64.b64encode(f"{MAS_ADMIN_CLIENT_ID}:{secret}".encode()).decode()
|
2026-01-08 02:00:52 -03:00
|
|
|
last_err = None
|
|
|
|
|
for attempt in range(5):
|
|
|
|
|
try:
|
|
|
|
|
r = requests.post(
|
|
|
|
|
MAS_TOKEN_URL,
|
|
|
|
|
headers={"Authorization": f"Basic {basic}"},
|
|
|
|
|
data={"grant_type": "client_credentials", "scope": "urn:mas:admin"},
|
|
|
|
|
timeout=30,
|
|
|
|
|
)
|
|
|
|
|
r.raise_for_status()
|
|
|
|
|
return r.json()["access_token"]
|
|
|
|
|
except Exception as exc: # noqa: BLE001
|
|
|
|
|
last_err = exc
|
|
|
|
|
time.sleep(2 ** attempt)
|
|
|
|
|
raise last_err
|
2025-12-31 12:00:12 -03:00
|
|
|
|
2026-01-08 00:13:40 -03:00
|
|
|
def mas_user_id(token, username):
|
|
|
|
|
r = requests.get(
|
|
|
|
|
f"{MAS_ADMIN_API_BASE}/users/by-username/{urllib.parse.quote(username)}",
|
|
|
|
|
headers={"Authorization": f"Bearer {token}"},
|
|
|
|
|
timeout=30,
|
|
|
|
|
)
|
|
|
|
|
r.raise_for_status()
|
|
|
|
|
return r.json()["data"]["id"]
|
|
|
|
|
|
|
|
|
|
def mas_personal_session(token, user_id):
|
|
|
|
|
r = requests.post(
|
|
|
|
|
f"{MAS_ADMIN_API_BASE}/personal-sessions",
|
|
|
|
|
headers={"Authorization": f"Bearer {token}"},
|
|
|
|
|
json={
|
|
|
|
|
"actor_user_id": user_id,
|
|
|
|
|
"human_name": "guest-name-randomizer",
|
|
|
|
|
"scope": "urn:matrix:client:api:*",
|
|
|
|
|
"expires_in": 300,
|
|
|
|
|
},
|
|
|
|
|
timeout=30,
|
|
|
|
|
)
|
|
|
|
|
r.raise_for_status()
|
|
|
|
|
data = r.json().get("data", {}).get("attributes", {}) or {}
|
|
|
|
|
return data["access_token"], r.json()["data"]["id"]
|
|
|
|
|
|
|
|
|
|
def mas_revoke_session(token, session_id):
|
|
|
|
|
requests.post(
|
|
|
|
|
f"{MAS_ADMIN_API_BASE}/personal-sessions/{urllib.parse.quote(session_id)}/revoke",
|
|
|
|
|
headers={"Authorization": f"Bearer {token}"},
|
|
|
|
|
json={},
|
|
|
|
|
timeout=30,
|
|
|
|
|
)
|
|
|
|
|
|
2026-01-01 16:29:11 -03:00
|
|
|
def resolve_alias(token, alias):
|
|
|
|
|
headers = {"Authorization": f"Bearer {token}"}
|
|
|
|
|
enc = urllib.parse.quote(alias)
|
|
|
|
|
r = requests.get(f"{BASE}/_matrix/client/v3/directory/room/{enc}", headers=headers)
|
|
|
|
|
r.raise_for_status()
|
|
|
|
|
return r.json()["room_id"]
|
|
|
|
|
|
2026-01-08 00:26:20 -03:00
|
|
|
def room_members(token, room_id):
|
2025-12-31 12:00:12 -03:00
|
|
|
headers = {"Authorization": f"Bearer {token}"}
|
2026-01-08 00:26:20 -03:00
|
|
|
r = requests.get(f"{BASE}/_matrix/client/v3/rooms/{urllib.parse.quote(room_id)}/members", headers=headers)
|
|
|
|
|
r.raise_for_status()
|
|
|
|
|
members = set()
|
2026-01-08 00:13:40 -03:00
|
|
|
existing_names = set()
|
2026-01-08 00:26:20 -03:00
|
|
|
for ev in r.json().get("chunk", []):
|
|
|
|
|
user_id = ev.get("state_key")
|
|
|
|
|
if user_id:
|
|
|
|
|
members.add(user_id)
|
|
|
|
|
disp = (ev.get("content") or {}).get("displayname")
|
|
|
|
|
if disp:
|
|
|
|
|
existing_names.add(disp)
|
|
|
|
|
return members, existing_names
|
|
|
|
|
|
|
|
|
|
def mas_list_users(token):
|
|
|
|
|
headers = {"Authorization": f"Bearer {token}"}
|
|
|
|
|
users = []
|
|
|
|
|
cursor = None
|
2025-12-31 12:00:12 -03:00
|
|
|
while True:
|
2026-01-08 00:26:20 -03:00
|
|
|
url = f"{MAS_ADMIN_API_BASE}/users?page[size]=100"
|
|
|
|
|
if cursor:
|
|
|
|
|
url += f"&page[after]={urllib.parse.quote(cursor)}"
|
|
|
|
|
r = requests.get(url, headers=headers, timeout=30)
|
|
|
|
|
r.raise_for_status()
|
|
|
|
|
data = r.json().get("data", [])
|
|
|
|
|
if not data:
|
|
|
|
|
break
|
|
|
|
|
users.extend(data)
|
|
|
|
|
cursor = data[-1].get("meta", {}).get("page", {}).get("cursor")
|
|
|
|
|
if not cursor:
|
2025-12-31 12:00:12 -03:00
|
|
|
break
|
2026-01-08 00:26:20 -03:00
|
|
|
return users
|
2025-12-31 12:00:12 -03:00
|
|
|
|
2026-01-08 05:34:03 -03:00
|
|
|
def synapse_list_users(token):
|
|
|
|
|
headers = {"Authorization": f"Bearer {token}"}
|
|
|
|
|
users = []
|
|
|
|
|
from_token = None
|
|
|
|
|
while True:
|
|
|
|
|
url = f"{BASE}/_synapse/admin/v2/users?local=true&deactivated=false&limit=100"
|
|
|
|
|
if from_token:
|
|
|
|
|
url += f"&from={urllib.parse.quote(from_token)}"
|
|
|
|
|
r = requests.get(url, headers=headers, timeout=30)
|
|
|
|
|
r.raise_for_status()
|
|
|
|
|
payload = r.json()
|
|
|
|
|
users.extend(payload.get("users", []))
|
|
|
|
|
from_token = payload.get("next_token")
|
|
|
|
|
if not from_token:
|
|
|
|
|
break
|
|
|
|
|
return users
|
|
|
|
|
|
2026-01-17 07:32:57 -03:00
|
|
|
def should_prune_guest(entry, now_ms):
|
|
|
|
|
if not entry.get("is_guest"):
|
|
|
|
|
return False
|
|
|
|
|
last_seen = entry.get("last_seen_ts")
|
|
|
|
|
if last_seen is None:
|
|
|
|
|
return False
|
|
|
|
|
try:
|
|
|
|
|
last_seen = int(last_seen)
|
|
|
|
|
except (TypeError, ValueError):
|
|
|
|
|
return False
|
|
|
|
|
return now_ms - last_seen > STALE_GUEST_MS
|
|
|
|
|
|
|
|
|
|
def prune_guest(token, user_id):
|
|
|
|
|
headers = {"Authorization": f"Bearer {token}"}
|
|
|
|
|
try:
|
|
|
|
|
r = requests.delete(
|
|
|
|
|
f"{BASE}/_synapse/admin/v2/users/{urllib.parse.quote(user_id)}",
|
|
|
|
|
headers=headers,
|
|
|
|
|
params={"erase": "true"},
|
|
|
|
|
timeout=30,
|
|
|
|
|
)
|
|
|
|
|
except Exception as exc: # noqa: BLE001
|
|
|
|
|
print(f"guest prune failed for {user_id}: {exc}")
|
|
|
|
|
return False
|
|
|
|
|
if r.status_code in (200, 202, 204, 404):
|
|
|
|
|
return True
|
|
|
|
|
print(f"guest prune failed for {user_id}: {r.status_code} {r.text}")
|
|
|
|
|
return False
|
|
|
|
|
|
2026-01-08 00:26:20 -03:00
|
|
|
def user_id_for_username(username):
|
|
|
|
|
return f"@{username}:live.bstein.dev"
|
|
|
|
|
|
|
|
|
|
def get_displayname(token, user_id):
|
|
|
|
|
headers = {"Authorization": f"Bearer {token}"}
|
|
|
|
|
r = requests.get(f"{BASE}/_matrix/client/v3/profile/{urllib.parse.quote(user_id)}", headers=headers)
|
|
|
|
|
r.raise_for_status()
|
|
|
|
|
return r.json().get("displayname")
|
|
|
|
|
|
2026-01-08 05:34:03 -03:00
|
|
|
def get_displayname_admin(token, user_id):
|
|
|
|
|
headers = {"Authorization": f"Bearer {token}"}
|
|
|
|
|
r = requests.get(
|
|
|
|
|
f"{BASE}/_synapse/admin/v2/users/{urllib.parse.quote(user_id)}",
|
|
|
|
|
headers=headers,
|
|
|
|
|
timeout=30,
|
|
|
|
|
)
|
|
|
|
|
if r.status_code == 404:
|
|
|
|
|
return None
|
|
|
|
|
r.raise_for_status()
|
|
|
|
|
return r.json().get("displayname")
|
|
|
|
|
|
2026-01-08 00:26:20 -03:00
|
|
|
def set_displayname(token, room_id, user_id, name, in_room):
|
2025-12-31 12:00:12 -03:00
|
|
|
headers = {"Authorization": f"Bearer {token}"}
|
|
|
|
|
payload = {"displayname": name}
|
2026-01-08 00:26:20 -03:00
|
|
|
r = requests.put(
|
|
|
|
|
f"{BASE}/_matrix/client/v3/profile/{urllib.parse.quote(user_id)}/displayname",
|
|
|
|
|
headers=headers,
|
|
|
|
|
json=payload,
|
|
|
|
|
)
|
2025-12-31 12:00:12 -03:00
|
|
|
r.raise_for_status()
|
2026-01-08 00:26:20 -03:00
|
|
|
if not in_room:
|
|
|
|
|
return
|
2026-01-01 16:29:11 -03:00
|
|
|
state_url = f"{BASE}/_matrix/client/v3/rooms/{urllib.parse.quote(room_id)}/state/m.room.member/{urllib.parse.quote(user_id)}"
|
2026-01-08 00:26:20 -03:00
|
|
|
content = {"membership": "join", "displayname": name}
|
|
|
|
|
requests.put(state_url, headers=headers, json=content, timeout=30)
|
2025-12-31 12:00:12 -03:00
|
|
|
|
2026-01-08 05:34:03 -03:00
|
|
|
def set_displayname_admin(token, user_id, name):
|
|
|
|
|
headers = {"Authorization": f"Bearer {token}"}
|
|
|
|
|
payload = {"displayname": name}
|
|
|
|
|
r = requests.put(
|
|
|
|
|
f"{BASE}/_synapse/admin/v2/users/{urllib.parse.quote(user_id)}",
|
|
|
|
|
headers=headers,
|
|
|
|
|
json=payload,
|
|
|
|
|
timeout=30,
|
|
|
|
|
)
|
|
|
|
|
if r.status_code in (200, 201, 204):
|
|
|
|
|
return True
|
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
|
def needs_rename_username(username):
|
|
|
|
|
return username.isdigit() or username.startswith("guest-")
|
|
|
|
|
|
|
|
|
|
def needs_rename_display(display):
|
|
|
|
|
return not display or display.isdigit() or display.startswith("guest-")
|
|
|
|
|
|
2026-01-08 11:59:51 -03:00
|
|
|
def db_rename_numeric(existing_names):
|
2026-01-08 12:12:08 -03:00
|
|
|
profile_rows = []
|
|
|
|
|
profile_index = {}
|
|
|
|
|
users = []
|
2026-01-08 11:59:51 -03:00
|
|
|
conn = psycopg2.connect(
|
|
|
|
|
host=os.environ["PGHOST"],
|
|
|
|
|
port=int(os.environ["PGPORT"]),
|
|
|
|
|
dbname=os.environ["PGDATABASE"],
|
|
|
|
|
user=os.environ["PGUSER"],
|
|
|
|
|
password=os.environ["PGPASSWORD"],
|
|
|
|
|
)
|
|
|
|
|
try:
|
|
|
|
|
with conn:
|
|
|
|
|
with conn.cursor() as cur:
|
2026-01-08 12:12:08 -03:00
|
|
|
cur.execute(
|
|
|
|
|
"SELECT user_id, full_user_id, displayname FROM profiles WHERE full_user_id ~ %s",
|
|
|
|
|
(f"^@\\d+:{SERVER_NAME}$",),
|
|
|
|
|
)
|
|
|
|
|
profile_rows = cur.fetchall()
|
|
|
|
|
profile_index = {row[1]: row for row in profile_rows}
|
|
|
|
|
for user_id, full_user_id, display in profile_rows:
|
|
|
|
|
if display and not needs_rename_display(display):
|
|
|
|
|
continue
|
|
|
|
|
new = None
|
|
|
|
|
for _ in range(30):
|
|
|
|
|
candidate = f"{random.choice(ADJ)}-{random.choice(NOUN)}"
|
|
|
|
|
if candidate not in existing_names:
|
|
|
|
|
new = candidate
|
|
|
|
|
existing_names.add(candidate)
|
|
|
|
|
break
|
|
|
|
|
if not new:
|
|
|
|
|
continue
|
|
|
|
|
cur.execute(
|
|
|
|
|
"UPDATE profiles SET displayname = %s WHERE full_user_id = %s",
|
|
|
|
|
(new, full_user_id),
|
|
|
|
|
)
|
|
|
|
|
|
2026-01-08 11:59:51 -03:00
|
|
|
cur.execute(
|
|
|
|
|
"SELECT name FROM users WHERE name ~ %s",
|
2026-01-08 12:03:53 -03:00
|
|
|
(f"^@\\d+:{SERVER_NAME}$",),
|
2026-01-08 11:59:51 -03:00
|
|
|
)
|
2026-01-08 12:12:08 -03:00
|
|
|
users = [row[0] for row in cur.fetchall()]
|
|
|
|
|
if not users:
|
2026-01-08 11:59:51 -03:00
|
|
|
return
|
|
|
|
|
cur.execute(
|
2026-01-08 12:12:08 -03:00
|
|
|
"SELECT user_id, full_user_id FROM profiles WHERE full_user_id = ANY(%s)",
|
|
|
|
|
(users,),
|
2026-01-08 11:59:51 -03:00
|
|
|
)
|
2026-01-08 12:12:08 -03:00
|
|
|
for existing_full in cur.fetchall():
|
|
|
|
|
profile_index.setdefault(existing_full[1], existing_full)
|
|
|
|
|
|
|
|
|
|
for full_user_id in users:
|
|
|
|
|
if full_user_id in profile_index:
|
2026-01-08 11:59:51 -03:00
|
|
|
continue
|
2026-01-08 12:12:08 -03:00
|
|
|
localpart = full_user_id.split(":", 1)[0].lstrip("@")
|
2026-01-08 11:59:51 -03:00
|
|
|
new = None
|
|
|
|
|
for _ in range(30):
|
2026-01-08 12:03:53 -03:00
|
|
|
candidate = f"{random.choice(ADJ)}-{random.choice(NOUN)}"
|
2026-01-08 11:59:51 -03:00
|
|
|
if candidate not in existing_names:
|
|
|
|
|
new = candidate
|
|
|
|
|
existing_names.add(candidate)
|
|
|
|
|
break
|
|
|
|
|
if not new:
|
|
|
|
|
continue
|
|
|
|
|
cur.execute(
|
2026-01-08 12:15:59 -03:00
|
|
|
"INSERT INTO profiles (user_id, displayname, full_user_id) VALUES (%s, %s, %s) "
|
|
|
|
|
"ON CONFLICT (full_user_id) DO UPDATE SET displayname = EXCLUDED.displayname",
|
2026-01-08 12:12:08 -03:00
|
|
|
(localpart, new, full_user_id),
|
2026-01-08 11:59:51 -03:00
|
|
|
)
|
|
|
|
|
finally:
|
|
|
|
|
conn.close()
|
|
|
|
|
|
2026-01-08 00:13:40 -03:00
|
|
|
admin_token = mas_admin_token()
|
|
|
|
|
seeder_id = mas_user_id(admin_token, SEEDER_USER)
|
2026-01-08 00:26:20 -03:00
|
|
|
seeder_token, seeder_session = mas_personal_session(admin_token, seeder_id)
|
2026-01-08 00:13:40 -03:00
|
|
|
try:
|
2026-01-08 00:26:20 -03:00
|
|
|
room_id = resolve_alias(seeder_token, ROOM_ALIAS)
|
|
|
|
|
members, existing = room_members(seeder_token, room_id)
|
2026-01-08 05:47:21 -03:00
|
|
|
users = mas_list_users(admin_token)
|
|
|
|
|
mas_usernames = set()
|
|
|
|
|
for user in users:
|
|
|
|
|
attrs = user.get("attributes") or {}
|
|
|
|
|
username = attrs.get("username") or ""
|
|
|
|
|
if username:
|
|
|
|
|
mas_usernames.add(username)
|
|
|
|
|
legacy_guest = attrs.get("legacy_guest")
|
|
|
|
|
if not username:
|
|
|
|
|
continue
|
|
|
|
|
if not (legacy_guest or needs_rename_username(username)):
|
|
|
|
|
continue
|
|
|
|
|
user_id = user_id_for_username(username)
|
|
|
|
|
access_token, session_id = mas_personal_session(admin_token, user["id"])
|
|
|
|
|
try:
|
|
|
|
|
display = get_displayname(access_token, user_id)
|
|
|
|
|
if display and not needs_rename_display(display):
|
|
|
|
|
continue
|
|
|
|
|
new = None
|
|
|
|
|
for _ in range(30):
|
|
|
|
|
candidate = f"{random.choice(ADJ)}-{random.choice(NOUN)}"
|
|
|
|
|
if candidate not in existing:
|
|
|
|
|
new = candidate
|
|
|
|
|
existing.add(candidate)
|
|
|
|
|
break
|
|
|
|
|
if not new:
|
|
|
|
|
continue
|
|
|
|
|
set_displayname(access_token, room_id, user_id, new, user_id in members)
|
|
|
|
|
finally:
|
|
|
|
|
mas_revoke_session(admin_token, session_id)
|
2026-01-08 00:26:20 -03:00
|
|
|
|
2026-01-08 06:14:32 -03:00
|
|
|
try:
|
|
|
|
|
entries = synapse_list_users(seeder_token)
|
|
|
|
|
except Exception as exc: # noqa: BLE001
|
|
|
|
|
print(f"synapse admin list skipped: {exc}")
|
|
|
|
|
entries = []
|
2026-01-17 07:32:57 -03:00
|
|
|
now_ms = int(time.time() * 1000)
|
2026-01-08 06:14:32 -03:00
|
|
|
for entry in entries:
|
2026-01-08 05:47:21 -03:00
|
|
|
user_id = entry.get("name") or ""
|
|
|
|
|
if not user_id.startswith("@"):
|
|
|
|
|
continue
|
|
|
|
|
localpart = user_id.split(":", 1)[0].lstrip("@")
|
|
|
|
|
if localpart in mas_usernames:
|
|
|
|
|
continue
|
|
|
|
|
is_guest = entry.get("is_guest")
|
2026-01-17 07:32:57 -03:00
|
|
|
if is_guest and should_prune_guest(entry, now_ms):
|
|
|
|
|
if prune_guest(seeder_token, user_id):
|
|
|
|
|
continue
|
2026-01-08 05:47:21 -03:00
|
|
|
if not (is_guest or needs_rename_username(localpart)):
|
|
|
|
|
continue
|
|
|
|
|
display = get_displayname_admin(seeder_token, user_id)
|
2026-01-08 05:34:03 -03:00
|
|
|
if display and not needs_rename_display(display):
|
2026-01-08 00:26:20 -03:00
|
|
|
continue
|
2026-01-08 00:13:40 -03:00
|
|
|
new = None
|
|
|
|
|
for _ in range(30):
|
2026-01-08 00:15:41 -03:00
|
|
|
candidate = f"{random.choice(ADJ)}-{random.choice(NOUN)}"
|
2026-01-08 00:13:40 -03:00
|
|
|
if candidate not in existing:
|
|
|
|
|
new = candidate
|
|
|
|
|
existing.add(candidate)
|
|
|
|
|
break
|
|
|
|
|
if not new:
|
|
|
|
|
continue
|
2026-01-08 05:47:21 -03:00
|
|
|
if not set_displayname_admin(seeder_token, user_id, new):
|
|
|
|
|
continue
|
2026-01-08 11:59:51 -03:00
|
|
|
db_rename_numeric(existing)
|
2026-01-08 05:47:21 -03:00
|
|
|
finally:
|
|
|
|
|
mas_revoke_session(admin_token, seeder_session)
|
2026-01-17 07:32:57 -03:00
|
|
|
PY
|