28 KiB
💋 SexyChat (Aphrodite)
A real-time encrypted chat platform with an AI companion, role-based moderation, and a glassmorphism UI. Built with Flask-SocketIO, AES-GCM encryption, and a local Ollama-powered AI named Violet.
Table of Contents
- Features
- Architecture
- Project Structure
- Setup & Installation
- Configuration
- Running the Server
- Database Models
- Socket Events Reference
- REST API Endpoints
- Role System & Admin Panel
- Encryption & Security
- Theme System
- Settings Panel
- Violet AI Companion
- Premium / Paywall
Features
Core Chat
- Public Lobby — Real-time ephemeral group chat. Messages are never persisted; they live only in connected clients' browsers.
- Private Messaging — AES-GCM-256 encrypted PMs with persistent history (up to 500 messages per conversation).
- Guest Mode — Join and chat anonymously without an account.
- Registration & Login — Bcrypt password hashing, JWT token auth (7-day expiry), automatic session restore on page refresh.
- Rate Limiting — 6 messages per 5-second window per session.
Violet AI Companion
- Local Ollama inference (
sadiq-bd/llama3.2-3b-uncensored:latestby default). - Transit-encrypted messages — decrypted server-side only for inference, never stored in plaintext.
- Per-user conversation memory (last 20 turns), persisted across sessions.
/resetcommand to clear. - Free trial (configurable, default: 3 messages), then paywall — admins can grant unlimited access.
- Real-time typing indicator while Violet is generating a response.
- Friendly signup prompt for unregistered guests who try to PM Violet.
Moderation & Roles
- 4-tier role hierarchy:
root(👑) >admin(⚔️) >mod(🛡️) >user - Kick, ban (username + IP), kickban, mute/unmute, verify/unverify.
- Persistent bans and mutes survive server restarts (stored in DB, loaded on startup).
- Admin panel with three tabs: Users, Bans, Mutes.
- Privilege escalation prevention — you can't modify users at or above your own power level.
User Features
- Persistent ignore list (stealth blocking — ignored users receive no indication).
- Context menu (right-click a username) for PM, ignore, and mod actions.
- Settings panel with four tabs: Account, Chat, Violet, Premium.
- Password change from settings (requires current password).
- Unread PM indicators on conversation tabs.
UI/UX
- Glassmorphism dark theme with CSS custom property theming.
- 10 color themes (dark, light, neon, cyberpunk, and more).
- Responsive mobile layout with collapsible sidebar.
- Auto-expanding message textarea.
- Enter-to-send toggle (configurable).
- Google Fonts (Inter + Outfit).
Architecture
┌─────────────┐ WebSocket / HTTP ┌──────────────────────────────┐
│ Browser │ ◄────────────────────► │ Gunicorn + Eventlet │
│ (chat.js │ │ Flask-SocketIO │
│ crypto.js) │ │ │
└─────────────┘ │ ┌────────────────────────┐ │
│ │ REST Blueprint │ │
│ │ (routes.py) │ │
│ └────────────────────────┘ │
│ │
│ ┌────────────────────────┐ │
│ │ AI Worker (greenlet) │ │
│ │ Serialized Ollama queue │ │
│ └───────────┬────────────┘ │
└──────────────┼───────────────┘
│
┌──────────────▼───────────────┐
│ Ollama (localhost:11434) │
│ LLaMA 3.2 3B Uncensored │
└──────────────────────────────┘
│
┌──────────────▼───────────────┐
│ SQLite (instance/sexchat.db)│
│ Users, Messages, Bans, │
│ Mutes, VioletHistory │
└──────────────────────────────┘
Key Design Decisions
- Single-process, single-worker — Eventlet async mode with one Gunicorn worker. All in-memory state (connected users, rooms, rate limits) lives in-process.
- AI inference queue — A dedicated eventlet greenlet serialises Ollama requests one at a time, broadcasting
violet_typingindicators while busy. Runs within Flask app context for DB access. - Lobby is ephemeral — No lobby messages are ever written to the database.
- PMs are encrypted at rest — The server stores only AES-GCM ciphertext and nonces for registered-user PMs.
- Shared config module —
config.pycentralises all constants, config loading, AES-GCM helpers, and JWT helpers soapp.pyandroutes.pyshare a single source of truth.
Tech Stack
| Layer | Technology |
|---|---|
| Web server | Gunicorn 21.x with eventlet worker |
| Framework | Flask 3.x + Flask-SocketIO 5.x |
| Concurrency | Eventlet (monkey-patched greenlets) |
| Database | SQLite (default) or PostgreSQL via DATABASE_URL |
| ORM | SQLAlchemy 3.x + Flask-Migrate 4.x (Alembic) |
| AI | Ollama HTTP API (local inference) |
| Auth | JWT (PyJWT 2.x) + bcrypt 4.x |
| Encryption | AES-GCM-256 (server: cryptography 42.x, client: Web Crypto API) |
| Frontend | Vanilla JS, Socket.IO 4 client, custom CSS |
| Scaling | Optional Redis message queue for multi-worker deployments |
Project Structure
aprhodite/
├── app.py # Flask-SocketIO app: socket events, AI worker, admin panel, moderation
├── config.py # Shared config loader, constants, AES-GCM helpers, JWT helpers, Violet prompt
├── database.py # SQLAlchemy + Flask-Migrate init, Violet bot user seeding
├── models.py # ORM models: User, Message, Ban, Mute, UserIgnore, VioletHistory
├── routes.py # REST API blueprint: auth, PM history, AI message, payment webhook
├── start.py # Process manager: start / stop / restart / status / debug
├── gunicorn.conf.py # Gunicorn configuration (workers, bind, worker class)
├── config.json # Runtime secrets & overrides (gitignored)
├── requirements.txt # Python dependencies
├── index.html # Single-page app: join screen, chat, settings modal, admin panel modal
├── static/
│ ├── chat.js # Frontend logic: socket events, PMs, settings, admin panel, themes
│ ├── crypto.js # Client-side AES-GCM encryption (PBKDF2 key derivation, SubtleCrypto)
│ ├── style.css # All styles: glassmorphism, 10 themes, admin panel, responsive layout
│ └── socket.io.min.js# Socket.IO v4 client library
└── instance/
└── sexchat.db # SQLite database (auto-created on first run)
Setup & Installation
Prerequisites
- Python 3.11+
- Ollama (required for Violet AI)
Install
git clone https://git.computertech.dev/lord3nd3r/aprhodite.git
cd aprhodite
python -m venv venv
source venv/bin/activate
pip install -r requirements.txt
Pull the AI Model
ollama pull sadiq-bd/llama3.2-3b-uncensored:latest
Create config.json
{
"SECRET_KEY": "your-random-secret-key",
"JWT_SECRET": "your-random-jwt-secret",
"ADMIN_PASSWORD": "a-strong-moderator-password",
"AI_FREE_LIMIT": 3,
"HOST": "0.0.0.0",
"PORT": 5000
}
Tip: Generate secrets with
python -c "import uuid; print(uuid.uuid4().hex)".
Database
On first run, database.py calls db.create_all() and seeds the Violet bot user automatically. No manual migration is needed for a fresh install.
For schema changes after the initial setup:
flask db migrate -m "description of changes"
flask db upgrade
Configuration
Configuration is resolved with the following precedence: Environment Variable → config.json → Default Value.
All config loading is centralised in config.py.
| Key | Default | Description |
|---|---|---|
SECRET_KEY |
Random UUID hex | Flask session secret |
JWT_SECRET |
Random UUID hex | JWT HMAC signing key |
ADMIN_PASSWORD |
"admin1234" |
Shared mod password for guest elevation |
DATABASE_URL |
"sqlite:///sexchat.db" |
SQLAlchemy database URI |
PAYMENT_SECRET |
"change-me-payment-secret" |
Payment webhook validation secret |
CORS_ORIGINS |
None |
Socket.IO CORS allowlist |
HOST |
"0.0.0.0" |
Server bind address |
PORT |
5000 |
Server bind port |
OLLAMA_URL |
"http://localhost:11434" |
Ollama API endpoint |
VIOLET_MODEL |
"sadiq-bd/llama3.2-3b-uncensored:latest" |
Ollama model tag for Violet |
AI_FREE_LIMIT |
3 |
Free Violet messages before paywall |
SOCKETIO_MESSAGE_QUEUE / REDIS_URL |
None |
Redis URL for multi-worker Socket.IO |
Hardcoded Constants (in config.py)
| Constant | Value | Description |
|---|---|---|
MAX_MSG_LEN |
500 chars | Maximum message length |
MAX_HISTORY |
500 messages | PM history cap per conversation pair |
JWT_EXPIRY_DAYS |
7 days | JWT token lifespan |
AI_BOT_NAME |
"Violet" |
AI bot display name |
PBKDF2_ITERATIONS |
100,000 | Client-side key derivation iterations (crypto.js) |
| Rate limit | 6 msgs / 5 sec | Per-session lobby rate limit (app.py) |
MAX_HISTORY_PER_USER |
20 turns | Violet conversation memory depth (app.py) |
Running the Server
Debug Mode (foreground, full logging)
python start.py debug
Daemon Mode
python start.py start # Start in background (PID written to .pid file)
python start.py stop # Graceful shutdown
python start.py restart # Stop + start
python start.py status # Check if running
Direct Gunicorn
gunicorn --worker-class eventlet -w 1 --bind 0.0.0.0:5000 start:application
The server will be available at http://localhost:5000.
Database Models
Six models defined in models.py. All timestamps use datetime.now(timezone.utc).
users
| Column | Type | Description |
|---|---|---|
id |
Integer, PK | Auto-increment |
username |
String(20), unique, indexed | Alphanumeric + - + _ |
password_hash |
String(128) | Bcrypt hash |
email |
String(255), unique, nullable | Optional email |
role |
String(10), default "user" |
root, admin, mod, or user |
has_ai_access |
Boolean, default False |
Unlimited Violet access flag |
ai_messages_used |
Integer, default 0 |
Free trial counter |
is_verified |
Boolean, default False |
Manual verification status |
created_at |
DateTime | UTC timestamp |
messages (Encrypted PM History)
| Column | Type | Description |
|---|---|---|
id |
Integer, PK | |
sender_id |
FK → users.id |
|
recipient_id |
FK → users.id |
|
encrypted_content |
Text | Base64-encoded AES-GCM ciphertext |
nonce |
String(64) | Base64-encoded 12-byte IV |
timestamp |
DateTime | Indexed with sender/recipient for query performance |
bans
| Column | Type | Description |
|---|---|---|
id |
Integer, PK | |
username |
String(20), indexed | Banned username (lowercase) |
ip |
String(45), nullable, indexed | Banned IP address |
reason |
String(255), nullable | Ban reason |
created_at |
DateTime |
mutes
| Column | Type | Description |
|---|---|---|
id |
Integer, PK | |
username |
String(20), unique, indexed | Muted username |
created_at |
DateTime |
user_ignores
| Column | Type | Description |
|---|---|---|
id |
Integer, PK | |
ignorer_id |
FK → users.id |
User doing the ignoring |
ignored_id |
FK → users.id |
User being ignored |
created_at |
DateTime |
Unique composite index on (ignorer_id, ignored_id).
violet_history
| Column | Type | Description |
|---|---|---|
id |
Integer, PK | |
user_id |
FK → users.id |
Owning user |
role |
String(10) | "user" or "assistant" |
text |
Text | Plaintext conversation turn |
timestamp |
DateTime |
Socket Events Reference
Connection & Auth
| Direction | Event | Data | Description |
|---|---|---|---|
| C→S | join |
{ mode, username, password?, email?, mod_password? } |
Authenticate and enter lobby. mode: guest, login, register, or restore. |
| S→C | joined |
{ username, is_admin, role, is_registered, has_ai_access, ai_messages_used, email?, token?, ignored_list? } |
Auth success — includes JWT token for session restore. |
| S→C | error |
{ msg } |
Error message. |
| S→C | kicked |
{ msg } |
User was kicked/banned. |
Lobby Chat
| Direction | Event | Data | Description |
|---|---|---|---|
| C→S | message |
{ text } |
Send lobby message (rate-limited). |
| S→C | message |
{ username, text, is_admin, is_registered, ts } |
Broadcast lobby message. |
| S→C | system |
{ msg, ts } |
System notification (joins, parts, mod actions). |
| S→C | nicklist |
{ users: [{ username, is_admin, is_registered, is_verified, role }] } |
Full online user list (includes Violet bot). |
Private Messaging
| Direction | Event | Data | Description |
|---|---|---|---|
| C→S | pm_open |
{ target } |
Initiate PM with target user. |
| C→S | pm_accept |
{ room } |
Accept an incoming PM invitation (validated against pending invites). |
| C→S | pm_message |
{ room, text? } or { room, ciphertext, nonce, transit_key? } |
Send PM — plaintext (guests) or encrypted (registered). Include transit_key for Violet. |
| S→C | pm_invite |
{ from, room, room_key } |
Incoming PM invitation with server-derived room key. |
| S→C | pm_ready |
{ with, room, room_key } |
PM room opened — sent to the initiator. |
| S→C | pm_message |
{ from, text?, ciphertext?, nonce?, room, ts } |
Receive a PM. |
Violet AI
| Direction | Event | Data | Description |
|---|---|---|---|
| C→S | pm_message (to Violet room) |
{ room, ciphertext, nonce, transit_key } |
Encrypted message routed to Violet via AI worker. |
| C→S | violet_reset |
(none) | Clear your conversation memory with Violet. |
| S→C | violet_typing |
{ busy, room? } |
AI processing indicator (shown while Ollama generates). |
| S→C | ai_response |
{ error: "ai_limit_reached", room } |
Free trial exhausted. |
| S→C | ai_unlock |
{ has_ai_access, msg? } |
Premium access granted or revoked (pushed live). |
Moderation
| Direction | Event | Data | Description |
|---|---|---|---|
| C→S | mod_kick |
{ target } |
Kick user (mod+). |
| C→S | mod_ban |
{ target } |
Ban username + IP (mod+). |
| C→S | mod_kickban |
{ target } |
Kick + ban simultaneously (mod+). |
| C→S | mod_mute |
{ target } |
Toggle mute on user (mod+). |
| C→S | mod_verify |
{ target } |
Toggle verification on user (mod+). |
User Actions
| Direction | Event | Data | Description |
|---|---|---|---|
| C→S | user_ignore |
{ target } |
Ignore a user (registered only). |
| C→S | user_unignore |
{ target } |
Unignore a user. |
| C→S | change_password |
{ old_password, new_password } |
Change password (6-char minimum). |
| S→C | ignore_status |
{ target, ignored } |
Ignore list update confirmation. |
| S→C | password_changed |
{ success, msg? } |
Password change result. |
| S→C | role_updated |
{ role } |
Live notification when your role is changed by an admin. |
Admin Panel
| Direction | Event | Data | Description |
|---|---|---|---|
| C→S | admin_get_users |
(none) | Request full user list (mod+). |
| C→S | admin_get_bans |
(none) | Request ban list (mod+). |
| C→S | admin_get_mutes |
(none) | Request mute list (mod+). |
| C→S | admin_set_role |
{ user_id, role } |
Change a user's role (admin+ only, can't set ≥ own role). |
| C→S | admin_verify_user |
{ user_id } |
Toggle verification (mod+). |
| C→S | admin_toggle_ai |
{ user_id } |
Toggle unlimited AI access (admin+). Notifies user in real-time. |
| C→S | admin_unban |
{ ban_id } |
Remove a ban (mod+). |
| C→S | admin_unmute |
{ mute_id } |
Remove a mute (mod+). |
| S→C | admin_users |
{ users: [{ id, username, role, is_verified, has_ai_access, email, created_at, online }] } |
User list with online status. |
| S→C | admin_bans |
{ bans: [{ id, username, ip?, reason?, created_at }] } |
Ban list. |
| S→C | admin_mutes |
{ mutes: [{ id, username, created_at }] } |
Mute list. |
| S→C | admin_action_ok |
{ msg } |
Action success toast (panel auto-refreshes). |
REST API Endpoints
All endpoints are under the /api prefix.
POST /api/auth/register
Create a new account.
// Request
{ "username": "alice", "password": "hunter2", "email": "alice@example.com" }
// Response 201
{
"token": "eyJ...",
"user": { "id": 3, "username": "alice", "has_ai_access": false, "ai_messages_used": 0 }
}
POST /api/auth/login
Authenticate and receive a JWT.
// Request
{ "username": "alice", "password": "hunter2" }
// Response 200
{ "token": "eyJ...", "user": { "id": 3, "username": "alice", ... } }
GET /api/pm/history?with={username}
Fetch encrypted PM history with another user. Requires Authorization: Bearer <jwt>.
// Response 200
{
"room_key": "base64...",
"messages": [
{ "from_me": true, "ciphertext": "...", "nonce": "...", "ts": "2025-04-12T..." },
{ "from_me": false, "ciphertext": "...", "nonce": "...", "ts": "2025-04-12T..." }
]
}
POST /api/ai/message
Send an encrypted message to Violet via REST (alternative to the socket path).
// Request (Authorization: Bearer <jwt>)
{ "ciphertext": "...", "nonce": "...", "transit_key": "..." }
// Response 200
{ "ciphertext": "...", "nonce": "...", "ai_messages_used": 2, "has_ai_access": false }
POST /api/payment/success
Payment webhook (server-to-server). Validates secret with constant-time HMAC comparison.
// Request
{ "secret": "PAYMENT_SECRET", "user_id": 3 }
// Response 200
{ "status": "ok", "has_ai_access": true }
Role System & Admin Panel
Hierarchy
| Role | Power Level | Badge | Capabilities |
|---|---|---|---|
root |
3 | 👑 | Everything. Can promote to admin. Cannot be demoted. |
admin |
2 | ⚔️ | Set roles (mod/user), toggle AI access, all mod actions. |
mod |
1 | 🛡️ | Kick, ban, mute, verify, view admin panel. |
user |
0 | — | Standard chat, PMs, Violet AI (free trial). |
Rules:
- You cannot modify a user whose power level is ≥ your own.
- You cannot assign a role whose power level is ≥ your own.
- The first registered human user should be set to
rootin the database. - Legacy: Guests can enter the shared mod password at login to gain temporary
modsession access.
Admin Panel
Accessible via the shield icon (🛡️) in the header (visible to mod and above). Three tabs:
| Tab | Features |
|---|---|
| Users | Searchable user list. Change roles (dropdown), toggle verification (checkmark), grant/revoke AI access (brain icon). Shows online status. |
| Bans | All active bans with username, IP, and date. One-click unban button. |
| Mutes | All active mutes. One-click unmute button. |
All actions trigger a system message in the lobby, auto-refresh the panel, and (where applicable) push live updates to the affected user's session.
Encryption & Security
PM Encryption (AES-GCM-256)
1. Server generates a deterministic room key: HMAC-SHA256(JWT_SECRET, room_name)
2. Room key is delivered to both clients via pm_invite / pm_ready events
3. Client encrypts each message with AES-GCM-256 using the room key + random 12-byte nonce
4. Server stores only base64(ciphertext) + base64(nonce) — never sees plaintext
5. Recipient decrypts client-side with the same room key
Violet AI Transit Encryption
1. Client generates a random AES-256 transit key
2. Client encrypts message with transit key → sends { ciphertext, nonce, transit_key }
3. Server decrypts with transit key → sends plaintext to Ollama for inference
4. Server encrypts Violet's response with the same transit key → delivers to client
5. Transit key is ephemeral — used only in memory, never stored
Security Measures
| Category | Implementation |
|---|---|
| Password hashing | Bcrypt with adaptive cost factor |
| Authentication | JWT tokens (HMAC-SHA256), 7-day expiry |
| Session restore | JWT stored in localStorage, validated on reconnect |
| Input validation | Regex-enforced usernames ([a-zA-Z0-9_-], 1–20 chars), message length limits, SQLAlchemy parameterized queries |
| Rate limiting | 6 messages per 5-second window per session |
| RBAC | @_require_role() decorator with power-level hierarchy checks |
| Webhook auth | Constant-time HMAC comparison for payment secret |
| Ban persistence | Username + IP stored in DB bans table, loaded into memory on startup |
| Mute persistence | Stored in DB mutes table, loaded on startup |
| PM invite validation | Server-side pending invite mapping — pm_accept only works for legitimately invited rooms |
Theme System
10 themes available in Settings → Chat → Theme. Each theme is a set of CSS custom properties applied via the data-theme attribute on the document root.
| Theme | Style | Palette |
|---|---|---|
| Midnight Purple | Dark | Deep purple + neon magenta (default) |
| Crimson Noir | Dark | Black + crimson red |
| Ocean Deep | Dark | Navy + cyan / teal |
| Ember | Dark | Dark brown + orange / amber |
| Neon Green | Dark | Black + bright lime |
| Cyberpunk | Dark | Purple + electric yellow |
| Rose Gold | Dark | Dark rose + blush pink |
| Arctic | Light | Cool blue-white + indigo accents |
| Daylight | Light | White + pink / purple accents |
| Midnight Blue | Dark | Deep navy + blue glow |
Theme preference is saved to localStorage (sc_theme) and applied immediately on page load — no flash of default theme.
Settings Panel
Accessed via the gear icon (⚙️) in the header. Four tabs:
Account
- View your username and email.
- Change password (requires current password, 6-character minimum for the new one).
Chat
- Theme — 10-option grid with gradient color swatches.
- Font Size — 12–20px slider.
- Timestamp Format — 12-hour / 24-hour toggle.
- Enter to Send — On/Off. When off, Enter inserts a newline and Shift+Enter sends.
- Notification Sounds — On/Off.
Violet
- AI access status display (Unlimited ✓ or X remaining free messages).
- Messages used counter.
- Reset conversation memory button.
Premium
- Feature showcase (Unlimited Violet, Encrypted PMs, Priority Access, Custom Themes).
- Pricing display ($10/month).
- Coming soon placeholder.
All preferences are persisted in localStorage with the sc_ prefix (e.g., sc_theme, sc_fontSize, sc_enterSend).
Violet AI Companion
Violet is an AI chat companion powered by a local Ollama model. She's configured as a "flirtatious and sophisticated nightclub hostess at an exclusive, dimly-lit members-only club" via her system prompt in config.py.
How It Works
- User clicks Violet in the nicklist → PM room opens automatically (no invite needed for the bot).
- User's message is transit-encrypted (AES-GCM) and queued to a single-threaded inference greenlet.
- The AI worker decrypts the message, builds a prompt from the last 20 conversation turns (
violet_historytable), and calls Ollama's/api/generateendpoint. - The response is transit-encrypted with the same key and delivered as a PM back to the user.
- Both the user's message and Violet's reply are saved to
violet_historyfor context continuity across sessions.
Commands
| Command | Description |
|---|---|
/reset |
Clear Violet's memory of your conversation (deletes all violet_history rows for your user). |
Guest Behavior
Unregistered guests who PM Violet receive a friendly plaintext reply:
"Hey hun 💜 You'll need to register an account before we can chat privately. Go back to the join screen and sign up — I'll be waiting for you! 😘"
Free Trial & Paywall
Users get AI_FREE_LIMIT (default: 3) free messages. After that, a paywall modal is shown. Admins can grant unlimited access via the admin panel's "Grant AI" button — the user is notified in real-time via ai_unlock.
Fallback
If Ollama is unreachable or returns an error, Violet sends a graceful fallback message instead of crashing.
Premium / Paywall
When a user exhausts their free Violet messages, a paywall modal appears with pricing and feature highlights. Premium access (has_ai_access = True) can be granted two ways:
- Admin Panel — Any
admin+ can click the brain icon (🧠) on a user in the Users tab to toggle unlimited AI access. The user receives a real-time notification. - Payment Webhook —
POST /api/payment/successwith the payment secret and user ID. Validated with constant-time comparison.
Premium unlocks unlimited Violet messages. The ai_unlock event is pushed to the user's active socket session immediately.
Note: The payment flow is currently a stub. For production, replace the secret-comparison logic with a proper payment provider webhook (e.g., Stripe's
stripe.Webhook.construct_event()).
Dependencies
From requirements.txt:
| Package | Version | Purpose |
|---|---|---|
flask |
≥3.0 | Web framework |
flask-socketio |
≥5.3 | WebSocket support |
eventlet |
≥0.35 | Async concurrency |
gunicorn |
≥21.0 | Production WSGI server |
flask-sqlalchemy |
≥3.1 | ORM |
flask-migrate |
≥4.0 | Database migrations (Alembic) |
psycopg2-binary |
≥2.9 | PostgreSQL driver (optional) |
bcrypt |
≥4.0 | Password hashing |
PyJWT |
≥2.8 | JWT auth tokens |
cryptography |
≥42.0 | Server-side AES-GCM |
redis |
≥5.0 | Socket.IO multi-worker adapter (optional) |
requests |
≥2.31 | Ollama HTTP client |
License
Private project. All rights reserved.
Credits
Built by End3r — git.computertech.dev/lord3nd3r/aprhodite