Documentation Index
Fetch the complete documentation index at: https://docs.7331.org/llms.txt
Use this file to discover all available pages before exploring further.
v1.12.0
Stranger geo enrichment + session blocklist (2026-05-10)Stranger identity enrichment
When a stranger match is created, the server now captures the stranger’s country (resolved from their IP via geo lookup) and includes it in thematch:found peer object as country_code (ISO 3166-1 alpha-2, or null if unavailable). This gives both parties a lightweight location signal without exposing the raw IP.
Stranger moderation blocklist
Moderators can now block a stranger’s anonymous session from future matchmaking. The block is based on a signature derived from the stranger’s session token — it survives reconnects as long as the session cookie is unchanged. Blocked strangers receive 403ERR_STRANGER_BLOCKED when they attempt to enqueue.
REST surface changes
POST /v1/portal/match— new 403ERR_STRANGER_BLOCKEDfor blocked stranger sessions.
v1.11.0
Direct chat archive + multi-interest matching + slot limits (2026-05-06)Matchmaking overhaul
Multi-interest queuing. ThePOST /v1/portal/match body now accepts an interests array (1–3 tags) instead of a single interest string. Users are placed into all listed queues simultaneously — the first peer match on any shared tag wins. The matched tags are listed in the opening system message: "Chat started — say hi to {peer}! Matched on: tag1, tag2".
Open-ended tags. Interest tags are now free-form alphanumeric strings (1–12 characters). The server no longer maintains an allow-list — any word-like tag is accepted.
Concurrent DIRECT slot limits. Each user has a cap on the number of simultaneous active DIRECT chats:
| Plan | Slots |
|---|---|
| Free | 1 |
| Premium | 3 |
ERR_MATCH_SLOT_LIMIT.
Per-user ZSET tracking. Active and ended DIRECT chats are now tracked in per-user Redis sorted sets (score = expiry timestamp) instead of a single string key. This enables multi-slot support and lets the slot counter ignore expired entries automatically.
Chat archive lifecycle
When a DIRECT chat ends (explicit leave or grace expiry), it now enters a 24-hour read-only archive rather than being deleted:- The chat hash and message stream are preserved with a 24h TTL.
- The chat is moved to the user’s ended-chat ZSET (score = archive deadline).
GET /v1/portal/chatsincludes archived entries with"archived": true.- The socket
chat:listevent surfaces archived chats on reconnect. - Archived chats render read-only and dimmed in the sidebar; they disappear after 24h.
REST surface changes
POST /v1/portal/match— body field renamedinterest→interests(array). New 409ERR_MATCH_SLOT_LIMIT.GET /v1/portal/match/status— response field renamedinterest→interests(array).GET /v1/portal/chats— response entries now include"archived": boolean.GET /v1/portal/match/statistics— new endpoint: returns online count and top 5 tags by queue depth. Requires registered session (strangers receive 401).
v1.10.0
Portal repositioning + status-code standardization (2026-04-21) Docs now lead with Portal (shared virtual browsers + E2EE chat + 1-on-1 matchmaking). Bot management is reframed as a community-operator integration surface. New concept pages for chat types, E2EE, and matchmaking. API surface changes:- Match endpoints documented:
POST /v1/portal/match,GET /v1/portal/match/status,POST /v1/portal/match/dequeue,POST /v1/portal/match/leave. Dequeue was flipped fromDELETEto POST this release to align with the v1.9.0 POST+GET-only rule — the old path is gone. - DIRECT-chat system messages persisted on explicit
match/leave— peer sees ” left the chat” in their timeline and inget_history. matchenqueue now returns 409ERR_MATCH_NO_PEERS_ONLINEwhen the caller is the only user online (previously accepted and sat alone until the 120s timeout).- Notifications grouped into their own nav section with full coverage: unread count, mark-read (409 vs 404 now disambiguated), mark-all-read, admin delete.
api/routes/portal.py and admin/chats.py:
| Endpoint | Before | After | Why |
|---|---|---|---|
POST /v1/portal/match/leave (drift) | 404 or 409 collapsed | 404 for missing DIRECT | Distinct client-drift signal |
POST /v1/notifications/{id}/read | 404 for already-read | 409 ERR_NOTIFICATION_ALREADY_READ | Not a 404 when the row exists |
| Browse private chat | 400 | 403 | Permission denial, not validation |
| Chat start failure (transient) | 422 | 503 | Infra unavailable, not bad input |
| Chat-not-running on moderation | 404 | 409 | Wrong chat state, not absent |
| Invite / clipboard infra failure | 500 | 503 | External service, not a server bug |
core/constants/errors.py:
ERR_ROOM_NOT_RUNNING— “Chat not found or not running” → “Chat is not running”ERR_NOTIFICATION_ALREADY_READ— “Notification not found or already read” → “Notification is already read”- Added
ERR_ROOM_START_FAILED,ERR_CANNOT_BROWSE_PRIVATE,ERR_MATCH_NO_PEERS_ONLINE.
get_history.
v1.9.0
POST+GET-Only API (2026-04-20) Breaking changes (no deprecation period). The entire REST surface is now POST+GET-only — no PUT, no DELETE. Every mutation has an explicit verb in its path, and target entity IDs live in the JSON body, not the URL path. Method flips (URL unchanged):PUT /v1/notifications/{id}/read→POSTPUT /v1/notifications/read-all→POST
PUT /v1/users/me→POST /v1/users/me/updatePUT /v1/bot/me→POST /v1/bot/me/updatePUT /v1/bot/users/{user_id}→POST /v1/bot/users/{user_id}/updatePUT /v1/admin/bots/{bot_id}/update→POST /v1/admin/bots/{bot_id}/updatePUT /v1/admin/users/{user_id}→POST /v1/admin/users/{user_id}/updatePUT /v1/admin/chats/{room_id}→POST /v1/admin/chats/{room_id}/updateDELETE /v1/admin/bots/{bot_id}→POST /v1/admin/bots/{bot_id}/deleteDELETE /v1/admin/notifications/{id}→POST /v1/admin/notifications/{id}/deleteDELETE /v1/admin/users/{user_id}→POST /v1/admin/users/{user_id}/deleteDELETE /v1/admin/chats/{room_id}→POST /v1/admin/chats/{room_id}/deleteDELETE /v1/portal/chats/{room_id}→POST /v1/portal/chats/{room_id}/deletePUT /v1/portal/chats/{room_id}/config→POST /v1/portal/chats/{room_id}/config/updateDELETE /v1/portal/chats/{room_id}/invites/{code}→POST /v1/portal/chats/{room_id}/invites/{code}/delete
POST /v1/portal/chats/{room_id}/kicks→POST /v1/portal/chats/{room_id}/kick(body:{user_id, ...})POST /v1/portal/chats/{room_id}/bans→POST /v1/portal/chats/{room_id}/banPOST /v1/portal/chats/{room_id}/mutes→POST /v1/portal/chats/{room_id}/mutePOST /v1/portal/chats/{room_id}/warnings→POST /v1/portal/chats/{room_id}/warnPOST /v1/portal/chats/{room_id}/timeouts→POST /v1/portal/chats/{room_id}/timeoutDELETE /v1/portal/chats/{room_id}/bans/{user_id}→POST /v1/portal/chats/{room_id}/unban(body:{user_id, reason})DELETE /v1/portal/chats/{room_id}/mutes/{user_id}→POST /v1/portal/chats/{room_id}/unmute(body:{user_id})POST /v1/portal/hard-mute/{user_id}→POST /v1/portal/hard-mute(body:{user_id, ...})POST /v1/portal/hard-unmute/{user_id}→POST /v1/portal/hard-unmute(body:{user_id})
DELETE /v1/portal/chats/{room_id}/messages/{message_id}→POST /v1/portal/chats/{room_id}/messages/delete(body:{ids: [...]}, 1–100 ids)DELETE /v1/portal/chats/{room_id}/messages/mine→POST /v1/portal/chats/{room_id}/messages/delete-mineDELETE /v1/portal/chats/{room_id}/messages/last/{count}→POST /v1/portal/chats/{room_id}/messages/delete-last(body:{count})
POST /v1/admin/punishments/user/{user_id}/ban→POST /v1/admin/punishments/banPOST /v1/admin/punishments/user/{user_id}/warn→POST /v1/admin/punishments/warnPOST /v1/admin/punishments/user/{user_id}/timeout→POST /v1/admin/punishments/timeoutPOST /v1/admin/punishments/user/{user_id}/unban→POST /v1/admin/punishments/unban
POST /v1/admin/subscriptions/user/{user_id}/grant→POST /v1/admin/subscriptions/grant(body:{user_id, tier, duration_days, reason})POST /v1/admin/subscriptions/user/{user_id}/trial→ folded into/grantwithtier=TRIALPOST /v1/admin/subscriptions/user/{user_id}/revoke→POST /v1/admin/subscriptions/revoke(body:{user_id, reason})
user_id / message_id / count from path to JSON body.
v1.8.0
Chat Moderation Route Restructure (2026-04-19) Breaking changes (no deprecation period):- Chat moderation create-punishment endpoints now use collection-resource URLs with
user_idin the JSON body instead of the path:POST /v1/portal/chats/{room_id}/ban/{user_id}→POST /v1/portal/chats/{room_id}/bans(user_id in body)POST /v1/portal/chats/{room_id}/mute/{user_id}→POST /v1/portal/chats/{room_id}/mutes(user_id in body)POST /v1/portal/chats/{room_id}/kick/{user_id}→POST /v1/portal/chats/{room_id}/kicks(user_id in body)POST /v1/portal/chats/{room_id}/warn/{user_id}→POST /v1/portal/chats/{room_id}/warnings(user_id in body)POST /v1/portal/chats/{room_id}/timeout/{user_id}→POST /v1/portal/chats/{room_id}/timeouts(user_id in body)
- Reversal endpoints are now
DELETEon the collection resource:POST /v1/portal/chats/{room_id}/unban/{user_id}→DELETE /v1/portal/chats/{room_id}/bans/{user_id}POST /v1/portal/chats/{room_id}/unmute/{user_id}→DELETE /v1/portal/chats/{room_id}/mutes/{user_id}
- Aligns the chat moderation surface with REST collection-resource conventions (POST creates, DELETE removes).
- Request/response payloads and authorization rules are otherwise unchanged.
v1.7.0
Notifications, Membership History, UI Polish New features:- Notification system — admins can send targeted or broadcast notifications to users. 5 severity types: success (green), info (blue), warning (amber), danger (red), system (gray). Rich body rendering with bold and links.
- Notification templates — automated notifications on platform events: welcome, banned, unbanned, timed out, warned, kicked, trial started, subscription upgraded/revoked, permission changed, new IP login. Wired at the service layer so every code path (routes, cron jobs, socket handlers) triggers notifications.
- Chat membership history — every join, leave, kick, and ban is logged with join method (direct, vanity, invite) and invite code. Full timeline per chat.
- Universal chat membership — joining any chat (public, private, official) now creates a DB membership record. Previously only private chats via invite created DB members.
GET /v1/notifications— list user notifications (paginated)GET /v1/notifications/unread— unread count for badgePUT /v1/notifications/{id}/read— mark as readPUT /v1/notifications/read-all— mark all as readPOST /v1/admin/notifications— create targeted or broadcast notificationDELETE /v1/admin/notifications/{id}— delete notificationGET /v1/portal/chats/{room_id}/activity— chat membership activity log
notification:new— real-time push when admin sends a notification
NOTIFICATION_CREATE (801),NOTIFICATION_BROADCAST (802),NOTIFICATION_DELETE (803)
notifications— user notifications with type, read state, and senderroom_membership_history— join/leave/kick/ban events per chat
- Mail icon in sidebar footer with unread dot
- Inbox panel with All/Unread filter tabs
- Notification detail modal with colored header, type badge, rich body rendering
- Session history: grouped by day, separate detail popup modal with map
- Avatar picker: reset glyph to Discord pfp, cleaner grid layout
- Account section: key-value layout with colored role/plan values
- Removed logout from sidebar (lives in settings only)
v1.6.0
Bot-Initiated Auth, Bulk Deletion, UI Overhaul New features:- Bot-initiated login — new
POST /v1/authentication/bot/send-codeendpoint lets the Discord bot generate login codes directly via/otp. Skips the web request step for returning users. Separate Redis key pools per login source prevent code collisions. - Fizzbuzz web login — after every 5 bot logins, users must log in via the website once. Configurable via
BOT_LOGIN_WEB_REQUIRED_EVERYconstant. - Bulk message deletion — two new endpoints:
DELETE /v1/portal/chats/{room_id}/messages/mine(delete all your own messages) andDELETE /v1/portal/chats/{room_id}/messages/last/{count}(mods+ delete last 1–100 messages). Both broadcastmessage:bulk_deletedsocket event. - Public health endpoint enriched —
GET /v1/healthnow returnssource_hashanduptime_seconds.
POST /v1/authentication/bot/send-code— Bot-initiated login code generationDELETE /v1/portal/chats/{room_id}/messages/mine— Delete all own messages in a chatDELETE /v1/portal/chats/{room_id}/messages/last/{count}— Delete last N messages (mod+)
message:bulk_deleted— Payload:{room_id, deleted_ids[], deleted_by}
- Removed neko-rooms public exposure from HAProxy
- Disabled Swagger/OpenAPI in production (
docs_url=None) - Login source tracking (
LoginSourceenum:web/bot) with separate Redis key pools
- Floating window layout (desktop
lg+) - Auth page: dynamic bot status gating, bot calling card with
/otpinstructions, invite link, OTP expiry with red expired state - Permission badge system (colored star icons replacing emoji crowns)
- User profile page: avatar picker, login history with Leaflet map, logout confirmation
- Chat settings redesign: tabbed layout with member grid, auto-wipe modal, invite management
- API source hash displayed under Portal title with clickable stats popup (status, database, cache, uptime)
- Vanity URL routing with
useParams+window.history.replaceState
v1.6.0
Per-Session E2EE Keypairs (Multi-Device) Replaces the old “one keypair per user” model with per-session keypairs indexed by thepsession cookie. Multiple devices now coexist — no sync dance, no session eviction.
Changes:
- Keypair sync endpoints removed (
POST /v1/portal/keypair-sync/request|respond|claim). Each device generates and registers its own keypair on login. POST /v1/portal/register-keynow associates the pubkey with the current session cookie.- Chat key distribution fans out: the sender encrypts the shared chat key once per recipient session using
nacl.box. - Key ring is now
{user_id: {session_id: pubkey}}on the wire (nested). portal:key_added/portal:key_removedcarrysession_id. Removal withsession_id: nullmeans “drop every session of this user” (ban / full disconnect).- Presence is session-scoped —
portal:presenceandroom:membersemit one entry per active session so multi-device users show on both devices. - Session enforcement (
MAX_SESSIONS_PER_USER=1) is lifted.
POST /v1/portal/chats/{room_id}/messagescontinues to enforce server-side key possession (403 ERR_EPOCH_MISMATCH,403 ERR_NO_ROOM_KEY).
v1.4.1
Chat ID Migration Cleanup Breaking changes:room_joinsocket event now acceptsroom_id,vanity, orinvite_code(newRoomJoinInputunion model). Rawroom_namefor join is no longer supported.ROOM_ALREADY_EXISTSsocket error has been removed. Chat names are non-unique — multiple chats can share the same display name.
browser_settingsonCreateRoomRequest,UpdateRoomRequest, andRoomDetailResponseis now typed asBrowserConfig(wasdict[str, Any]).build_status_payloadreturnsWipeStatusPayloadmodel directly (callers serialize at emit boundary).
- Neko client cache eviction in
stop_roomnow runs before any I/O, preventing stale clients on partial failure.
v1.4.0
Chat Moderation + Hard MuteNote: The per-chat mute/unmute paths introduced here were later restructured in v1.8.0 — see the top entry for the current URL shape.Breaking changes:
POST /v1/portal/mute/{user_id}has been replaced by per-chat mute (current path:POST /v1/portal/chats/{room_id}/muteswithuser_idin body; see v1.8.0).POST /v1/portal/unmute/{user_id}has been replaced by per-chat unmute (current path:DELETE /v1/portal/chats/{room_id}/mutes/{user_id}; see v1.8.0).user:mutedanduser:unmutedsocket events now include aroom_idfield and are broadcast to the chat only (not globally).DELETE /v1/portal/chats/{room_id}/messages/{message_id}no longer requires Mod+ globally — it uses chat-based authorization.POST /v1/portal/chats/{room_id}/clearno longer requires Admin+ — chat owners can clear their own chats.
POST /v1/portal/chats/{room_id}/mutes— Mute a user in a specific chat (chat owner, MOD+ for public/official, ADMIN+ for any chat). User ID in JSON body.DELETE /v1/portal/chats/{room_id}/mutes/{user_id}— Unmute a user in a specific chat.POST /v1/portal/hard-mute/{user_id}— Hard mute a user across all chats (ADMIN+ only)POST /v1/portal/hard-unmute/{user_id}— Hard unmute a user (ADMIN+ only)
user:hard_muted— Broadcast when a user is hard muted globallyuser:hard_unmuted— Broadcast when a hard mute is removed
- Two-tier mute system: per-chat mutes (Redis key per chat+user) and hard mutes (global Redis key). Composite
is_muted()checks both in a single pipelined round trip. - New
can_moderate_room()permission helper — chat owner always, MOD+ for public/official, ADMIN+ for private chats. Distinct fromcan_manage_room()(which locks MODs out of private chats entirely). - New audit events:
PORTAL_HARD_MUTE (705),PORTAL_HARD_UNMUTE (706). - All path parameters now use
Annotatedtypes withStringConstraints—RoomName,MessageId,InviteCode,ValidEntityId— validated at the route level with regex and length constraints.
v1.3.2
Per-IP Pending Login Cap Security:- Each IP address is now limited to 5 pending login codes at a time. Exceeding this returns
429 Too Many RequestswithLoginError.IP_RATE_LIMITED. - Codes are tracked via a Redis sorted set per IP, automatically pruned on expiry. Verified or deleted codes immediately free a slot.
- Configurable via
MAX_PENDING_LOGINS_PER_IPconstant (default: 5).
v1.3.1
Simplified Signup Flow Breaking changes:POST /v1/authentication/verifyno longer accepts ausernamefield. The request body is now just{"code": "..."}.- The
LoginError.USERNAME_REQUIREDerror has been removed. - New users no longer choose a username during signup. The backend generates a random username automatically (
user_+ random hex). Users can change their username later viaPUT /v1/users/me.
v1.3.0
Groups to Chats Migration Breaking changes:- All group endpoints have been removed (
/groups/*,/admin/groups/*,/bot/groups/*,/statistics/groups) - The
Groupmodel,GroupService, andGroupStateenum have been deleted GET /users/me/groupshas been removed- Bot group lookup and update endpoints have been removed
GET /v1/admin/chats— List all chats (admin overview)GET /v1/admin/chats/{room_id}— Get chat detail with members and browser settingsPUT /v1/admin/chats/{room_id}— Update chat fields (name, visibility, vanity invite, browser settings)DELETE /v1/admin/chats/{room_id}— Hard-delete a chat (DB + Redis + neko container)POST /v1/admin/chats/{room_id}/transfer— Transfer chat ownership (Owner-only)
- Chats are now first-class DB entities (
chatstable) with persistent identity, membership, and browser settings RoomDBServicehandles all DB chat persistence (CRUD, membership, slot checks)RoomServicehandles Redis live state and neko container lifecycle- Chat creation is slot-checked per subscription tier (BASIC: 0, TRIAL/PREMIUM: 1, ADMIN: 5, OWNER: unlimited)
- Chat visibility:
is_public=true(admin/owner chats, visible to all) vsis_public=false(premium user chats, members only)
v1.2.0
Split Backend + Portal Architecture:- REST API and Socket.IO now run as separate processes on dedicated subdomains
- REST API:
portal-api.7331.org(port 8000) — FastAPI only - WebSocket:
portal-ws.7331.org(port 8001) — Socket.IO only (Starlette + python-socketio) - Both share the same Postgres and Redis. Cross-process emit via
AsyncRedisManager - IPC for commands (disconnect user, destroy chat) via Redis pub/sub
- Per-chat — every chat is its own channel, no global chat
- Chat system with named chats (DB-persisted, slot-checked per subscription tier)
- Two-tier invite system: vanity invites (permanent, DB) and ephemeral invites (Redis, TTL)
- End-to-end encrypted messages with per-chat key distribution
- Chat lifecycle: create, start, stop, destroy with idle auto-shutdown
- Virtual browser chats via neko-rooms orchestrator (Brave, Tor)
- Presence sync via Redis key (WS writes on connect/disconnect, REST reads for
/portal/status)
GET /v1/portal/status— Online count and user list (reads from Redis presence key)GET /v1/portal/chats/{room_id}/messages— Chat message historyDELETE /v1/portal/chats/{room_id}/messages/{message_id}— Delete messagePOST /v1/portal/mute/{user_id}— Mute user (replaced in v1.4.0)POST /v1/portal/unmute/{user_id}— Unmute user (replaced in v1.4.0)POST /v1/portal/chats/{room_id}/clear— Clear chat messagesGET /v1/portal/chats/{room_id}/screenshot— Chat screenshotGET /v1/portal/vanity/{slug}— Resolve vanity invite
v1.1.0
API Gap Remediation New endpoints:GET /statistics/bots— Bot count statistics by verification statusGET /admin/subscriptions/active— List all active subscriptionsGET /admin/punishments/search— Cross-entity punishment search with filtersGET /admin/punishments/{id}— Get a single punishment by IDGET /admin/punishments/stats— Punishment analytics (counts by type over N days)PUT /admin/bots/{bot_id}/update— Update bot name, owner, rate limit, and bot_type (replaces separate verify/unverify endpoints)POST /admin/bots/{bot_id}/regenerate-key— Rotate a bot’s API keyPOST /admin/bots/{bot_id}/reset-stats— Reset bot cached statistics
Note: Bot search is available at GET /bots/search (public endpoint with permission-aware responses — admins see extended detail, regular users see public profiles). There is no separate admin-only bot search endpoint.
Permission changes:
- User search, user detail, and audit logs now accessible to moderators
- Moderators see only users below their permission level
- Moderators see only warn/timeout events in audit logs
- Audit logging added to bot-initiated user updates
- Pagination params normalized to
offset/limitacross all endpoints - All service methods now return typed models (no more
dict[str, Any])
v1.0.0
Initial Release- User authentication via login codes
- User profile and subscription management
- Bot registration and statistics tracking
- Batch command reporting for bots
- Admin panel with user/bot management
- Punishment system (ban, warn, timeout, unban)
- Subscription granting and revocation
- Audit logging for all admin actions
- Rate limiting per bot API key
- Redis caching layer for bot stats
